quest-loop 0.3.5 → 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 +22 -1
- package/README.md +10 -5
- package/hooks/session-start.mjs +23 -11
- package/hooks/subagent-stop.mjs +40 -20
- package/lib/cli.mjs +109 -3
- package/lib/codex-native.mjs +85 -3
- package/lib/graph.mjs +82 -0
- package/lib/help.mjs +73 -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/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,28 @@ 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.
|
|
8
29
|
|
|
9
30
|
## [0.3.5] — 2026-07-08
|
|
10
31
|
|
package/README.md
CHANGED
|
@@ -128,7 +128,8 @@ you drive it from Claude Code, Codex, or a plain shell.
|
|
|
128
128
|
|
|
129
129
|
## Quickstart
|
|
130
130
|
|
|
131
|
-
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:
|
|
132
133
|
|
|
133
134
|
```bash
|
|
134
135
|
# 1. Create a quest store (.quests/) here
|
|
@@ -141,11 +142,14 @@ quest create --title "Add dark mode" \
|
|
|
141
142
|
--done-when "\`npm test\` passes including new theme tests" \
|
|
142
143
|
--validation "npm test"
|
|
143
144
|
|
|
144
|
-
# 3. Check
|
|
145
|
-
quest lint
|
|
145
|
+
# 3. Check the new quest against the contract spec
|
|
146
|
+
quest lint 1
|
|
146
147
|
|
|
147
|
-
# 4.
|
|
148
|
-
quest
|
|
148
|
+
# 4. Show counts, active work, ready work, and the next command
|
|
149
|
+
quest
|
|
150
|
+
|
|
151
|
+
# 5. Read the quest before working it
|
|
152
|
+
quest show 1 --json
|
|
149
153
|
```
|
|
150
154
|
|
|
151
155
|
By default, `quest init` also installs project-scoped native agent templates for
|
|
@@ -237,6 +241,7 @@ full base protocol lives in
|
|
|
237
241
|
|
|
238
242
|
| Command | Purpose |
|
|
239
243
|
|---|---|
|
|
244
|
+
| bare `quest` | Show a compact dashboard with counts, active work, ready work, active runs, and the next command |
|
|
240
245
|
| `quest init` | Create a quest store (`.quests/`) and install project-scoped Codex/Claude agent templates by default |
|
|
241
246
|
| `quest create` | Create a new quest (the only way records are born) |
|
|
242
247
|
| `quest list` | List quests (filter by status, parent, or readiness) |
|
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
|
@@ -99,6 +99,10 @@ function printDoctorResult(command, result, out) {
|
|
|
99
99
|
for (const r of result.repairs) out(` ${r.ok ? "OK " : "ERR"} ${r.name}: ${r.command} (${r.detail})`);
|
|
100
100
|
}
|
|
101
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
|
+
}
|
|
102
106
|
out(result.ok ? `${command} doctor: OK` : `${command} doctor: problems found`);
|
|
103
107
|
}
|
|
104
108
|
|
|
@@ -132,6 +136,54 @@ function openInvocation(command, cwd, env, forwarded) {
|
|
|
132
136
|
throw new Error(`unknown provider "${command}"`);
|
|
133
137
|
}
|
|
134
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
|
+
|
|
135
187
|
async function runNativeSetupCommand(command, args, { out, cwd, env }, { label, doctor, doctorFix, install }) {
|
|
136
188
|
const [subcommand, ...rest] = args;
|
|
137
189
|
if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
@@ -174,6 +226,28 @@ async function runNativeSetupCommand(command, args, { out, cwd, env }, { label,
|
|
|
174
226
|
const res = spawnSync(invocation.cmd, invocation.args, { cwd: invocation.cwd, env, stdio: "inherit" });
|
|
175
227
|
return typeof res.status === "number" ? res.status : 1;
|
|
176
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
|
+
}
|
|
177
251
|
if (subcommand === "install-agents") {
|
|
178
252
|
const p = parse(command, rest, {
|
|
179
253
|
scope: { type: "string", default: "project" },
|
|
@@ -243,7 +317,8 @@ async function dispatch(argv, ctx) {
|
|
|
243
317
|
const runs = local.readRuns(storeDir);
|
|
244
318
|
const ended = new Set(runs.filter((r) => r.event === "run_ended").map((r) => r.run_id));
|
|
245
319
|
const active = runs.filter((r) => r.event === "run_started" && !ended.has(r.run_id)).length;
|
|
246
|
-
|
|
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 }));
|
|
247
322
|
return 0;
|
|
248
323
|
}
|
|
249
324
|
|
|
@@ -368,8 +443,8 @@ const HANDLERS = {
|
|
|
368
443
|
if (p.values.json) out(JSON.stringify({ ok: true, id: created.id, path: created.path }));
|
|
369
444
|
else {
|
|
370
445
|
out(`Created quest ${created.id}: ${v.title}`);
|
|
371
|
-
out(`
|
|
372
|
-
out(`
|
|
446
|
+
out(`Record: ${created.path}`);
|
|
447
|
+
out(`Next: \`quest lint ${created.id}\``);
|
|
373
448
|
}
|
|
374
449
|
return 0;
|
|
375
450
|
},
|
|
@@ -379,10 +454,41 @@ const HANDLERS = {
|
|
|
379
454
|
status: { type: "string" },
|
|
380
455
|
parent: { type: "string" },
|
|
381
456
|
ready: { type: "boolean" },
|
|
457
|
+
queue: { type: "boolean" },
|
|
382
458
|
});
|
|
383
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" });
|
|
384
462
|
const config = getStore(cwd, env);
|
|
385
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
|
+
}
|
|
386
492
|
let quests = p.values.ready ? store.readyQuests() : store.listQuests();
|
|
387
493
|
if (p.values.status) quests = quests.filter((q) => q.status === p.values.status);
|
|
388
494
|
if (p.values.parent) quests = quests.filter((q) => q.parent === Number(p.values.parent));
|
package/lib/codex-native.mjs
CHANGED
|
@@ -226,6 +226,88 @@ export function nativeProjectRoot(cwd = process.cwd(), env = process.env) {
|
|
|
226
226
|
return projectRoot(cwd, env);
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
function questBin() {
|
|
230
|
+
return join(PLUGIN_ROOT, "bin", "quest");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function questRunBin() {
|
|
234
|
+
return join(PLUGIN_ROOT, "bin", "quest-run");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function shellQuote(arg) {
|
|
238
|
+
return /^[A-Za-z0-9_./:=@+-]+$/.test(arg) ? arg : `'${String(arg).replaceAll("'", "'\\''")}'`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function providerRecommendation(provider, checks) {
|
|
242
|
+
const bad = (name) => checkByName({ checks }, name)?.ok === false;
|
|
243
|
+
const commandId = "<id>";
|
|
244
|
+
if (provider === "codex") {
|
|
245
|
+
const setupCheckNames = [
|
|
246
|
+
"version-sync",
|
|
247
|
+
"quest-cli-path",
|
|
248
|
+
"codex-cli",
|
|
249
|
+
"plugin-installed",
|
|
250
|
+
"plugin-version",
|
|
251
|
+
"hook-parser",
|
|
252
|
+
"single-neutral-skill-root",
|
|
253
|
+
"native-agents",
|
|
254
|
+
];
|
|
255
|
+
const setupReady = setupCheckNames.every((name) => checkByName({ checks }, name)?.ok === true);
|
|
256
|
+
if (!setupReady) {
|
|
257
|
+
return {
|
|
258
|
+
key: "repair-first",
|
|
259
|
+
label: "repair Codex setup first",
|
|
260
|
+
available: false,
|
|
261
|
+
command: `${shellQuote(questBin())} codex doctor --fix`,
|
|
262
|
+
reason: "Codex setup checks must be green before Quest can print a trustworthy work handoff",
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
if (!bad("multi-agent-feature") && !bad("goals-feature")) {
|
|
266
|
+
return {
|
|
267
|
+
key: "native-subagents",
|
|
268
|
+
label: "native Codex quest-executor subagent",
|
|
269
|
+
available: true,
|
|
270
|
+
command: `${shellQuote(questBin())} codex work ${commandId} --dry-run`,
|
|
271
|
+
reason: "multi_agent, goals, Quest plugin, skill root, and native-agent template checks are green",
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (!bad("goals-feature")) {
|
|
275
|
+
return {
|
|
276
|
+
key: "quest-run-goal-required",
|
|
277
|
+
label: "headless Codex runner fallback with required goal tools",
|
|
278
|
+
available: true,
|
|
279
|
+
command: `${shellQuote(questRunBin())} ${commandId} --worker codex --codex-goal-mode require`,
|
|
280
|
+
reason: "Codex goal tools are available, but native multi-agent dispatch is not",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
key: "quest-run-goal-auto",
|
|
285
|
+
label: "headless Codex runner fallback with auto goal mode",
|
|
286
|
+
available: true,
|
|
287
|
+
command: `${shellQuote(questRunBin())} ${commandId} --worker codex --codex-goal-mode auto`,
|
|
288
|
+
reason: "Codex native goal mode is unavailable, so the runner must not require goal tools",
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const setupReady = checks.every((c) => c.ok);
|
|
293
|
+
if (!setupReady) {
|
|
294
|
+
return {
|
|
295
|
+
key: "repair-first",
|
|
296
|
+
label: "repair Claude setup first",
|
|
297
|
+
available: false,
|
|
298
|
+
command: `${shellQuote(questBin())} claude doctor --fix`,
|
|
299
|
+
reason: "Claude setup checks must be green before Quest can print a trustworthy work handoff",
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
key: "native-subagents",
|
|
304
|
+
label: "native Claude quest-executor subagent",
|
|
305
|
+
available: true,
|
|
306
|
+
command: `${shellQuote(questBin())} claude work ${commandId} --dry-run`,
|
|
307
|
+
reason: "Claude CLI, Quest plugin, and native-agent template checks are green",
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
229
311
|
function questCliPathCheck(cwd, env, versions) {
|
|
230
312
|
const pathQuestVersion = runCmd("quest", ["--version"], { cwd, env });
|
|
231
313
|
const pathVersion = pathQuestVersion.status === 0 ? pathQuestVersion.stdout.trim() : null;
|
|
@@ -378,7 +460,7 @@ export function doctor({ cwd = process.cwd(), env = process.env } = {}) {
|
|
|
378
460
|
|
|
379
461
|
checks.push(nativeAgentsCheck("codex", cwd, env));
|
|
380
462
|
|
|
381
|
-
return { ok: checks.every((c) => c.ok), checks };
|
|
463
|
+
return { ok: checks.every((c) => c.ok), checks, recommended_path: providerRecommendation("codex", checks) };
|
|
382
464
|
}
|
|
383
465
|
|
|
384
466
|
export function claudeDoctor({ cwd = process.cwd(), env = process.env } = {}) {
|
|
@@ -434,7 +516,7 @@ export function claudeDoctor({ cwd = process.cwd(), env = process.env } = {}) {
|
|
|
434
516
|
|
|
435
517
|
checks.push(nativeAgentsCheck("claude", cwd, env));
|
|
436
518
|
|
|
437
|
-
return { ok: checks.every((c) => c.ok), checks };
|
|
519
|
+
return { ok: checks.every((c) => c.ok), checks, recommended_path: providerRecommendation("claude", checks) };
|
|
438
520
|
}
|
|
439
521
|
|
|
440
522
|
function checkByName(result, name) {
|
|
@@ -536,7 +618,7 @@ function doctorWithFix(provider, { cwd = process.cwd(), env = process.env } = {}
|
|
|
536
618
|
...repairProviderPlugin(provider, before, { cwd, env }),
|
|
537
619
|
];
|
|
538
620
|
const after = doctorForProvider(provider, { cwd, env });
|
|
539
|
-
return { ok: after.ok, checks: after.checks, repairs, before };
|
|
621
|
+
return { ok: after.ok, checks: after.checks, recommended_path: after.recommended_path, repairs, before };
|
|
540
622
|
}
|
|
541
623
|
|
|
542
624
|
export function codexDoctorFix(options = {}) {
|
package/lib/graph.mjs
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const TERMINAL = new Set(["complete", "cancelled"]);
|
|
2
|
+
|
|
3
|
+
function byId(quests) {
|
|
4
|
+
return new Map(quests.map((q) => [q.id, q]));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function prioritySort(a, b) {
|
|
8
|
+
return a.priority.localeCompare(b.priority) || a.id - b.id;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function dependencyIds(q) {
|
|
12
|
+
return Array.isArray(q.depends_on) && q.depends_on.every(Number.isInteger) ? q.depends_on : [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function hasMalformedDependsOn(q) {
|
|
16
|
+
return "depends_on" in q && (!Array.isArray(q.depends_on) || q.depends_on.some((d) => !Number.isInteger(d)));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function openChildIds(quests, parentId) {
|
|
20
|
+
return quests
|
|
21
|
+
.filter((q) => q.parent === parentId && !TERMINAL.has(q.status))
|
|
22
|
+
.map((q) => q.id)
|
|
23
|
+
.sort((a, b) => a - b);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function queueBlockers(q, known, quests, epicIds) {
|
|
27
|
+
const reasons = [];
|
|
28
|
+
if (q.parent !== undefined && !known.has(q.parent)) reasons.push(`missing parent: #${q.parent}`);
|
|
29
|
+
if (hasMalformedDependsOn(q)) reasons.push("malformed depends_on: must be a list of quest ids");
|
|
30
|
+
|
|
31
|
+
const deps = dependencyIds(q);
|
|
32
|
+
const missingDeps = deps.filter((id) => !known.has(id));
|
|
33
|
+
if (missingDeps.length) reasons.push(`missing depends_on: ${missingDeps.map((id) => `#${id}`).join(", ")}`);
|
|
34
|
+
|
|
35
|
+
const incompleteDeps = deps.filter((id) => known.has(id) && known.get(id).status !== "complete");
|
|
36
|
+
if (incompleteDeps.length) reasons.push(`incomplete depends_on: ${incompleteDeps.map((id) => `#${id}`).join(", ")}`);
|
|
37
|
+
|
|
38
|
+
if (epicIds.has(q.id)) {
|
|
39
|
+
const open = openChildIds(quests, q.id);
|
|
40
|
+
if (open.length) reasons.push(`open children: ${open.map((id) => `#${id}`).join(", ")}`);
|
|
41
|
+
}
|
|
42
|
+
return reasons;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function computeQueue(quests) {
|
|
46
|
+
const all = [...quests];
|
|
47
|
+
const known = byId(all);
|
|
48
|
+
const epicIds = new Set(all.filter((q) => q.parent !== undefined).map((q) => q.parent));
|
|
49
|
+
const workerReady = [];
|
|
50
|
+
const inlineCloseReadyEpics = [];
|
|
51
|
+
const blocked = [];
|
|
52
|
+
|
|
53
|
+
for (const q of all) {
|
|
54
|
+
if (q.status !== "todo") continue;
|
|
55
|
+
const reasons = queueBlockers(q, known, all, epicIds);
|
|
56
|
+
if (reasons.length) {
|
|
57
|
+
blocked.push({ quest: q, reasons });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (epicIds.has(q.id)) inlineCloseReadyEpics.push(q);
|
|
61
|
+
else workerReady.push(q);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
workerReady.sort(prioritySort);
|
|
65
|
+
inlineCloseReadyEpics.sort(prioritySort);
|
|
66
|
+
blocked.sort((a, b) => prioritySort(a.quest, b.quest));
|
|
67
|
+
return { workerReady, inlineCloseReadyEpics, blocked };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function lintGraphReferences(quests) {
|
|
71
|
+
const known = byId(quests);
|
|
72
|
+
const problems = new Map(quests.map((q) => [q.id, []]));
|
|
73
|
+
for (const q of quests) {
|
|
74
|
+
if (q.parent !== undefined && !known.has(q.parent)) {
|
|
75
|
+
problems.get(q.id).push(`parent references unknown quest ${q.parent}`);
|
|
76
|
+
}
|
|
77
|
+
for (const dep of dependencyIds(q)) {
|
|
78
|
+
if (!known.has(dep)) problems.get(q.id).push(`depends_on references unknown quest ${dep}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return problems;
|
|
82
|
+
}
|
package/lib/help.mjs
CHANGED
|
@@ -48,11 +48,12 @@ export const COMMANDS = {
|
|
|
48
48
|
},
|
|
49
49
|
list: {
|
|
50
50
|
purpose: "List quests (filter by status, parent, or readiness)",
|
|
51
|
-
usage: "quest list [--status <s>] [--parent <id>] [--ready] [--json]",
|
|
51
|
+
usage: "quest list [--status <s>] [--parent <id>] [--ready|--queue] [--json]",
|
|
52
52
|
flags: [
|
|
53
53
|
["--status <s>", "todo | in_progress | blocked | complete | cancelled"],
|
|
54
54
|
["--parent <id>", "children of an epic"],
|
|
55
|
-
["--ready", "todo quests whose depends_on are
|
|
55
|
+
["--ready", "worker-dispatchable todo quests whose depends_on are complete, excluding epics"],
|
|
56
|
+
["--queue", "worker-ready quests, inline-close-ready epics, and blocked queue reasons"],
|
|
56
57
|
["--json", "machine-readable output"],
|
|
57
58
|
],
|
|
58
59
|
example: "quest list --ready",
|
|
@@ -138,37 +139,46 @@ export const COMMANDS = {
|
|
|
138
139
|
example: "quest runs --active",
|
|
139
140
|
},
|
|
140
141
|
codex: {
|
|
141
|
-
purpose: "Inspect, repair, install, or
|
|
142
|
-
usage: "quest codex doctor [--fix] [--json] | quest codex open [--dry-run] [-- <codex args>] | quest codex install-agents [--scope project|user] [--dry-run] [--force] [--json]",
|
|
142
|
+
purpose: "Inspect, repair, install, open, or hand off Quest's native Codex integration",
|
|
143
|
+
usage: "quest codex doctor [--fix] [--json] | quest codex work <id> --dry-run | quest codex open [--dry-run] [-- <codex args>] | quest codex install-agents [--scope project|user] [--dry-run] [--force] [--json]",
|
|
143
144
|
flags: [
|
|
144
145
|
["doctor", "check Codex CLI, multi-agent support, plugin install/version, hooks, skill roots, and native-agent templates"],
|
|
145
146
|
["--fix", "repair missing/stale Quest agents and Quest plugin install/version when possible, then rerun doctor"],
|
|
147
|
+
["work <id>", "run the health gate and print the native subagent or runner-fallback handoff"],
|
|
146
148
|
["open", "repair/check setup, then launch interactive Codex from the project root"],
|
|
147
149
|
["install-agents", "install quest-executor and quest-reviewer as native Codex custom agents"],
|
|
148
150
|
["--scope <s>", "project (default: .codex/agents at repo root) or user (~/.codex/agents)"],
|
|
149
|
-
["--dry-run", "
|
|
151
|
+
["--dry-run", "install-agents: preview writes; work/open: print the handoff or launch command without starting provider work"],
|
|
150
152
|
["--force", "replace existing non-Quest agent files"],
|
|
151
153
|
["--json", "machine-readable output"],
|
|
152
154
|
],
|
|
153
|
-
example: "quest codex doctor --fix && quest codex
|
|
155
|
+
example: "quest codex doctor --fix && quest codex work 12 --dry-run",
|
|
154
156
|
},
|
|
155
157
|
claude: {
|
|
156
|
-
purpose: "Inspect, repair, install, or
|
|
157
|
-
usage: "quest claude doctor [--fix] [--json] | quest claude open [--dry-run] [-- <claude args>] | quest claude install-agents [--scope project|user] [--dry-run] [--force] [--json]",
|
|
158
|
+
purpose: "Inspect, repair, install, open, or hand off Quest's native Claude Code integration",
|
|
159
|
+
usage: "quest claude doctor [--fix] [--json] | quest claude work <id> --dry-run | quest claude open [--dry-run] [-- <claude args>] | quest claude install-agents [--scope project|user] [--dry-run] [--force] [--json]",
|
|
158
160
|
flags: [
|
|
159
161
|
["doctor", "check Claude CLI, plugin install/version, and native-agent templates"],
|
|
160
162
|
["--fix", "repair missing/stale Quest agents and Quest plugin install/version when possible, then rerun doctor"],
|
|
163
|
+
["work <id>", "run the health gate and print the native subagent handoff"],
|
|
161
164
|
["open", "repair/check setup, then launch interactive Claude Code from the project root"],
|
|
162
165
|
["install-agents", "install quest-executor and quest-reviewer as native Claude Code custom agents"],
|
|
163
166
|
["--scope <s>", "project (default: .claude/agents at repo root) or user (~/.claude/agents)"],
|
|
164
|
-
["--dry-run", "
|
|
167
|
+
["--dry-run", "install-agents: preview writes; work/open: print the handoff or launch command without starting provider work"],
|
|
165
168
|
["--force", "replace existing non-Quest agent files"],
|
|
166
169
|
["--json", "machine-readable output"],
|
|
167
170
|
],
|
|
168
|
-
example: "quest claude doctor --fix && quest claude
|
|
171
|
+
example: "quest claude doctor --fix && quest claude work 12 --dry-run",
|
|
169
172
|
},
|
|
170
173
|
};
|
|
171
174
|
|
|
175
|
+
const COMMAND_GROUPS = [
|
|
176
|
+
["Setup and authoring", ["init", "create", "edit", "lint", "protocol"]],
|
|
177
|
+
["Queue and work", ["list", "show", "start", "checkpoint", "runs"]],
|
|
178
|
+
["Lifecycle", ["cancel", "reopen", "amend"]],
|
|
179
|
+
["Provider handoff", ["codex", "claude"]],
|
|
180
|
+
];
|
|
181
|
+
|
|
172
182
|
export function renderCommandHelp(name) {
|
|
173
183
|
const c = COMMANDS[name];
|
|
174
184
|
const lines = [c.purpose, "", `Usage: ${c.usage}`];
|
|
@@ -182,55 +192,84 @@ export function renderCommandHelp(name) {
|
|
|
182
192
|
}
|
|
183
193
|
|
|
184
194
|
export function renderGeneralHelp() {
|
|
185
|
-
const lines = [INTRO, "", "Commands:"];
|
|
195
|
+
const lines = [INTRO, "", "Commands by workflow:"];
|
|
186
196
|
const w = Math.max(...Object.keys(COMMANDS).map((k) => k.length));
|
|
187
|
-
|
|
197
|
+
const seen = new Set();
|
|
198
|
+
for (const [group, names] of COMMAND_GROUPS) {
|
|
199
|
+
lines.push("", `${group}:`);
|
|
200
|
+
for (const name of names) {
|
|
201
|
+
const c = COMMANDS[name];
|
|
202
|
+
seen.add(name);
|
|
203
|
+
lines.push(` quest ${name.padEnd(w)} ${c.purpose}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const ungrouped = Object.keys(COMMANDS).filter((name) => !seen.has(name));
|
|
207
|
+
if (ungrouped.length) {
|
|
208
|
+
lines.push("", "Other:");
|
|
209
|
+
for (const name of ungrouped) lines.push(` quest ${name.padEnd(w)} ${COMMANDS[name].purpose}`);
|
|
210
|
+
}
|
|
188
211
|
lines.push("", "Run `quest <command> --help` for flags and a copy-pasteable example.", "Headless workers: see `quest-run --help`.");
|
|
189
212
|
return lines.join("\n");
|
|
190
213
|
}
|
|
191
214
|
|
|
192
|
-
|
|
215
|
+
function renderQuestSummary(q, { includeStatus = false } = {}) {
|
|
216
|
+
const status = includeStatus ? `[${q.status}] ` : "";
|
|
217
|
+
return ` ${q.id}. ${status}[${q.priority}] ${q.title}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function renderStatusOverview({ counts, ready, activeRuns, backend, inFlightOrBlocked = [] }) {
|
|
193
221
|
const lines = [`Quest store: ${backend} backend`];
|
|
194
222
|
const parts = ["todo", "in_progress", "blocked", "complete", "cancelled"]
|
|
195
223
|
.filter((s) => counts[s])
|
|
196
224
|
.map((s) => `${counts[s]} ${s}`);
|
|
197
225
|
lines.push(parts.length ? `Quests: ${parts.join(" · ")}` : "Quests: none yet");
|
|
226
|
+
lines.push(`Active runs: ${activeRuns}`);
|
|
227
|
+
|
|
228
|
+
lines.push("", "In flight / blocked:");
|
|
229
|
+
if (inFlightOrBlocked.length) {
|
|
230
|
+
for (const q of inFlightOrBlocked.slice(0, 3)) lines.push(renderQuestSummary(q, { includeStatus: true }));
|
|
231
|
+
} else {
|
|
232
|
+
lines.push(" none");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
lines.push("", "Ready work:");
|
|
198
236
|
if (ready.length) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
lines.push(""
|
|
237
|
+
for (const q of ready.slice(0, 3)) lines.push(renderQuestSummary(q));
|
|
238
|
+
} else {
|
|
239
|
+
lines.push(" none");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let next;
|
|
243
|
+
if (activeRuns > 0) {
|
|
244
|
+
next = "quest runs --active";
|
|
245
|
+
} else if (inFlightOrBlocked.length) {
|
|
246
|
+
next = `quest show ${inFlightOrBlocked[0].id} --json`;
|
|
247
|
+
} else if (ready.length) {
|
|
248
|
+
next = `quest show ${ready[0].id} --json`;
|
|
202
249
|
} else if (!parts.length) {
|
|
203
|
-
|
|
250
|
+
next = "quest create --help";
|
|
204
251
|
} else {
|
|
205
|
-
|
|
252
|
+
next = "quest list --queue";
|
|
206
253
|
}
|
|
207
|
-
|
|
254
|
+
lines.push("", `Next: \`${next}\``);
|
|
208
255
|
return lines.join("\n");
|
|
209
256
|
}
|
|
210
257
|
|
|
211
258
|
export function renderNoStore() {
|
|
212
259
|
return [
|
|
213
|
-
"No quest store found (
|
|
214
|
-
"",
|
|
215
|
-
"Get started:",
|
|
216
|
-
" quest init create a local store here",
|
|
217
|
-
" quest init --backend github --repo owner/name",
|
|
260
|
+
"No quest store found (.quests/ not found from here upward).",
|
|
218
261
|
"",
|
|
219
|
-
"
|
|
262
|
+
"Next: `quest init`",
|
|
263
|
+
"GitHub-backed store: `quest init --backend github --repo owner/name`",
|
|
220
264
|
].join("\n");
|
|
221
265
|
}
|
|
222
266
|
|
|
223
267
|
export function renderInitNextSteps(backend, { agentsInstalled = true } = {}) {
|
|
224
268
|
return [
|
|
225
269
|
"",
|
|
226
|
-
|
|
227
|
-
...(
|
|
228
|
-
...(agentsInstalled ? ["
|
|
229
|
-
|
|
230
|
-
` ${agentsInstalled ? "3" : "1"}. Author a quest: quest create --help (or $quest:plan in your agent session)`,
|
|
231
|
-
` ${agentsInstalled ? "4" : "2"}. Check it: quest lint --all`,
|
|
232
|
-
` ${agentsInstalled ? "5" : "3"}. Work it: $quest:work <id> in-session, or quest-run <id> headless`,
|
|
233
|
-
...(agentsInstalled ? [] : [" Agent templates skipped; install later with `quest codex install-agents --scope project` and `quest claude install-agents --scope project`."]),
|
|
234
|
-
...(backend === "github" ? ["", "Records live as GitHub issues; config and amendments stay local in .quests/."] : []),
|
|
270
|
+
`Store ready: ${backend} backend`,
|
|
271
|
+
...(backend === "github" ? ["Records live as GitHub issues; config and amendments stay local in .quests/."] : []),
|
|
272
|
+
...(agentsInstalled ? ["Agent templates installed. Restart agent sessions before using new project agents."] : ["Agent templates skipped."]),
|
|
273
|
+
`Next: \`${agentsInstalled ? "quest codex doctor --fix" : "quest create --help"}\``,
|
|
235
274
|
].join("\n");
|
|
236
275
|
}
|
package/lib/runner.mjs
CHANGED
|
@@ -184,6 +184,16 @@ async function recordBlocked(storeDir, env, id, summary, validation) {
|
|
|
184
184
|
// Option resolution: flag → record frontmatter → config defaults
|
|
185
185
|
// ---------------------------------------------------------------------------
|
|
186
186
|
|
|
187
|
+
function parseNumericFlag(flags, name, { integer = false, min = -Infinity, label = "number" } = {}) {
|
|
188
|
+
const raw = flags[name];
|
|
189
|
+
if (raw == null) return undefined;
|
|
190
|
+
const value = Number(raw);
|
|
191
|
+
if (typeof raw !== "string" || raw.trim() === "" || !Number.isFinite(value) || (integer && !Number.isInteger(value)) || value < min) {
|
|
192
|
+
throw new RunUsageError(`--${name} must be ${label} (got "${raw}")`);
|
|
193
|
+
}
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
|
|
187
197
|
function resolveOptions(front, config, flags) {
|
|
188
198
|
const worker = flags.worker ?? front.worker ?? config.defaults.worker;
|
|
189
199
|
if (!["claude", "codex"].includes(worker)) throw new RunUsageError(`--worker must be claude or codex (got "${worker}")`);
|
|
@@ -191,12 +201,12 @@ function resolveOptions(front, config, flags) {
|
|
|
191
201
|
const recordModel = front.model && front.model !== "inherit" ? front.model : undefined;
|
|
192
202
|
const model = flags.model ?? recordModel ?? wd.model;
|
|
193
203
|
const effort = flags.effort ?? front.effort ?? (worker === "codex" ? wd.reasoning_effort : wd.effort);
|
|
194
|
-
const maxIterations = flags
|
|
195
|
-
const maxCost = flags
|
|
196
|
-
const maxTokens = flags
|
|
204
|
+
const maxIterations = parseNumericFlag(flags, "max-iterations", { integer: true, min: 1, label: "a positive integer" }) ?? front.max_iterations ?? config.defaults.max_iterations;
|
|
205
|
+
const maxCost = parseNumericFlag(flags, "max-cost", { min: 0, label: "a non-negative number" }) ?? front.max_cost ?? undefined;
|
|
206
|
+
const maxTokens = parseNumericFlag(flags, "max-tokens", { integer: true, min: 1, label: "a positive integer" });
|
|
197
207
|
// Per-session wall-clock cap (seconds → ms). Flag > config default > 1800s.
|
|
198
|
-
// A
|
|
199
|
-
const sessionTimeoutSec = flags
|
|
208
|
+
// A zero value disables the timeout.
|
|
209
|
+
const sessionTimeoutSec = parseNumericFlag(flags, "session-timeout", { min: 0, label: "a non-negative number" }) ?? config.defaults.session_timeout ?? 1800;
|
|
200
210
|
const sessionTimeout = Number.isFinite(sessionTimeoutSec) && sessionTimeoutSec > 0 ? Math.round(sessionTimeoutSec * 1000) : 0;
|
|
201
211
|
// Codex exec sandbox: flag → config defaults.codex.sandbox → workspace-write.
|
|
202
212
|
// No silent escalation: the safe default stays workspace-write, and the value
|
|
@@ -563,7 +573,7 @@ function makeWorktree(repoRoot, id, title, io) {
|
|
|
563
573
|
}
|
|
564
574
|
|
|
565
575
|
async function runReady(storeDir, config, flags, io) {
|
|
566
|
-
const parallel =
|
|
576
|
+
const parallel = parseNumericFlag(flags, "parallel", { integer: true, min: 1, label: "a positive integer" }) ?? 1;
|
|
567
577
|
const isolate = flags.isolate;
|
|
568
578
|
if (isolate && isolate !== "worktree") throw new RunUsageError(`--isolate only supports "worktree" (got "${isolate}")`);
|
|
569
579
|
const repoRoot = dirname(storeDir);
|
|
@@ -573,29 +583,17 @@ async function runReady(storeDir, config, flags, io) {
|
|
|
573
583
|
const skippedEpics = new Set();
|
|
574
584
|
const inFlight = new Map();
|
|
575
585
|
|
|
576
|
-
const
|
|
577
|
-
const { stdout, code } = await questCli(storeDir, io.env, ["list", "--
|
|
578
|
-
if (code !== 0) return [];
|
|
579
|
-
try {
|
|
580
|
-
return JSON.parse(stdout).map((q) => q.id);
|
|
581
|
-
} catch {
|
|
582
|
-
return [];
|
|
583
|
-
}
|
|
584
|
-
};
|
|
585
|
-
|
|
586
|
-
// Ids that at least one quest names as its parent — i.e. epics. Epics are
|
|
587
|
-
// closed by the orchestrator inline (verify children, run the epic validation
|
|
588
|
-
// loop, checkpoint), never dispatched to a worker. readyQuests already gates
|
|
589
|
-
// out epics with open children; this catches the fully-verified epic that has
|
|
590
|
-
// become "ready" so --ready still refuses to burn a worker run on it. A
|
|
591
|
-
// direct `quest-run <id>` on an epic stays allowed.
|
|
592
|
-
const epicIds = async () => {
|
|
593
|
-
const { stdout, code } = await questCli(storeDir, io.env, ["list", "--json"]);
|
|
594
|
-
if (code !== 0) return new Set();
|
|
586
|
+
const queue = async () => {
|
|
587
|
+
const { stdout, code } = await questCli(storeDir, io.env, ["list", "--queue", "--json"]);
|
|
588
|
+
if (code !== 0) return { workerReady: [], inlineCloseReadyEpics: [] };
|
|
595
589
|
try {
|
|
596
|
-
|
|
590
|
+
const parsed = JSON.parse(stdout);
|
|
591
|
+
return {
|
|
592
|
+
workerReady: (parsed.worker_ready ?? []).map((q) => q.id),
|
|
593
|
+
inlineCloseReadyEpics: (parsed.inline_close_ready_epics ?? []).map((q) => q.id),
|
|
594
|
+
};
|
|
597
595
|
} catch {
|
|
598
|
-
return
|
|
596
|
+
return { workerReady: [], inlineCloseReadyEpics: [] };
|
|
599
597
|
}
|
|
600
598
|
};
|
|
601
599
|
|
|
@@ -619,14 +617,13 @@ async function runReady(storeDir, config, flags, io) {
|
|
|
619
617
|
};
|
|
620
618
|
|
|
621
619
|
for (;;) {
|
|
622
|
-
const
|
|
623
|
-
const
|
|
620
|
+
const q = await queue();
|
|
621
|
+
for (const id of q.inlineCloseReadyEpics.filter((id) => !skippedEpics.has(id))) {
|
|
622
|
+
skippedEpics.add(id);
|
|
623
|
+
io.errOut(`quest-run: quest ${id} is an inline-close-ready epic — refusing to auto-dispatch it. Close it inline per $quest:orchestrate: verify its children, run the epic validation loop, then \`quest checkpoint ${id} --status complete\`. Never burn a worker run on an epic.`);
|
|
624
|
+
}
|
|
625
|
+
const ready = q.workerReady.filter((id) => !done.has(id) && !inFlight.has(id));
|
|
624
626
|
for (const id of ready) {
|
|
625
|
-
if (epics.has(id)) {
|
|
626
|
-
skippedEpics.add(id);
|
|
627
|
-
io.errOut(`quest-run: quest ${id} is an epic (other quests name it as parent) — refusing to auto-dispatch it. Close it inline per $quest:orchestrate: verify its children, run the epic validation loop, then \`quest checkpoint ${id} --status complete\`. Never burn a worker run on an epic.`);
|
|
628
|
-
continue;
|
|
629
|
-
}
|
|
630
627
|
if (inFlight.size >= parallel) break;
|
|
631
628
|
startOne(id);
|
|
632
629
|
}
|
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`.
|