quest-loop 0.1.0 → 0.3.2
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 +137 -0
- package/README.md +119 -19
- package/agents/quest-executor.md +12 -2
- package/agents/quest-executor.toml +13 -6
- package/agents/quest-reviewer.md +9 -0
- package/agents/quest-reviewer.toml +11 -7
- package/hooks/hooks.json +40 -5
- package/hooks/subagent-stop.mjs +140 -10
- package/lib/cli.mjs +141 -2
- package/lib/codex-native.mjs +425 -0
- package/lib/config.mjs +1 -1
- package/lib/contract.mjs +22 -2
- package/lib/help.mjs +42 -7
- package/lib/runner.mjs +59 -3
- package/lib/store-github.mjs +34 -2
- package/lib/store-local.mjs +32 -2
- package/lib/store.mjs +2 -0
- package/lib/workers.mjs +26 -12
- package/package.json +3 -3
- package/skills/orchestrate/SKILL.md +100 -23
- package/skills/orchestrate/agents/openai.yaml +1 -1
- package/skills/plan/SKILL.md +42 -5
- package/skills/plan/agents/openai.yaml +1 -1
- package/skills/protocol/SKILL.md +2 -2
- package/skills/protocol/agents/openai.yaml +1 -1
- package/skills/protocol/references/contract-spec.md +14 -5
- package/skills/protocol/references/protocol.md +12 -4
- package/skills/retro/SKILL.md +2 -2
- package/skills/retro/agents/openai.yaml +1 -1
- package/skills/setup/SKILL.md +92 -0
- package/skills/setup/agents/openai.yaml +7 -0
- package/skills/work/SKILL.md +3 -3
- package/skills/work/agents/openai.yaml +1 -1
package/hooks/subagent-stop.mjs
CHANGED
|
@@ -3,12 +3,24 @@
|
|
|
3
3
|
// recording a checkpoint — the protocol's "no stop without a checkpoint" rule,
|
|
4
4
|
// enforced deterministically.
|
|
5
5
|
//
|
|
6
|
-
// A subagent counts as a quest-executor iff its transcript
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
6
|
+
// A subagent counts as a quest-executor iff one of its transcript entries records
|
|
7
|
+
// an actual *mutating* quest invocation — `quest start <id>` or `quest checkpoint
|
|
8
|
+
// <id>` (under any binary prefix: `quest`, `./bin/quest`, `node bin/quest`) — as a
|
|
9
|
+
// tool_use block whose shell command carries the marker. Read-only verbs
|
|
10
|
+
// (`quest show <id> --json`, `list`, `protocol`, `runs`) are how reviewers and
|
|
11
|
+
// orchestrators inspect a quest without owning it, so they must NEVER key
|
|
12
|
+
// detection: an agent whose transcript holds only read verbs is allowed silently.
|
|
13
|
+
// The executor id comes from that first mutating invocation (deterministic,
|
|
14
|
+
// transcript order) — including `quest checkpoint <id>`, because an executor
|
|
15
|
+
// resuming an already-in_progress quest skips `quest start` and its first mutating
|
|
16
|
+
// verb is the checkpoint. We parse the JSONL per-entry and inspect only tool_use
|
|
17
|
+
// command inputs; prose, quoted skill text, examples, and echoed file contents live
|
|
18
|
+
// in text blocks and tool_result content (never in a tool_use command), so
|
|
19
|
+
// `quest start 12` / `quest checkpoint 12` examples in the skills can no longer key
|
|
20
|
+
// detection. No mutating invocation → not our concern, allow silently. We then
|
|
21
|
+
// compare the quest's latest checkpoint against the subagent's start time (the
|
|
22
|
+
// transcript's first timestamp): a checkpoint newer than start clears the stop; so
|
|
23
|
+
// does a terminal store status (complete/blocked/cancelled) reached during the run.
|
|
12
24
|
//
|
|
13
25
|
// Conservative by construction: any missing/unreadable input or parse failure →
|
|
14
26
|
// allow (exit 0), with a one-line stderr diagnostic. We never false-positive-block
|
|
@@ -22,7 +34,73 @@ import { readFileSync, writeSync } from "node:fs";
|
|
|
22
34
|
import { findStoreDir } from "../lib/config.mjs";
|
|
23
35
|
import { loadQuest } from "../lib/store-local.mjs";
|
|
24
36
|
|
|
25
|
-
|
|
37
|
+
// Mutating quest verbs only. Group 1 is the verb, group 2 the id. The marker is
|
|
38
|
+
// anchored to a COMMAND position — an optional `node ` launcher and/or path prefix
|
|
39
|
+
// then `quest <verb> <id>` at the head of a command — so `quest checkpoint 1`
|
|
40
|
+
// merely quoted inside another program's argument (e.g. `grep 'quest checkpoint 1'`,
|
|
41
|
+
// `git commit -m "quest checkpoint 1"`) does not falsely key executor detection.
|
|
42
|
+
// Read verbs (show/list/protocol/runs) are deliberately absent.
|
|
43
|
+
const MARKER = /^(?:node\s+)?(?:[.\w/@-]*\/)?quest\s+(start|checkpoint)\s+(\d+)/;
|
|
44
|
+
|
|
45
|
+
// Split a shell command into top-level segments on `&&`, `||`, `;`, `|`, and
|
|
46
|
+
// newline, IGNORING any separator that sits inside single or double quotes. This
|
|
47
|
+
// keeps quoted text (grep patterns, commit messages) from forging a command head.
|
|
48
|
+
// (Heredoc bodies on their own line remain a rare residual; a false "checkpoint"
|
|
49
|
+
// nudge is benign, and heredoc parsing is not worth the complexity in a hook.)
|
|
50
|
+
function splitTopLevel(s) {
|
|
51
|
+
const segs = [];
|
|
52
|
+
let buf = "";
|
|
53
|
+
let quote = null;
|
|
54
|
+
let escaped = false;
|
|
55
|
+
for (let i = 0; i < s.length; i++) {
|
|
56
|
+
const c = s[i];
|
|
57
|
+
if (escaped) {
|
|
58
|
+
buf += c;
|
|
59
|
+
escaped = false;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (quote !== "'" && c === "\\") {
|
|
63
|
+
buf += c;
|
|
64
|
+
escaped = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (quote) {
|
|
68
|
+
if (c === quote) quote = null;
|
|
69
|
+
buf += c;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (c === "'" || c === '"') { quote = c; buf += c; continue; }
|
|
73
|
+
if ((c === "&" && s[i + 1] === "&") || (c === "|" && s[i + 1] === "|")) { segs.push(buf); buf = ""; i++; continue; }
|
|
74
|
+
if (c === ";" || c === "|" || c === "\n") { segs.push(buf); buf = ""; continue; }
|
|
75
|
+
buf += c;
|
|
76
|
+
}
|
|
77
|
+
segs.push(buf);
|
|
78
|
+
return segs;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Strip a leading shell wrapper (`bash -lc '…'`, `sh -c "…"`) and split the command
|
|
82
|
+
// chain, then look for a quest invocation at the head of any segment. Returns the
|
|
83
|
+
// marker id or null.
|
|
84
|
+
function markerIdInCommand(cmd) {
|
|
85
|
+
if (typeof cmd !== "string") return null;
|
|
86
|
+
let s = cmd.trim();
|
|
87
|
+
const wrap = s.match(/^(?:sudo\s+)?(?:ba)?sh\s+-l?c\s+(['"])([\s\S]*)\1\s*$/);
|
|
88
|
+
if (wrap) s = wrap[2].trim();
|
|
89
|
+
for (const seg of splitTopLevel(s)) {
|
|
90
|
+
const m = MARKER.exec(seg.trim());
|
|
91
|
+
if (m) return Number(m[2]); // m[1] = verb, m[2] = id
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// A Codex `command_execution` item's shell command, across the JSONL envelope
|
|
97
|
+
// shapes we know: top-level `item`, legacy `msg` (the item sits directly at
|
|
98
|
+
// `entry.msg`), and `msg.item`. Returns the command string or null.
|
|
99
|
+
function commandExecutionCommand(node) {
|
|
100
|
+
if (!node || typeof node !== "object") return null;
|
|
101
|
+
if (node.type === "command_execution" && typeof node.command === "string") return node.command;
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
26
104
|
const TERMINAL = ["complete", "blocked", "cancelled"];
|
|
27
105
|
|
|
28
106
|
async function readStdin() {
|
|
@@ -57,6 +135,59 @@ function firstTimestamp(text) {
|
|
|
57
135
|
return null;
|
|
58
136
|
}
|
|
59
137
|
|
|
138
|
+
// A tool_use block's shell command, or null. Bash invocations carry the command
|
|
139
|
+
// under `input.command`; only that field is a real command invocation. We never
|
|
140
|
+
// scan other input fields (e.g. an Edit's new_string or a Read's file_path), which
|
|
141
|
+
// could echo skill text and re-introduce the false positive.
|
|
142
|
+
function commandOf(input) {
|
|
143
|
+
return input && typeof input === "object" && typeof input.command === "string" ? input.command : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// The mutating-verb marker id from one transcript entry, considering only tool_use
|
|
147
|
+
// command invocations. Assistant messages carry an array of content blocks; string
|
|
148
|
+
// content (plain prose) and tool_result blocks (echoed output/file contents) are
|
|
149
|
+
// ignored. Matches `quest start <id>` or `quest checkpoint <id>` and returns the id.
|
|
150
|
+
function markerIdInEntry(entry) {
|
|
151
|
+
if (!entry || typeof entry !== "object") return null;
|
|
152
|
+
|
|
153
|
+
// Codex JSONL: a `command_execution` item under `item`, the legacy `msg`, or
|
|
154
|
+
// `msg.item`. Handling all three keeps executor detection robust across shapes.
|
|
155
|
+
for (const cand of [entry.item, entry.msg, entry.msg?.item]) {
|
|
156
|
+
const cmd = commandExecutionCommand(cand);
|
|
157
|
+
if (cmd != null) {
|
|
158
|
+
const id = markerIdInCommand(cmd);
|
|
159
|
+
if (id != null) return id;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Claude assistant messages: tool_use blocks carrying a shell command. String
|
|
164
|
+
// content (plain prose) and tool_result blocks are ignored.
|
|
165
|
+
const msg = entry.message;
|
|
166
|
+
const content = msg && typeof msg === "object" ? msg.content : null;
|
|
167
|
+
if (!Array.isArray(content)) return null; // string content is prose, never an invocation
|
|
168
|
+
for (const block of content) {
|
|
169
|
+
if (!block || typeof block !== "object" || block.type !== "tool_use") continue;
|
|
170
|
+
const id = markerIdInCommand(commandOf(block.input));
|
|
171
|
+
if (id != null) return id;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// The executor's quest id = the FIRST mutating invocation (`quest start <id>` or
|
|
177
|
+
// `quest checkpoint <id>`) that appears as a real command invocation, scanning
|
|
178
|
+
// entries in transcript order. Returns id or null. Per-entry parsing is what keeps
|
|
179
|
+
// skill-text examples from keying detection; read verbs never match at all.
|
|
180
|
+
function executorQuestId(text) {
|
|
181
|
+
for (const line of text.split("\n")) {
|
|
182
|
+
if (!line.trim()) continue;
|
|
183
|
+
let entry;
|
|
184
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
185
|
+
const id = markerIdInEntry(entry);
|
|
186
|
+
if (id != null) return id;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
60
191
|
try {
|
|
61
192
|
const raw = await readStdin();
|
|
62
193
|
let payload = {};
|
|
@@ -69,9 +200,8 @@ try {
|
|
|
69
200
|
try { transcript = readFileSync(transcriptPath, "utf8"); }
|
|
70
201
|
catch (err) { diag(`cannot read transcript (${err.code || err.message}); allowing`); allow(); }
|
|
71
202
|
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
const id = Number(m[1]);
|
|
203
|
+
const id = executorQuestId(transcript);
|
|
204
|
+
if (id == null) allow(); // no mutating quest invocation (read-only agent) — leave it alone, silently
|
|
75
205
|
|
|
76
206
|
// Prefer an explicit start field if a future payload carries one; else the
|
|
77
207
|
// transcript's first timestamp.
|
package/lib/cli.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import * as local from "./store-local.mjs";
|
|
|
11
11
|
import * as github from "./store-github.mjs";
|
|
12
12
|
import { openStore } from "./store.mjs";
|
|
13
13
|
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";
|
|
14
15
|
|
|
15
16
|
const PLUGIN_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
16
17
|
|
|
@@ -73,10 +74,96 @@ function getStore(cwd, env) {
|
|
|
73
74
|
return loadConfig(storeDir, env);
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
function preflightInitAgentInstall(cwd, env) {
|
|
78
|
+
const plans = [
|
|
79
|
+
["codex", installCodexAgents({ scope: "project", dryRun: true, cwd, env })],
|
|
80
|
+
["claude", installClaudeAgents({ scope: "project", dryRun: true, cwd, env })],
|
|
81
|
+
];
|
|
82
|
+
const conflicts = plans.flatMap(([provider, result]) => result.conflicts.map((c) => `${provider}:${c.path}`));
|
|
83
|
+
if (conflicts.length) {
|
|
84
|
+
throw new UsageError(
|
|
85
|
+
"native agent template conflict; inspect existing files or run " +
|
|
86
|
+
"`quest codex install-agents --scope project --force` / " +
|
|
87
|
+
"`quest claude install-agents --scope project --force` after resolving: " +
|
|
88
|
+
conflicts.join(", "),
|
|
89
|
+
{ command: "init" },
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return plans;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function runNativeSetupCommand(command, args, { out, cwd, env }, { label, doctor, install }) {
|
|
96
|
+
const [subcommand, ...rest] = args;
|
|
97
|
+
if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
98
|
+
out(renderCommandHelp(command));
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
if (subcommand === "doctor") {
|
|
102
|
+
const p = parse(command, rest, {}, { positionals: 0 });
|
|
103
|
+
if (p.help) {
|
|
104
|
+
out(renderCommandHelp(command));
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
const result = doctor({ cwd, env });
|
|
108
|
+
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
|
+
}
|
|
113
|
+
// 1 (generic diagnostic failure), not 5 — exit 5 is reserved for
|
|
114
|
+
// ContractError (a quest contract violation), which a doctor finding is not.
|
|
115
|
+
return result.ok ? 0 : 1;
|
|
116
|
+
}
|
|
117
|
+
if (subcommand === "install-agents") {
|
|
118
|
+
const p = parse(command, rest, {
|
|
119
|
+
scope: { type: "string", default: "project" },
|
|
120
|
+
"dry-run": { type: "boolean" },
|
|
121
|
+
force: { type: "boolean" },
|
|
122
|
+
}, { positionals: 0 });
|
|
123
|
+
if (p.help) {
|
|
124
|
+
out(renderCommandHelp(command));
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
if (!["project", "user"].includes(p.values.scope)) throw new UsageError(`--scope must be project or user (got "${p.values.scope}")`, { command });
|
|
128
|
+
const isDry = Boolean(p.values["dry-run"]);
|
|
129
|
+
const result = install({
|
|
130
|
+
scope: p.values.scope,
|
|
131
|
+
dryRun: isDry,
|
|
132
|
+
force: Boolean(p.values.force),
|
|
133
|
+
cwd,
|
|
134
|
+
env,
|
|
135
|
+
});
|
|
136
|
+
// A real run refuses to overwrite; a --dry-run PREVIEWS the conflict
|
|
137
|
+
// (its whole purpose) rather than erroring before showing the plan.
|
|
138
|
+
if (!result.ok && !isDry) {
|
|
139
|
+
const symlinkConflicts = result.conflicts.filter((c) => c.reason === "symlink");
|
|
140
|
+
if (symlinkConflicts.length) {
|
|
141
|
+
throw new UsageError(`refusing to write agent templates through symlinked path: ${symlinkConflicts.map((c) => c.path).join(", ")}`, { command });
|
|
142
|
+
}
|
|
143
|
+
const conflicts = result.conflicts.map((c) => c.path).join(", ");
|
|
144
|
+
throw new UsageError(`agent file already exists; pass --force to replace: ${conflicts}`, { command });
|
|
145
|
+
}
|
|
146
|
+
if (p.values.json) out(JSON.stringify(result));
|
|
147
|
+
else {
|
|
148
|
+
out(`${isDry ? "Would install" : "Installed"} ${label} agents to ${result.target}`);
|
|
149
|
+
for (const a of result.actions) out(` ${a.action.padEnd(9)} ${a.path}`);
|
|
150
|
+
if (!result.ok) out(` conflicts present — pass --force to replace: ${result.conflicts.map((c) => c.path).join(", ")}`);
|
|
151
|
+
}
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
throw new UsageError(`unknown ${command} subcommand "${subcommand}"`, { command });
|
|
155
|
+
}
|
|
156
|
+
|
|
76
157
|
async function dispatch(argv, ctx) {
|
|
77
158
|
const [command, ...rest] = argv;
|
|
78
159
|
const { out, errOut, cwd, env } = ctx;
|
|
79
160
|
|
|
161
|
+
if (command === "--version" || command === "-V" || command === "version") {
|
|
162
|
+
const versions = versionInfo();
|
|
163
|
+
out(versions.package);
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
80
167
|
if (!command) {
|
|
81
168
|
const storeDir = findStoreDir(cwd, env);
|
|
82
169
|
if (!storeDir) {
|
|
@@ -114,11 +201,16 @@ const HANDLERS = {
|
|
|
114
201
|
backend: { type: "string", default: "local" },
|
|
115
202
|
repo: { type: "string" },
|
|
116
203
|
"agents-md": { type: "boolean" },
|
|
204
|
+
"no-agents": { type: "boolean" },
|
|
117
205
|
});
|
|
118
206
|
if (p.help) return void out(renderCommandHelp("init")) ?? 0;
|
|
119
207
|
const backend = p.values.backend;
|
|
120
208
|
if (!["local", "github"].includes(backend)) throw new UsageError(`--backend must be local or github (got "${backend}")`, { command: "init" });
|
|
121
209
|
if (backend === "github" && !p.values.repo) throw new UsageError("--repo owner/name is required with --backend github", { command: "init" });
|
|
210
|
+
if (existsSync(join(cwd, ".quests", "config.json"))) {
|
|
211
|
+
throw new ContractError(`a quest store already exists at ${join(cwd, ".quests")}`, { hint: "use the existing store, or delete it first if you really mean to start over" });
|
|
212
|
+
}
|
|
213
|
+
const agentPlans = p.values["no-agents"] ? [] : preflightInitAgentInstall(cwd, env);
|
|
122
214
|
if (backend === "github") {
|
|
123
215
|
github.assertAuth(env); // green `gh auth status` required
|
|
124
216
|
github.ensureLabels(p.values.repo, env); // idempotent label creation
|
|
@@ -127,7 +219,7 @@ const HANDLERS = {
|
|
|
127
219
|
const config = {
|
|
128
220
|
backend,
|
|
129
221
|
...(p.values.repo ? { github: { repo: p.values.repo } } : {}),
|
|
130
|
-
defaults: { worker: "claude", claude: { model: "opus", effort: "xhigh" }, codex: { model: "gpt-5.5", reasoning_effort: "medium" }, max_iterations: 8, priority: "p2" },
|
|
222
|
+
defaults: { worker: "claude", claude: { model: "opus", effort: "xhigh" }, codex: { model: "gpt-5.5", reasoning_effort: "medium", goal_mode: "auto" }, max_iterations: 8, priority: "p2" },
|
|
131
223
|
notify: { command: "" },
|
|
132
224
|
};
|
|
133
225
|
writeFileSync(join(storeDir, "config.json"), JSON.stringify(config, null, 2) + "\n");
|
|
@@ -145,8 +237,15 @@ const HANDLERS = {
|
|
|
145
237
|
].join("\n");
|
|
146
238
|
appendFileSync(join(cwd, "AGENTS.md"), section);
|
|
147
239
|
}
|
|
240
|
+
for (const [provider] of agentPlans) {
|
|
241
|
+
const label = provider === "codex" ? "Codex" : "Claude";
|
|
242
|
+
const install = provider === "codex" ? installCodexAgents : installClaudeAgents;
|
|
243
|
+
const result = install({ scope: "project", cwd, env });
|
|
244
|
+
out(`Installed ${label} agents to ${result.target}`);
|
|
245
|
+
for (const a of result.actions) out(` ${a.action.padEnd(9)} ${a.path}`);
|
|
246
|
+
}
|
|
148
247
|
out(`Initialized ${backend} quest store at ${storeDir}`);
|
|
149
|
-
out(renderInitNextSteps(backend));
|
|
248
|
+
out(renderInitNextSteps(backend, { agentsInstalled: agentPlans.length > 0 }));
|
|
150
249
|
return 0;
|
|
151
250
|
},
|
|
152
251
|
|
|
@@ -309,6 +408,38 @@ const HANDLERS = {
|
|
|
309
408
|
return 0;
|
|
310
409
|
},
|
|
311
410
|
|
|
411
|
+
async reopen(args, { out, errOut, cwd, env }) {
|
|
412
|
+
const p = parse("reopen", args, { reason: { type: "string" } }, { positionals: 1 });
|
|
413
|
+
if (p.help) return void out(renderCommandHelp("reopen")) ?? 0;
|
|
414
|
+
const id = requireId(p, "reopen");
|
|
415
|
+
// --reason is validated in the store (a contract requirement, exit 5), not
|
|
416
|
+
// as a usage error — mirrors `cancel`. reopenQuest throws if it is missing.
|
|
417
|
+
const config = getStore(cwd, env);
|
|
418
|
+
const store = openStore(config, { env });
|
|
419
|
+
const q = store.reopenQuest(id, p.values.reason);
|
|
420
|
+
// Reopening a child of a *complete* parent epic is allowed, never blocked —
|
|
421
|
+
// but the epic's completion verdict may now be falsified, so warn the
|
|
422
|
+
// orchestrator on stderr (the epic-falsification cue). Best-effort: a
|
|
423
|
+
// missing/unloadable parent (or any lookup failure) must never fail an
|
|
424
|
+
// already-successful reopen. Runs for both backends via this one handler.
|
|
425
|
+
if (q.front.parent !== undefined) {
|
|
426
|
+
try {
|
|
427
|
+
const parent = store.loadQuest(q.front.parent);
|
|
428
|
+
if (parent.front.status === "complete") {
|
|
429
|
+
errOut(`quest: warning — quest ${id}'s parent epic ${q.front.parent} is complete; reopening this child may falsify the epic's completion verdict. Rule on it, and \`quest reopen ${q.front.parent} --reason "…"\` too if the integration gate no longer holds.`);
|
|
430
|
+
}
|
|
431
|
+
} catch {
|
|
432
|
+
/* best-effort: never fail the reopen on a parent-lookup problem */
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (p.values.json) out(JSON.stringify({ ok: true, id, status: q.front.status }));
|
|
436
|
+
else {
|
|
437
|
+
out(`Quest ${id} reopened — now in_progress (reason recorded in an audited checkpoint).`);
|
|
438
|
+
out(`Reopened quests are dispatched directly by id (\`quest-run ${id}\` or $quest:work ${id}); they do not re-enter \`quest list --ready\`.`);
|
|
439
|
+
}
|
|
440
|
+
return 0;
|
|
441
|
+
},
|
|
442
|
+
|
|
312
443
|
async edit(args, { out, cwd, env }) {
|
|
313
444
|
const p = parse("edit", args, {
|
|
314
445
|
"add-done-when": { type: "string", multiple: true },
|
|
@@ -398,4 +529,12 @@ const HANDLERS = {
|
|
|
398
529
|
else for (const r of runs) out(`${r.run_id} quest ${r.quest} ${r.worker} ${r.ended ? `ended ${r.final_status}` : "ACTIVE"} iters:${r.iterations} $${r.cost_usd.toFixed(2)}`);
|
|
399
530
|
return 0;
|
|
400
531
|
},
|
|
532
|
+
|
|
533
|
+
async codex(args, { out, cwd, env }) {
|
|
534
|
+
return runNativeSetupCommand("codex", args, { out, cwd, env }, { label: "Codex", doctor: codexDoctor, install: installCodexAgents });
|
|
535
|
+
},
|
|
536
|
+
|
|
537
|
+
async claude(args, { out, cwd, env }) {
|
|
538
|
+
return runNativeSetupCommand("claude", args, { out, cwd, env }, { label: "Claude", doctor: claudeDoctor, install: installClaudeAgents });
|
|
539
|
+
},
|
|
401
540
|
};
|