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/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
|
-
##
|
|
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
|
|
61
|
-
quest claude
|
|
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
|
|
88
|
-
quest codex
|
|
87
|
+
quest codex doctor --fix
|
|
88
|
+
quest codex open
|
|
89
89
|
codex debug prompt-input "noop"
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
`quest codex doctor`
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
134
|
-
quest lint
|
|
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
|
-
#
|
|
137
|
-
quest
|
|
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.
|
|
149
|
-
|
|
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
|
|
242
|
-
| `quest claude` | Validate
|
|
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
|
|
package/hooks/session-start.mjs
CHANGED
|
@@ -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.
|
|
6
|
-
// no-op (exit 0). Reads only; no network
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/hooks/subagent-stop.mjs
CHANGED
|
@@ -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
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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
|
|
14
|
-
// transcript order) — including `quest checkpoint <id>`, because an
|
|
15
|
-
// resuming an already-in_progress quest skips `quest start` and its first
|
|
16
|
-
// verb is the checkpoint.
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
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
|
|
26
|
-
// allow (exit 0)
|
|
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)
|
|
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)
|
|
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
|
|
215
|
-
if (!storeDir)
|
|
234
|
+
catch { allow(); }
|
|
235
|
+
if (!storeDir) allow();
|
|
216
236
|
|
|
217
237
|
let quest;
|
|
218
238
|
try { quest = loadQuest(storeDir, id); }
|
|
219
|
-
catch
|
|
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
|
-
|
|
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
|
-
|
|
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(`
|
|
312
|
-
out(`
|
|
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
|
};
|