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 CHANGED
@@ -4,7 +4,41 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
5
5
  [SemVer](https://semver.org/).
6
6
 
7
- ## Unreleased
7
+ ## [0.3.6] — 2026-07-09
8
+
9
+ ### Added
10
+ - `quest list --queue` now exposes worker-ready quests, inline-close-ready epics,
11
+ and blocked queue reasons as separate orchestration queues.
12
+ - `quest lint --all` now reports missing parent and `depends_on` references
13
+ across local and GitHub-backed stores.
14
+ - Provider `work --dry-run` commands now print native subagent handoffs or the
15
+ `quest-run` fallback command after running setup health checks.
16
+
17
+ ### Changed
18
+ - Session-start status output and command help are more concise and focused on
19
+ the next operator action.
20
+ - `$quest:plan` and `$quest:orchestrate` now document the v2 queue workflow and
21
+ explicit provider/model/effort handoffs more precisely.
22
+
23
+ ### Fixed
24
+ - Malformed hand-edited `depends_on` metadata now stays a normal contract or
25
+ queue-blocking problem instead of crashing graph lint or entering
26
+ `worker_ready`.
27
+ - Headless Codex fallback handoff commands now quote Quest binary paths that
28
+ contain spaces.
29
+
30
+ ## [0.3.5] — 2026-07-08
31
+
32
+ ### Added
33
+ - `quest codex doctor --fix` and `quest claude doctor --fix` now repair
34
+ Quest-owned native-agent templates and provider plugin install/version state,
35
+ then rerun the same doctor checks.
36
+ - `quest codex open` and `quest claude open` now run the provider health gate
37
+ before launching the interactive provider from the project root.
38
+
39
+ ### Changed
40
+ - Stale Quest agent templates are replaced without `--force`; unrelated custom
41
+ files still require explicit replacement.
8
42
 
9
43
  ## [0.3.4] — 2026-07-08
10
44
 
package/README.md CHANGED
@@ -57,8 +57,8 @@ Install Quest's project-scoped Claude agent templates and verify the local
57
57
  Claude setup with:
58
58
 
59
59
  ```bash
60
- quest claude install-agents --scope project
61
- quest claude doctor
60
+ quest claude doctor --fix
61
+ quest claude open
62
62
  ```
63
63
 
64
64
  For local development against a checkout, point Claude Code at the repo directly
@@ -84,16 +84,19 @@ Verify the install with:
84
84
 
85
85
  ```bash
86
86
  codex plugin list --marketplace quest
87
- quest codex install-agents --scope project
88
- quest codex doctor
87
+ quest codex doctor --fix
88
+ quest codex open
89
89
  codex debug prompt-input "noop"
90
90
  ```
91
91
 
92
- `quest codex doctor` checks the installed plugin version, Codex `multi_agent`
93
- support, hook parser, neutral skill roots, and installed native-agent templates.
94
- `codex debug prompt-input "noop"` should not print any hook parse warnings.
92
+ `quest codex doctor --fix` and `quest claude doctor --fix` install or refresh
93
+ Quest-owned native-agent templates and repair the provider plugin install/version
94
+ when the provider CLI supports it, then rerun doctor. `quest codex open` and
95
+ `quest claude open` run the same health gate before launching the interactive
96
+ provider. `codex debug prompt-input "noop"` should not print any hook parse
97
+ warnings.
95
98
 
96
- #### Updating the Codex plugin
99
+ #### Updating provider plugins
97
100
 
98
101
  Codex installs plugins from a marketplace snapshot. Pulling this Git repo or
99
102
  publishing a new tag is not enough to update the already-installed plugin cache.
@@ -103,10 +106,18 @@ Refresh the marketplace snapshot, then reinstall the plugin from it:
103
106
  codex plugin marketplace upgrade quest
104
107
  codex plugin add quest@quest
105
108
  codex plugin list --marketplace quest
106
- quest codex doctor
109
+ quest codex doctor --fix
107
110
  ```
108
111
 
109
- Then start a new Codex thread. If the update contains hook changes, re-run:
112
+ Claude updates through its plugin manager:
113
+
114
+ ```bash
115
+ claude plugin update quest@quest
116
+ quest claude doctor --fix
117
+ ```
118
+
119
+ Then start a new provider session. If the Codex update contains hook changes,
120
+ re-run:
110
121
 
111
122
  ```bash
112
123
  codex debug prompt-input "noop"
@@ -117,7 +128,8 @@ you drive it from Claude Code, Codex, or a plain shell.
117
128
 
118
129
  ## Quickstart
119
130
 
120
- Create a store in your project, author a quest, check it, and work it:
131
+ Create a store in your project, author a quest, check it, then use the compact
132
+ dashboard to pick the next action:
121
133
 
122
134
  ```bash
123
135
  # 1. Create a quest store (.quests/) here
@@ -130,11 +142,14 @@ quest create --title "Add dark mode" \
130
142
  --done-when "\`npm test\` passes including new theme tests" \
131
143
  --validation "npm test"
132
144
 
133
- # 3. Check it against the contract spec
134
- quest lint --all
145
+ # 3. Check the new quest against the contract spec
146
+ quest lint 1
147
+
148
+ # 4. Show counts, active work, ready work, and the next command
149
+ quest
135
150
 
136
- # 4. See what's ready to work (dependencies met)
137
- quest list --ready
151
+ # 5. Read the quest before working it
152
+ quest show 1 --json
138
153
  ```
139
154
 
140
155
  By default, `quest init` also installs project-scoped native agent templates for
@@ -145,8 +160,9 @@ both providers:
145
160
  - `.claude/agents/quest-executor.md`
146
161
  - `.claude/agents/quest-reviewer.md`
147
162
 
148
- Use `quest init --no-agents` when you only want the `.quests/` store. If an
149
- existing project agent template would be replaced, init fails before creating
163
+ Use `quest init --no-agents` when you only want the `.quests/` store. Stale
164
+ Quest-owned agent templates are replaced automatically. If an existing file is
165
+ not recognizable as a Quest agent template, init fails before creating
150
166
  `.quests/`; inspect the conflicting files, run the explicit provider install
151
167
  command with `--force` only if you intend to replace them, then rerun
152
168
  `quest init`.
@@ -225,6 +241,7 @@ full base protocol lives in
225
241
 
226
242
  | Command | Purpose |
227
243
  |---|---|
244
+ | bare `quest` | Show a compact dashboard with counts, active work, ready work, active runs, and the next command |
228
245
  | `quest init` | Create a quest store (`.quests/`) and install project-scoped Codex/Claude agent templates by default |
229
246
  | `quest create` | Create a new quest (the only way records are born) |
230
247
  | `quest list` | List quests (filter by status, parent, or readiness) |
@@ -238,8 +255,8 @@ full base protocol lives in
238
255
  | `quest amend` | Append a numbered protocol amendment (retro output) |
239
256
  | `quest protocol` | Print the loop protocol + this store's local amendments |
240
257
  | `quest runs` | Show headless runner activity (from `.quests/runs.ndjson`) |
241
- | `quest codex` | Validate Codex-native setup and install Codex native agent templates |
242
- | `quest claude` | Validate Claude-native setup and install Claude native agent templates |
258
+ | `quest codex` | Validate, repair, install, or open Codex-native setup |
259
+ | `quest claude` | Validate, repair, install, or open Claude-native setup |
243
260
 
244
261
  ## quest-run (headless runner)
245
262
 
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  // SessionStart hook (startup / clear / compact). When a `.quests/` store exists
3
3
  // at or above the session's cwd, write a short one-paragraph summary of in-flight
4
- // quests and active runs to stdout — Claude Code injects stdout as session
5
- // context. No store, a non-local backend, an empty store, or ANY error → a silent
6
- // no-op (exit 0). Reads only; no network and no child processes, so it stays fast.
4
+ // local quests and active runs to stdout — Claude Code injects stdout as session
5
+ // context. GitHub-backed stores get a no-network hint instead of live issue
6
+ // counts. No store or ANY error → a silent no-op (exit 0). Reads only; no network
7
+ // and no child processes, so it stays fast.
7
8
 
8
9
  import { writeSync } from "node:fs";
9
10
  import { findStoreDir, loadConfig } from "../lib/config.mjs";
@@ -24,25 +25,31 @@ function activeRunCount(storeDir) {
24
25
  }
25
26
 
26
27
  // One paragraph (<= ~3 lines): counts by status, up to 3 in-flight quests, the
27
- // active-run count, and the ready-list hint. Returns null when there is nothing
28
- // worth injecting (empty store).
28
+ // active-run count, and one exact next command.
29
29
  function buildContext(storeDir) {
30
30
  const all = listQuests(storeDir);
31
- if (!all.length) return null;
31
+ if (!all.length) return "Quest store (.quests): local backend, no quests.\nNext: run `quest create --help`.";
32
32
  const counts = {};
33
33
  for (const q of all) counts[q.status] = (counts[q.status] ?? 0) + 1;
34
34
  const countStr = STATUS_ORDER.filter((s) => counts[s]).map((s) => `${counts[s]} ${s}`).join(", ");
35
- const inFlight = all
35
+ const inFlightQuests = all
36
36
  .filter((q) => q.status === "in_progress" || q.status === "blocked")
37
37
  .sort((a, b) => (a.status === b.status ? a.id - b.id : a.status === "in_progress" ? -1 : 1))
38
- .slice(0, 3)
39
- .map((q) => `#${q.id} ${q.title} [${q.status}]`);
38
+ .slice(0, 3);
39
+ const inFlight = inFlightQuests.map((q) => `#${q.id} ${q.title} [${q.status}]`);
40
40
  const lines = [`Quest store (.quests): ${all.length} quest${all.length === 1 ? "" : "s"} — ${countStr}.`];
41
41
  if (inFlight.length) lines.push(`In flight: ${inFlight.join("; ")}.`);
42
- lines.push(`Active runs: ${activeRunCount(storeDir)}. Ready to work next: \`quest list --ready\`.`);
42
+ const next = inFlightQuests[0] ? `quest show ${inFlightQuests[0].id} --json` : "quest list --ready";
43
+ lines.push(`Active runs: ${activeRunCount(storeDir)}. Next: run \`${next}\`.`);
43
44
  return lines.join("\n");
44
45
  }
45
46
 
47
+ function githubContext(config) {
48
+ const repo = config.github?.repo;
49
+ const scope = repo ? ` for ${repo}` : "";
50
+ return `Quest store (.quests): github backend${scope}. No GitHub request was made by this hook.\nNext: run \`quest list --ready\` when you need live issue state.`;
51
+ }
52
+
46
53
  try {
47
54
  const raw = await readStdin();
48
55
  let payload = {};
@@ -50,7 +57,12 @@ try {
50
57
  const cwd = typeof payload.cwd === "string" && payload.cwd ? payload.cwd : process.cwd();
51
58
  const storeDir = findStoreDir(cwd, process.env);
52
59
  if (!storeDir) process.exit(0); // no store here — silent no-op, NOT an error
53
- if (loadConfig(storeDir, process.env).backend !== "local") process.exit(0); // only local is readable today
60
+ const config = loadConfig(storeDir, process.env);
61
+ if (config.backend === "github") {
62
+ writeSync(1, githubContext(config) + "\n");
63
+ process.exit(0);
64
+ }
65
+ if (config.backend !== "local") process.exit(0);
54
66
  const context = buildContext(storeDir);
55
67
  if (context) writeSync(1, context + "\n");
56
68
  process.exit(0);
@@ -6,25 +6,29 @@
6
6
  // A subagent counts as a quest-executor iff one of its transcript entries records
7
7
  // an actual *mutating* quest invocation — `quest start <id>` or `quest checkpoint
8
8
  // <id>` (under any binary prefix: `quest`, `./bin/quest`, `node bin/quest`) — as a
9
- // tool_use block whose shell command carries the marker. Read-only verbs
10
- // (`quest show <id> --json`, `list`, `protocol`, `runs`) are how reviewers and
11
- // orchestrators inspect a quest without owning it, so they must NEVER key
9
+ // tool_use block whose shell command carries the marker, OR the native subagent
10
+ // stop payload itself names the Quest-owned `quest-executor` agent and carries a
11
+ // launch prompt in the orchestrator's narrow `work quest <id>` shape. Read-only
12
+ // verbs (`quest show <id> --json`, `list`, `protocol`, `runs`) are how reviewers
13
+ // and orchestrators inspect a quest without owning it, so they must NEVER key
12
14
  // detection: an agent whose transcript holds only read verbs is allowed silently.
13
- // The executor id comes from that first mutating invocation (deterministic,
14
- // transcript order) — including `quest checkpoint <id>`, because an executor
15
- // resuming an already-in_progress quest skips `quest start` and its first mutating
16
- // verb is the checkpoint. We parse the JSONL per-entry and inspect only tool_use
17
- // command inputs; prose, quoted skill text, examples, and echoed file contents live
18
- // in text blocks and tool_result content (never in a tool_use command), so
19
- // `quest start 12` / `quest checkpoint 12` examples in the skills can no longer key
20
- // detection. No mutating invocation not our concern, allow silently. We then
15
+ // The executor id comes from the first mutating invocation (deterministic,
16
+ // transcript order) when present — including `quest checkpoint <id>`, because an
17
+ // executor resuming an already-in_progress quest skips `quest start` and its first
18
+ // mutating verb is the checkpoint. If no mutating command has run yet, a native
19
+ // `quest-executor` launch prompt with an explicit quest id can still identify the
20
+ // owner. We parse the JSONL per-entry and inspect only command inputs; prose,
21
+ // quoted skill text, examples, and echoed file contents live in text blocks and
22
+ // tool_result content (never in a command), so `quest start 12` / `quest checkpoint
23
+ // 12` examples in the skills can no longer key detection. No mutating invocation
24
+ // and no native executor launch id → not our concern, allow silently. We then
21
25
  // compare the quest's latest checkpoint against the subagent's start time (the
22
26
  // transcript's first timestamp): a checkpoint newer than start clears the stop; so
23
27
  // does a terminal store status (complete/blocked/cancelled) reached during the run.
24
28
  //
25
- // Conservative by construction: any missing/unreadable input or parse failure →
26
- // allow (exit 0), with a one-line stderr diagnostic. We never false-positive-block
27
- // unrelated subagents.
29
+ // Conservative by construction: any missing input, unknown store/quest,
30
+ // unreadable input, or parse failure → allow (exit 0). Expected allow paths stay
31
+ // silent so unrelated or partially-shaped subagents do not surface hook noise.
28
32
  //
29
33
  // Block contract (Claude Code command hook): print {"decision":"block","reason"}
30
34
  // to stdout and exit 0. (Note: the {"ok":false,...} shape is for prompt/agent
@@ -41,6 +45,8 @@ import { loadQuest } from "../lib/store-local.mjs";
41
45
  // `git commit -m "quest checkpoint 1"`) does not falsely key executor detection.
42
46
  // Read verbs (show/list/protocol/runs) are deliberately absent.
43
47
  const MARKER = /^(?:node\s+)?(?:[.\w/@-]*\/)?quest\s+(start|checkpoint)\s+(\d+)/;
48
+ const NATIVE_EXECUTOR_AGENT = "quest-executor";
49
+ const NATIVE_WORK_PROMPT = /\bwork\s+quest\s+(\d+)\b/i;
44
50
 
45
51
  // Split a shell command into top-level segments on `&&`, `||`, `;`, `|`, and
46
52
  // newline, IGNORING any separator that sits inside single or double quotes. This
@@ -143,6 +149,20 @@ function commandOf(input) {
143
149
  return input && typeof input === "object" && typeof input.command === "string" ? input.command : null;
144
150
  }
145
151
 
152
+ function firstString(...values) {
153
+ return values.find((v) => typeof v === "string" && v.trim());
154
+ }
155
+
156
+ function nativeExecutorQuestId(payload) {
157
+ if (!payload || typeof payload !== "object") return null;
158
+ const agentType = firstString(payload.agent_type, payload.agentType, payload.agent_name, payload.agentName, payload.name);
159
+ if (agentType !== NATIVE_EXECUTOR_AGENT) return null;
160
+ const prompt = firstString(payload.prompt, payload.task_prompt, payload.taskPrompt, payload.agent_prompt, payload.agentPrompt);
161
+ if (!prompt) return null;
162
+ const m = NATIVE_WORK_PROMPT.exec(prompt);
163
+ return m ? Number(m[1]) : null;
164
+ }
165
+
146
166
  // The mutating-verb marker id from one transcript entry, considering only tool_use
147
167
  // command invocations. Assistant messages carry an array of content blocks; string
148
168
  // content (plain prose) and tool_result blocks (echoed output/file contents) are
@@ -194,29 +214,29 @@ try {
194
214
  try { payload = raw.trim() ? JSON.parse(raw) : {}; } catch { payload = {}; }
195
215
 
196
216
  const transcriptPath = payload.transcript_path;
197
- if (typeof transcriptPath !== "string" || !transcriptPath) { diag("no transcript_path in payload; allowing"); allow(); }
217
+ if (typeof transcriptPath !== "string" || !transcriptPath) allow();
198
218
 
199
219
  let transcript;
200
220
  try { transcript = readFileSync(transcriptPath, "utf8"); }
201
221
  catch (err) { diag(`cannot read transcript (${err.code || err.message}); allowing`); allow(); }
202
222
 
203
- const id = executorQuestId(transcript);
223
+ const id = executorQuestId(transcript) ?? nativeExecutorQuestId(payload);
204
224
  if (id == null) allow(); // no mutating quest invocation (read-only agent) — leave it alone, silently
205
225
 
206
226
  // Prefer an explicit start field if a future payload carries one; else the
207
227
  // transcript's first timestamp.
208
228
  const start = toMs(payload.start_time) ?? toMs(payload.started_at) ?? firstTimestamp(transcript);
209
- if (start == null) { diag(`quest ${id}: could not determine subagent start time; allowing`); allow(); }
229
+ if (start == null) allow();
210
230
 
211
231
  const cwd = typeof payload.cwd === "string" && payload.cwd ? payload.cwd : process.cwd();
212
232
  let storeDir;
213
233
  try { storeDir = findStoreDir(cwd, process.env); }
214
- catch (err) { diag(`quest ${id}: ${err.message}; allowing`); allow(); }
215
- if (!storeDir) { diag(`quest ${id}: no store found from ${cwd}; allowing`); allow(); }
234
+ catch { allow(); }
235
+ if (!storeDir) allow();
216
236
 
217
237
  let quest;
218
238
  try { quest = loadQuest(storeDir, id); }
219
- catch (err) { diag(`quest ${id}: ${err.message}; allowing`); allow(); }
239
+ catch { allow(); }
220
240
 
221
241
  // A checkpoint recorded after the subagent started clears the stop.
222
242
  const latestCp = quest.checkpoints.reduce((max, cp) => {
package/lib/cli.mjs CHANGED
@@ -5,13 +5,14 @@ import { parseArgs } from "node:util";
5
5
  import { readFileSync, existsSync, writeFileSync, appendFileSync } from "node:fs";
6
6
  import { join, dirname } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { spawnSync } from "node:child_process";
8
9
  import { ConfigError, findStoreDir, loadConfig } from "./config.mjs";
9
10
  import { ContractError, lintRecord } from "./contract.mjs";
10
11
  import * as local from "./store-local.mjs";
11
12
  import * as github from "./store-github.mjs";
12
13
  import { openStore } from "./store.mjs";
13
14
  import { COMMANDS, renderCommandHelp, renderGeneralHelp, renderInitNextSteps, renderNoStore, renderStatusOverview } from "./help.mjs";
14
- import { claudeDoctor, doctor as codexDoctor, installAgents as installCodexAgents, installClaudeAgents, versionInfo } from "./codex-native.mjs";
15
+ import { claudeDoctor, claudeDoctorFix, codexDoctorFix, doctor as codexDoctor, installAgents as installCodexAgents, installClaudeAgents, nativeProjectRoot, versionInfo } from "./codex-native.mjs";
15
16
 
16
17
  const PLUGIN_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
17
18
 
@@ -92,28 +93,161 @@ function preflightInitAgentInstall(cwd, env) {
92
93
  return plans;
93
94
  }
94
95
 
95
- async function runNativeSetupCommand(command, args, { out, cwd, env }, { label, doctor, install }) {
96
+ function printDoctorResult(command, result, out) {
97
+ if (result.repairs?.length) {
98
+ out(`${command} doctor --fix: repairs`);
99
+ for (const r of result.repairs) out(` ${r.ok ? "OK " : "ERR"} ${r.name}: ${r.command} (${r.detail})`);
100
+ }
101
+ for (const c of result.checks) out(`${c.ok ? "OK " : "ERR"} ${c.name}: ${c.detail}`);
102
+ if (result.recommended_path) {
103
+ const r = result.recommended_path;
104
+ out(`Recommendation: ${r.label} — ${r.command}`);
105
+ }
106
+ out(result.ok ? `${command} doctor: OK` : `${command} doctor: problems found`);
107
+ }
108
+
109
+ function parseOpenArgs(args) {
110
+ const forwarded = [];
111
+ let dryRun = false;
112
+ for (let i = 0; i < args.length; i += 1) {
113
+ const arg = args[i];
114
+ if (arg === "--") {
115
+ forwarded.push(...args.slice(i + 1));
116
+ break;
117
+ }
118
+ if (arg === "--dry-run") {
119
+ dryRun = true;
120
+ continue;
121
+ }
122
+ if (arg === "--help" || arg === "-h") return { help: true };
123
+ forwarded.push(arg);
124
+ }
125
+ return { dryRun, forwarded };
126
+ }
127
+
128
+ function shellQuote(arg) {
129
+ return /^[A-Za-z0-9_./:=@+-]+$/.test(arg) ? arg : `'${String(arg).replaceAll("'", "'\\''")}'`;
130
+ }
131
+
132
+ function openInvocation(command, cwd, env, forwarded) {
133
+ const root = nativeProjectRoot(cwd, env);
134
+ if (command === "codex") return { cmd: "codex", args: ["-C", root, ...forwarded], cwd };
135
+ if (command === "claude") return { cmd: "claude", args: forwarded, cwd: root };
136
+ throw new Error(`unknown provider "${command}"`);
137
+ }
138
+
139
+ function questCommandPrelude(cwd, env) {
140
+ const root = nativeProjectRoot(cwd, env);
141
+ return [
142
+ `cd ${shellQuote(root)}`,
143
+ `export PATH=${shellQuote(join(PLUGIN_ROOT, "bin"))}:$PATH`,
144
+ "quest --version",
145
+ "git status --short --branch",
146
+ ];
147
+ }
148
+
149
+ function codexNativePrompt(id) {
150
+ return "First call create_goal with: quest " + id +
151
+ " has a new checkpoint whose quest_status is complete or blocked in `quest show " + id +
152
+ " --json`; verify with get_goal; work quest " + id +
153
+ " per $quest:work; only call update_goal(status=\"complete\") after the checkpoint exists.";
154
+ }
155
+
156
+ function claudeNativePrompt(id) {
157
+ return "/goal quest " + id +
158
+ " has a new checkpoint whose quest_status is complete or blocked in `quest show " + id +
159
+ " --json`\nWork quest " + id + " per $quest:work.";
160
+ }
161
+
162
+ function renderWorkHandoff(command, id, recommendation, cwd, env) {
163
+ const lines = [
164
+ `${command} work: recommended path: ${recommendation.label}`,
165
+ `Reason: ${recommendation.reason}`,
166
+ "",
167
+ "Quest command prelude:",
168
+ ...questCommandPrelude(cwd, env).map((line) => ` ${line}`),
169
+ "",
170
+ ];
171
+ if (recommendation.key === "native-subagents") {
172
+ lines.push(
173
+ command === "codex" ? "Codex native subagent handoff:" : "Claude native subagent handoff:",
174
+ " agent_type: quest-executor",
175
+ " prompt:",
176
+ ...String(command === "codex" ? codexNativePrompt(id) : claudeNativePrompt(id)).split("\n").map((line) => ` ${line}`),
177
+ );
178
+ } else {
179
+ lines.push(
180
+ "Headless fallback command:",
181
+ ` ${recommendation.command.replace("<id>", String(id))}`,
182
+ );
183
+ }
184
+ return lines.join("\n");
185
+ }
186
+
187
+ async function runNativeSetupCommand(command, args, { out, cwd, env }, { label, doctor, doctorFix, install }) {
96
188
  const [subcommand, ...rest] = args;
97
189
  if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
98
190
  out(renderCommandHelp(command));
99
191
  return 0;
100
192
  }
101
193
  if (subcommand === "doctor") {
102
- const p = parse(command, rest, {}, { positionals: 0 });
194
+ const p = parse(command, rest, { fix: { type: "boolean" } }, { positionals: 0 });
103
195
  if (p.help) {
104
196
  out(renderCommandHelp(command));
105
197
  return 0;
106
198
  }
107
- const result = doctor({ cwd, env });
199
+ const result = p.values.fix ? doctorFix({ cwd, env }) : doctor({ cwd, env });
108
200
  if (p.values.json) out(JSON.stringify(result));
109
- else {
110
- for (const c of result.checks) out(`${c.ok ? "OK " : "ERR"} ${c.name}: ${c.detail}`);
111
- out(result.ok ? `${command} doctor: OK` : `${command} doctor: problems found`);
112
- }
201
+ else printDoctorResult(command, result, out);
113
202
  // 1 (generic diagnostic failure), not 5 — exit 5 is reserved for
114
203
  // ContractError (a quest contract violation), which a doctor finding is not.
115
204
  return result.ok ? 0 : 1;
116
205
  }
206
+ if (subcommand === "open") {
207
+ const p = parseOpenArgs(rest);
208
+ if (p.help) {
209
+ out(renderCommandHelp(command));
210
+ return 0;
211
+ }
212
+ const result = doctorFix({ cwd, env });
213
+ if (!result.ok) {
214
+ printDoctorResult(command, result, out);
215
+ out(`${command} open: not launching because doctor still reports problems`);
216
+ return 1;
217
+ }
218
+ const invocation = openInvocation(command, cwd, env, p.forwarded);
219
+ const rendered = [invocation.cmd, ...invocation.args].map(shellQuote).join(" ");
220
+ if (p.dryRun) {
221
+ out(`${command} open: health OK`);
222
+ out(`Would run: ${rendered}`);
223
+ return 0;
224
+ }
225
+ out(`${command} open: health OK; launching ${rendered}`);
226
+ const res = spawnSync(invocation.cmd, invocation.args, { cwd: invocation.cwd, env, stdio: "inherit" });
227
+ return typeof res.status === "number" ? res.status : 1;
228
+ }
229
+ if (subcommand === "work") {
230
+ const p = parse(command, rest, { "dry-run": { type: "boolean" } }, { positionals: 1 });
231
+ if (p.help) {
232
+ out(renderCommandHelp(command));
233
+ return 0;
234
+ }
235
+ const rawId = p.positionals[0];
236
+ if (!rawId || !/^\d+$/.test(rawId)) throw new UsageError("a numeric quest id is required", { command });
237
+ if (!p.values["dry-run"]) {
238
+ throw new UsageError(`${command} work only supports --dry-run; launching provider sessions is out of scope`, { command });
239
+ }
240
+ const result = doctor({ cwd, env });
241
+ if (!result.recommended_path?.available) {
242
+ printDoctorResult(command, result, out);
243
+ out(`${command} work: not printing a handoff because doctor still reports setup problems`);
244
+ return 1;
245
+ }
246
+ if (!result.ok) printDoctorResult(command, result, out);
247
+ else out(`${command} work: health OK`);
248
+ out(renderWorkHandoff(command, rawId, result.recommended_path, cwd, env));
249
+ return 0;
250
+ }
117
251
  if (subcommand === "install-agents") {
118
252
  const p = parse(command, rest, {
119
253
  scope: { type: "string", default: "project" },
@@ -183,7 +317,8 @@ async function dispatch(argv, ctx) {
183
317
  const runs = local.readRuns(storeDir);
184
318
  const ended = new Set(runs.filter((r) => r.event === "run_ended").map((r) => r.run_id));
185
319
  const active = runs.filter((r) => r.event === "run_started" && !ended.has(r.run_id)).length;
186
- out(renderStatusOverview({ counts, ready: store.readyQuests(), activeRuns: active, backend: config.backend }));
320
+ const inFlightOrBlocked = all.filter((q) => q.status === "in_progress" || q.status === "blocked");
321
+ out(renderStatusOverview({ counts, ready: store.readyQuests(), activeRuns: active, backend: config.backend, inFlightOrBlocked }));
187
322
  return 0;
188
323
  }
189
324
 
@@ -308,8 +443,8 @@ const HANDLERS = {
308
443
  if (p.values.json) out(JSON.stringify({ ok: true, id: created.id, path: created.path }));
309
444
  else {
310
445
  out(`Created quest ${created.id}: ${v.title}`);
311
- out(` record: ${created.path}`);
312
- out(` next: quest lint ${created.id} && quest show ${created.id}`);
446
+ out(`Record: ${created.path}`);
447
+ out(`Next: \`quest lint ${created.id}\``);
313
448
  }
314
449
  return 0;
315
450
  },
@@ -319,10 +454,41 @@ const HANDLERS = {
319
454
  status: { type: "string" },
320
455
  parent: { type: "string" },
321
456
  ready: { type: "boolean" },
457
+ queue: { type: "boolean" },
322
458
  });
323
459
  if (p.help) return void out(renderCommandHelp("list")) ?? 0;
460
+ if (p.values.ready && p.values.queue) throw new UsageError("--ready and --queue are mutually exclusive", { command: "list" });
461
+ if (p.values.queue && (p.values.status || p.values.parent)) throw new UsageError("--queue cannot be combined with --status or --parent", { command: "list" });
324
462
  const config = getStore(cwd, env);
325
463
  const store = openStore(config, { env });
464
+ if (p.values.queue) {
465
+ const q = store.queueState();
466
+ const payload = {
467
+ worker_ready: q.workerReady,
468
+ inline_close_ready_epics: q.inlineCloseReadyEpics,
469
+ blocked: q.blocked.map((b) => ({ quest: b.quest, reasons: b.reasons })),
470
+ };
471
+ if (p.values.json) {
472
+ out(JSON.stringify(payload));
473
+ } else {
474
+ const lines = [];
475
+ const pushQuest = (quest) => lines.push(`${String(quest.id).padStart(3)} [${quest.priority}] ${quest.status.padEnd(11)} ${quest.title}`);
476
+ lines.push("Worker-ready quests:");
477
+ if (payload.worker_ready.length) payload.worker_ready.forEach(pushQuest);
478
+ else lines.push(" (none)");
479
+ lines.push("", "Inline-close-ready epics:");
480
+ if (payload.inline_close_ready_epics.length) payload.inline_close_ready_epics.forEach(pushQuest);
481
+ else lines.push(" (none)");
482
+ lines.push("", "Blocked from queue:");
483
+ if (payload.blocked.length) {
484
+ for (const b of payload.blocked) lines.push(`${String(b.quest.id).padStart(3)} ${b.quest.title} — ${b.reasons.join("; ")}`);
485
+ } else {
486
+ lines.push(" (none)");
487
+ }
488
+ for (const line of lines) out(line);
489
+ }
490
+ return 0;
491
+ }
326
492
  let quests = p.values.ready ? store.readyQuests() : store.listQuests();
327
493
  if (p.values.status) quests = quests.filter((q) => q.status === p.values.status);
328
494
  if (p.values.parent) quests = quests.filter((q) => q.parent === Number(p.values.parent));
@@ -536,10 +702,10 @@ const HANDLERS = {
536
702
  },
537
703
 
538
704
  async codex(args, { out, cwd, env }) {
539
- return runNativeSetupCommand("codex", args, { out, cwd, env }, { label: "Codex", doctor: codexDoctor, install: installCodexAgents });
705
+ return runNativeSetupCommand("codex", args, { out, cwd, env }, { label: "Codex", doctor: codexDoctor, doctorFix: codexDoctorFix, install: installCodexAgents });
540
706
  },
541
707
 
542
708
  async claude(args, { out, cwd, env }) {
543
- return runNativeSetupCommand("claude", args, { out, cwd, env }, { label: "Claude", doctor: claudeDoctor, install: installClaudeAgents });
709
+ return runNativeSetupCommand("claude", args, { out, cwd, env }, { label: "Claude", doctor: claudeDoctor, doctorFix: claudeDoctorFix, install: installClaudeAgents });
544
710
  },
545
711
  };