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.
@@ -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 readiness rule as store-local.readyQuests, over the remote list
272
- // including the epic gate: a quest with any non-terminal child (a quest whose
273
- // parent points at it, not in complete/cancelled) is never ready. Epics are
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
- const all = listQuests(repo, env);
278
- const done = new Set(all.filter((q) => q.status === "complete").map((q) => q.id));
279
- const openParents = new Set(
280
- all.filter((q) => q.parent !== undefined && q.status !== "complete" && q.status !== "cancelled").map((q) => q.parent),
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
 
@@ -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
- const all = listQuests(storeDir);
87
- const done = new Set(all.filter((q) => q.status === "complete").map((q) => q.id));
88
- // Epic gate: a quest with any non-terminal child (a quest whose parent points
89
- // at it, not in complete/cancelled) is never ready. Epics are closed by the
90
- // orchestrator inline (see $quest:orchestrate), never auto-dispatched. A
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.4",
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 --ready --json # the dispatch queue (deps met, priority order)
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 ready quest per its record:
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 --ready` empties and nothing is in flight:
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**: `quest list --ready` gates it out while any child is
83
- non-terminal, and `quest-run --ready` refuses it even once every child is
84
- terminal (a direct `quest-run <id>` on an epic still runs, but don't — it burns
85
- a worker on pure verification). Close it inline yourself, spending zero worker
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 `quest list --ready`.
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 --ready --json # → [{"id":12,"worker":"claude"…},{"id":13,"worker":"codex"…}]
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 →
@@ -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`, set the orchestrator goal for the wave, then spawn
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 --ready` becomes the
50
- dispatch queue that is the whole wave mechanic.
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`.
@@ -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 conflicts, init fails before creating `.quests/`; inspect the
23
- file, run the explicit provider install command with `--force` only when you
24
- intend replacement, then rerun `quest init`.
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. Symlinked
74
- agent template files can be written through with `--force`; keep the provider
75
- agent directories themselves as real directories.
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