quest-loop 0.3.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 CHANGED
@@ -4,6 +4,53 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
5
5
  [SemVer](https://semver.org/).
6
6
 
7
+ ## Unreleased
8
+
9
+ ## [0.3.2] — 2026-07-08
10
+
11
+ ### Added
12
+ - Added Claude setup parity: `quest claude install-agents --scope project` and
13
+ `quest claude doctor` now mirror the existing Codex setup flow.
14
+
15
+ ### Changed
16
+ - `quest init` now installs project-scoped Codex and Claude native agent
17
+ templates by default, `--no-agents` skips that install, and conflicting
18
+ project templates fail init before `.quests/` is created unless users
19
+ intentionally rerun the explicit provider install command with `--force`.
20
+ - `$quest:orchestrate` now states that Codex native subagents are the default
21
+ dispatch path for serial and parallel waves; `quest-run` is fallback-only
22
+ unless headless/background execution is explicitly requested.
23
+ - `$quest:plan` now routes accepted Plan Mode implementation requests straight
24
+ to the orchestrator role instead of leaving room for parent-session
25
+ `$quest:work` execution.
26
+
27
+ ### Fixed
28
+ - `quest codex doctor` now fails on stale `quest` binaries on PATH, stale
29
+ installed Codex plugin versions, and duplicate Quest skill roots in
30
+ `codex debug prompt-input "noop"` output.
31
+
32
+ ## [0.3.1] — 2026-07-08
33
+
34
+ ### Changed
35
+ - `$quest:orchestrate` now documents Codex/Claude native subagent parity:
36
+ orchestrator-level goals, goal-mode executor/reviewer dispatch prompts, and
37
+ `quest-run --codex-goal-mode require` as the Codex headless fallback.
38
+ - `$quest:plan` now makes the accepted Plan Mode handoff explicit: the parent
39
+ session becomes the orchestrator and spawned quest executors implement the
40
+ code.
41
+ - `$quest:plan` now requires generated `quest create` commands to specify
42
+ `--worker`, `--model`, and `--effort` explicitly, and asks the user before
43
+ entering `$quest:orchestrate`.
44
+ - `quest codex doctor` now checks Codex `multi_agent` feature availability and
45
+ describes native-agent checks as installed template parity.
46
+ - Wave-level orchestrator goals now treat `cancelled` quests as terminal
47
+ alongside `complete` and `blocked` store statuses.
48
+
49
+ ### Fixed
50
+ - `quest codex doctor` now checks Codex `goals` feature availability, so a
51
+ setup with `multi_agent=true` but goal tools disabled fails readiness instead
52
+ of green-lighting native goal-mode dispatch.
53
+
7
54
  ## [0.3.0] — 2026-07-08
8
55
 
9
56
  ### Added
package/README.md CHANGED
@@ -53,6 +53,14 @@ Then, inside a Claude Code session:
53
53
  /plugin install quest@quest
54
54
  ```
55
55
 
56
+ Install Quest's project-scoped Claude agent templates and verify the local
57
+ Claude setup with:
58
+
59
+ ```bash
60
+ quest claude install-agents --scope project
61
+ quest claude doctor
62
+ ```
63
+
56
64
  For local development against a checkout, point Claude Code at the repo directly
57
65
  — no marketplace needed:
58
66
 
@@ -81,9 +89,9 @@ quest codex doctor
81
89
  codex debug prompt-input "noop"
82
90
  ```
83
91
 
84
- `quest codex doctor` checks the installed plugin version, hook parser, neutral
85
- skill roots, and native Codex custom agents. `codex debug prompt-input "noop"`
86
- should not print any hook parse warnings.
92
+ `quest codex doctor` checks the installed plugin version, Codex `multi_agent`
93
+ support, hook parser, neutral skill roots, and installed native-agent templates.
94
+ `codex debug prompt-input "noop"` should not print any hook parse warnings.
87
95
 
88
96
  #### Updating the Codex plugin
89
97
 
@@ -129,7 +137,25 @@ quest lint --all
129
137
  quest list --ready
130
138
  ```
131
139
 
132
- Work the quest **in-session** with the skill:
140
+ By default, `quest init` also installs project-scoped native agent templates for
141
+ both providers:
142
+
143
+ - `.codex/agents/quest-executor.toml`
144
+ - `.codex/agents/quest-reviewer.toml`
145
+ - `.claude/agents/quest-executor.md`
146
+ - `.claude/agents/quest-reviewer.md`
147
+
148
+ Use `quest init --no-agents` when you only want the `.quests/` store. If an
149
+ existing project agent template would be replaced, init fails before creating
150
+ `.quests/`; inspect the conflicting files, run the explicit provider install
151
+ command with `--force` only if you intend to replace them, then rerun
152
+ `quest init`.
153
+
154
+ Project-scoped agent templates install at the Git repository root. For a nested
155
+ quest store, run `quest init --no-agents` in the nested directory and set
156
+ `QUEST_DIR` for agents launched from elsewhere, or initialize from the repo root.
157
+
158
+ Work one small quest **in-session** with the skill:
133
159
 
134
160
  ```
135
161
  $quest:work 12
@@ -144,6 +170,29 @@ form.
144
170
  quest-run 12
145
171
  ```
146
172
 
173
+ For planned or multi-quest work, prefer orchestration:
174
+
175
+ ```
176
+ $quest:orchestrate
177
+ ```
178
+
179
+ In Codex, the orchestrator sets a wave-level `create_goal`, then spawns native
180
+ `quest-executor` / `quest-reviewer` subagents with their own quest-level goals.
181
+ In Claude Code, the same flow uses `/goal` and the same bundled subagents. If a
182
+ Codex native subagent surface is unavailable after `tool_search`, or if you
183
+ explicitly want headless/background execution, use the fallback with goal mode
184
+ required:
185
+
186
+ ```bash
187
+ quest-run 12 --worker codex --codex-goal-mode require
188
+ ```
189
+
190
+ When a `$quest:plan` result is accepted from Plan Mode, the parent agent should
191
+ stay the orchestrator: create/lint quest records if needed, set the wave goal,
192
+ spawn subagents, verify checkpoints, and rule on reviewer findings. Product
193
+ implementation belongs to the spawned executor for each quest, not to the parent
194
+ session via `$quest:work`.
195
+
147
196
  Each iteration ends by recording evidence — a checkpoint a fresh session can
148
197
  resume from:
149
198
 
@@ -172,7 +221,7 @@ full base protocol lives in
172
221
 
173
222
  | Command | Purpose |
174
223
  |---|---|
175
- | `quest init` | Create a quest store (`.quests/`) in the current directory |
224
+ | `quest init` | Create a quest store (`.quests/`) and install project-scoped Codex/Claude agent templates by default |
176
225
  | `quest create` | Create a new quest (the only way records are born) |
177
226
  | `quest list` | List quests (filter by status, parent, or readiness) |
178
227
  | `quest show` | Show a quest record in full |
@@ -185,7 +234,8 @@ full base protocol lives in
185
234
  | `quest amend` | Append a numbered protocol amendment (retro output) |
186
235
  | `quest protocol` | Print the loop protocol + this store's local amendments |
187
236
  | `quest runs` | Show headless runner activity (from `.quests/runs.ndjson`) |
188
- | `quest codex` | Validate Codex-native setup and install native agent templates |
237
+ | `quest codex` | Validate Codex-native setup and install Codex native agent templates |
238
+ | `quest claude` | Validate Claude-native setup and install Claude native agent templates |
189
239
 
190
240
  ## quest-run (headless runner)
191
241
 
@@ -12,6 +12,16 @@ only insofar as it is recorded in the quest's checkpoint trail.
12
12
 
13
13
  ## First actions, always
14
14
 
15
+ Enter native goal mode before doing implementation work:
16
+
17
+ ```text
18
+ /goal quest <id> has a new checkpoint whose quest_status is complete or blocked
19
+ in `quest show <id> --json`
20
+ ```
21
+
22
+ If the harness cannot set that goal, say so in your final report; the
23
+ checkpoint trail is still mandatory.
24
+
15
25
  ```bash
16
26
  quest show <id> --json # the contract and every prior checkpoint
17
27
  quest protocol # the loop rules + this store's amendments
@@ -6,11 +6,17 @@ You execute ONE quest. Decomposition, dispatching, and rulings belong to the
6
6
  orchestrator. Your work exists only insofar as it is recorded in the quest's
7
7
  checkpoint trail.
8
8
 
9
- First actions, always: `quest show <id> --json`, `quest protocol`,
10
- `git log --oneline -5`. Then follow the work skill exactly: smallest unfinished
11
- milestone -> the quest's STATED validation loop -> commit green ->
12
- `quest checkpoint`. The record is your entire spec; if it is genuinely
13
- insufficient, checkpoint blocked saying exactly what is missing.
9
+ First actions, always: call `create_goal` with the stopping condition "quest
10
+ <id> has a new checkpoint whose quest_status is complete or blocked in
11
+ `quest show <id> --json`"; verify it with `get_goal`; then run `quest show <id>
12
+ --json`, `quest protocol`, and `git log --oneline -5`. If goal tools are not
13
+ available, say so honestly in your final report; the checkpoint trail is still
14
+ mandatory.
15
+
16
+ Then follow the work skill exactly: smallest unfinished milestone -> the quest's
17
+ STATED validation loop -> commit green -> `quest checkpoint`. The record is your
18
+ entire spec; if it is genuinely insufficient, checkpoint blocked saying exactly
19
+ what is missing.
14
20
 
15
21
  Non-negotiable backstops:
16
22
  - Never stop without recording a checkpoint via `quest checkpoint`.
@@ -23,5 +29,6 @@ Non-negotiable backstops:
23
29
 
24
30
  Final report: run `quest show <id>` so recorded state is visible, then report
25
31
  the quest_status you recorded, the checkpoint timestamp, and a one-line
26
- evidence summary citing the decisive command + result.
32
+ evidence summary citing the decisive command + result. Only call
33
+ `update_goal(status="complete")` after the checkpoint exists.
27
34
  """
@@ -11,6 +11,15 @@ not fix anything — you report findings the orchestrator must disposition.
11
11
 
12
12
  ## Procedure
13
13
 
14
+ Enter native goal mode before verification:
15
+
16
+ ```text
17
+ /goal return an accept or iterate verdict for quest <id> with evidence
18
+ ```
19
+
20
+ If the harness cannot set that goal, say so in your report; the review verdict
21
+ and evidence are still mandatory.
22
+
14
23
  1. Read the contract and trail: `quest show <id> --json`, `quest protocol`.
15
24
  2. Read the ACTUAL changes (diff/commits the checkpoints cite), not the
16
25
  summary of them.
@@ -5,17 +5,21 @@ developer_instructions = """
5
5
  You verify, adversarially, that a quest claiming complete earned it. You do not
6
6
  fix anything — you report findings the orchestrator must disposition.
7
7
 
8
- Procedure: read the contract and trail (`quest show <id> --json`,
9
- `quest protocol`); read the ACTUAL changes the checkpoints cite, not the
10
- summary; re-run the quest's stated Validation loop yourself claims must
11
- reproduce; interrogate each Done-when item (which command output proves it?
12
- was a stated check substituted silently? were tests edited to pass? did scope
13
- drift past the Objective without a recorded expansion?); check the trail
14
- itself (could a zero-context session resume from the record alone?).
8
+ Procedure: first call `create_goal` with the stopping condition "return an
9
+ accept or iterate verdict for quest <id> with evidence"; verify it with
10
+ `get_goal`. If goal tools are not available, say so honestly in your report;
11
+ the review verdict and evidence are still mandatory. Then read the contract and
12
+ trail (`quest show <id> --json`, `quest protocol`); read the ACTUAL changes the
13
+ checkpoints cite, not the summary; re-run the quest's stated Validation loop
14
+ yourself claims must reproduce; interrogate each Done-when item (which command
15
+ output proves it? was a stated check substituted silently? were tests edited to
16
+ pass? did scope drift past the Objective without a recorded expansion?); check
17
+ the trail itself (could a zero-context session resume from the record alone?).
15
18
 
16
19
  Report findings ordered by severity, each with the evidence (command + output
17
20
  or file + symbol) and why it blocks acceptance. If nothing survived scrutiny,
18
21
  say "no findings" plainly — do not invent nitpicks. End with a verdict:
19
22
  accept, or iterate with exactly what must change. Every finding must receive a
20
23
  disposition (fixed / follow-up quest / rejected-with-reason) before acceptance.
24
+ Only call `update_goal(status="complete")` after the verdict exists.
21
25
  """
package/lib/cli.mjs CHANGED
@@ -11,7 +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 { doctor as codexDoctor, installAgents as installCodexAgents, versionInfo } from "./codex-native.mjs";
14
+ import { claudeDoctor, doctor as codexDoctor, installAgents as installCodexAgents, installClaudeAgents, versionInfo } from "./codex-native.mjs";
15
15
 
16
16
  const PLUGIN_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
17
17
 
@@ -74,6 +74,86 @@ function getStore(cwd, env) {
74
74
  return loadConfig(storeDir, env);
75
75
  }
76
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
+
77
157
  async function dispatch(argv, ctx) {
78
158
  const [command, ...rest] = argv;
79
159
  const { out, errOut, cwd, env } = ctx;
@@ -121,11 +201,16 @@ const HANDLERS = {
121
201
  backend: { type: "string", default: "local" },
122
202
  repo: { type: "string" },
123
203
  "agents-md": { type: "boolean" },
204
+ "no-agents": { type: "boolean" },
124
205
  });
125
206
  if (p.help) return void out(renderCommandHelp("init")) ?? 0;
126
207
  const backend = p.values.backend;
127
208
  if (!["local", "github"].includes(backend)) throw new UsageError(`--backend must be local or github (got "${backend}")`, { command: "init" });
128
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);
129
214
  if (backend === "github") {
130
215
  github.assertAuth(env); // green `gh auth status` required
131
216
  github.ensureLabels(p.values.repo, env); // idempotent label creation
@@ -152,8 +237,15 @@ const HANDLERS = {
152
237
  ].join("\n");
153
238
  appendFileSync(join(cwd, "AGENTS.md"), section);
154
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
+ }
155
247
  out(`Initialized ${backend} quest store at ${storeDir}`);
156
- out(renderInitNextSteps(backend));
248
+ out(renderInitNextSteps(backend, { agentsInstalled: agentPlans.length > 0 }));
157
249
  return 0;
158
250
  },
159
251
 
@@ -439,60 +531,10 @@ const HANDLERS = {
439
531
  },
440
532
 
441
533
  async codex(args, { out, cwd, env }) {
442
- const [subcommand, ...rest] = args;
443
- if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
444
- out(renderCommandHelp("codex"));
445
- return 0;
446
- }
447
- if (subcommand === "doctor") {
448
- const p = parse("codex", rest, {}, { positionals: 0 });
449
- if (p.help) {
450
- out(renderCommandHelp("codex"));
451
- return 0;
452
- }
453
- const result = codexDoctor({ cwd, env });
454
- if (p.values.json) out(JSON.stringify(result));
455
- else {
456
- for (const c of result.checks) out(`${c.ok ? "OK " : "ERR"} ${c.name}: ${c.detail}`);
457
- out(result.ok ? "codex doctor: OK" : "codex doctor: problems found");
458
- }
459
- // 1 (generic diagnostic failure), not 5 — exit 5 is reserved for
460
- // ContractError (a quest contract violation), which a doctor finding is not.
461
- return result.ok ? 0 : 1;
462
- }
463
- if (subcommand === "install-agents") {
464
- const p = parse("codex", rest, {
465
- scope: { type: "string", default: "project" },
466
- "dry-run": { type: "boolean" },
467
- force: { type: "boolean" },
468
- }, { positionals: 0 });
469
- if (p.help) {
470
- out(renderCommandHelp("codex"));
471
- return 0;
472
- }
473
- if (!["project", "user"].includes(p.values.scope)) throw new UsageError(`--scope must be project or user (got "${p.values.scope}")`, { command: "codex" });
474
- const isDry = Boolean(p.values["dry-run"]);
475
- const result = installCodexAgents({
476
- scope: p.values.scope,
477
- dryRun: isDry,
478
- force: Boolean(p.values.force),
479
- cwd,
480
- env,
481
- });
482
- // A real run refuses to overwrite; a --dry-run PREVIEWS the conflict
483
- // (its whole purpose) rather than erroring before showing the plan.
484
- if (!result.ok && !isDry) {
485
- const conflicts = result.conflicts.map((c) => c.path).join(", ");
486
- throw new UsageError(`agent file already exists; pass --force to replace: ${conflicts}`, { command: "codex" });
487
- }
488
- if (p.values.json) out(JSON.stringify(result));
489
- else {
490
- out(`${isDry ? "Would install" : "Installed"} Codex agents to ${result.target}`);
491
- for (const a of result.actions) out(` ${a.action.padEnd(9)} ${a.path}`);
492
- if (!result.ok) out(` conflicts present — pass --force to replace: ${result.conflicts.map((c) => c.path).join(", ")}`);
493
- }
494
- return 0;
495
- }
496
- throw new UsageError(`unknown codex subcommand "${subcommand}"`, { command: "codex" });
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 });
497
539
  },
498
540
  };
@@ -1,7 +1,7 @@
1
- // Codex-native setup helpers. These are intentionally kept out of the quest
2
- // store layer: they inspect/install Codex integration files, not quest records.
1
+ // Native setup helpers. These are intentionally kept out of the quest store
2
+ // layer: they inspect/install agent integration files, not quest records.
3
3
 
4
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { homedir, tmpdir } from "node:os";
6
6
  import { dirname, join, resolve } from "node:path";
7
7
  import { spawnSync } from "node:child_process";
@@ -9,6 +9,20 @@ import { fileURLToPath } from "node:url";
9
9
 
10
10
  const PLUGIN_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
11
11
  const AGENTS = ["quest-executor", "quest-reviewer"];
12
+ const PROVIDERS = {
13
+ codex: {
14
+ label: "Codex",
15
+ extension: "toml",
16
+ projectDir: ".codex",
17
+ userDir: join(homedir(), ".codex", "agents"),
18
+ },
19
+ claude: {
20
+ label: "Claude",
21
+ extension: "md",
22
+ projectDir: ".claude",
23
+ userDir: join(homedir(), ".claude", "agents"),
24
+ },
25
+ };
12
26
 
13
27
  function readJson(rel) {
14
28
  return JSON.parse(readFileSync(join(PLUGIN_ROOT, rel), "utf8"));
@@ -45,27 +59,44 @@ function projectRoot(cwd, env) {
45
59
  return resolve(cwd);
46
60
  }
47
61
 
48
- function targetDir(scope, cwd, env) {
49
- if (scope === "user") return join(homedir(), ".codex", "agents");
50
- if (scope === "project") return join(projectRoot(cwd, env), ".codex", "agents");
62
+ function providerConfig(provider) {
63
+ const cfg = PROVIDERS[provider];
64
+ if (!cfg) throw new Error(`unknown native agent provider "${provider}"`);
65
+ return cfg;
66
+ }
67
+
68
+ function targetDir(provider, scope, cwd, env) {
69
+ const cfg = providerConfig(provider);
70
+ if (scope === "user") return cfg.userDir;
71
+ if (scope === "project") return join(projectRoot(cwd, env), cfg.projectDir, "agents");
51
72
  throw new Error(`--scope must be project or user (got "${scope}")`);
52
73
  }
53
74
 
54
- function installedAgentDirs(cwd, env) {
75
+ function installedAgentDirs(provider, cwd, env) {
76
+ const cfg = providerConfig(provider);
55
77
  return [
56
- join(projectRoot(cwd, env), ".codex", "agents"),
57
- join(homedir(), ".codex", "agents"),
78
+ join(projectRoot(cwd, env), cfg.projectDir, "agents"),
79
+ cfg.userDir,
58
80
  ];
59
81
  }
60
82
 
61
83
  function sameFile(path, text) {
62
84
  try {
85
+ if (lstatSync(path).isSymbolicLink()) return false;
63
86
  return readFileSync(path, "utf8") === text;
64
87
  } catch {
65
88
  return false;
66
89
  }
67
90
  }
68
91
 
92
+ function isSymlink(path) {
93
+ try {
94
+ return lstatSync(path).isSymbolicLink();
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
69
100
  function extractTexts(promptInputJson) {
70
101
  try {
71
102
  const items = JSON.parse(promptInputJson);
@@ -86,10 +117,10 @@ function extractTexts(promptInputJson) {
86
117
 
87
118
  function questSkillEntries(promptInputJson) {
88
119
  const text = extractTexts(promptInputJson);
89
- const entries = new Map();
120
+ const entries = [];
90
121
  const re = /- quest:(plan|work|orchestrate|retro|protocol|setup):[^\n]+\(file: ([^)]+)\)/g;
91
- for (const m of text.matchAll(re)) entries.set(m[1], m[2]);
92
- return [...entries.entries()].map(([name, path]) => ({ name, path })).sort((a, b) => a.name.localeCompare(b.name));
122
+ for (const m of text.matchAll(re)) entries.push({ name: m[1], path: m[2] });
123
+ return entries.sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path));
93
124
  }
94
125
 
95
126
  function pathRootForQuestSkill(path) {
@@ -99,6 +130,24 @@ function pathRootForQuestSkill(path) {
99
130
  return i === -1 ? path : path.slice(0, i);
100
131
  }
101
132
 
133
+ function featureEnabled(output, name) {
134
+ for (const line of String(output || "").split("\n")) {
135
+ const parts = line.trim().split(/\s+/);
136
+ if (parts[0] === name) return parts.at(-1) === "true";
137
+ }
138
+ return false;
139
+ }
140
+
141
+ function upgradeHint() {
142
+ return "run `codex plugin marketplace upgrade quest`, `codex plugin add quest@quest`, then start a new Codex thread";
143
+ }
144
+
145
+ function duplicateSkillNames(entries) {
146
+ const counts = new Map();
147
+ for (const entry of entries) counts.set(entry.name, (counts.get(entry.name) || 0) + 1);
148
+ return [...counts.entries()].filter(([, count]) => count > 1).map(([name]) => name).sort();
149
+ }
150
+
102
151
  export function versionInfo() {
103
152
  // package.json is always shipped; the plugin manifests may be absent on a
104
153
  // CLI-only npm install, so they are read tolerantly.
@@ -112,36 +161,101 @@ export function versionInfo() {
112
161
  };
113
162
  }
114
163
 
115
- export function installAgents({ scope = "project", dryRun = false, force = false, cwd = process.cwd(), env = process.env } = {}) {
116
- const dir = targetDir(scope, cwd, env);
164
+ function installProviderAgents(provider, { scope = "project", dryRun = false, force = false, cwd = process.cwd(), env = process.env } = {}) {
165
+ const cfg = providerConfig(provider);
166
+ const dir = targetDir(provider, scope, cwd, env);
167
+ const dirConflicts = [dirname(dir), dir]
168
+ .filter((path, index, all) => all.indexOf(path) === index)
169
+ .filter((path) => existsSync(path) && isSymlink(path))
170
+ .map((path) => ({ name: null, path, reason: "symlink" }));
117
171
 
118
172
  // Plan every agent first; only touch disk once the whole set is known to be
119
173
  // conflict-free. A mixed create+conflict run must not leave a partial install.
120
174
  const plans = AGENTS.map((name) => {
121
- const src = join(PLUGIN_ROOT, "agents", `${name}.toml`);
122
- const dest = join(dir, `${name}.toml`);
175
+ const src = join(PLUGIN_ROOT, "agents", `${name}.${cfg.extension}`);
176
+ const dest = join(dir, `${name}.${cfg.extension}`);
123
177
  const text = readFileSync(src, "utf8");
124
178
  const exists = existsSync(dest);
179
+ const symlink = exists && isSymlink(dest);
125
180
  let action;
126
- if (exists && sameFile(dest, text)) action = "unchanged";
181
+ if (symlink) action = "conflict";
182
+ else if (exists && sameFile(dest, text)) action = "unchanged";
127
183
  else if (exists && !force) action = "conflict";
128
184
  else action = exists ? "replace" : "create";
129
- return { name, path: dest, action, text };
185
+ return { name, path: dest, action, text, reason: symlink ? "symlink" : undefined };
130
186
  });
131
187
 
132
- const conflicts = plans.filter((p) => p.action === "conflict").map(({ name, path }) => ({ name, path }));
188
+ const conflicts = [
189
+ ...dirConflicts,
190
+ ...plans.filter((p) => p.action === "conflict").map(({ name, path, reason }) => ({ name, path, ...(reason ? { reason } : {}) })),
191
+ ];
133
192
  const actions = plans.map(({ name, path, action }) => ({ name, path, action }));
134
193
  const ok = conflicts.length === 0;
135
194
 
136
195
  if (ok && !dryRun) {
196
+ if (dirConflicts.length) throw new Error(`refusing to write agent templates through symlinked path: ${dirConflicts.map((c) => c.path).join(", ")}`);
137
197
  for (const p of plans) {
138
198
  if (p.action === "unchanged") continue;
139
199
  mkdirSync(dir, { recursive: true });
200
+ if (isSymlink(dirname(p.path)) || isSymlink(p.path)) throw new Error(`refusing to write agent template through symlinked path: ${p.path}`);
140
201
  writeFileSync(p.path, p.text);
141
202
  }
142
203
  }
143
204
 
144
- return { ok, scope, target: dir, dry_run: dryRun, actions, conflicts };
205
+ return { ok, provider, scope, target: dir, dry_run: dryRun, actions, conflicts };
206
+ }
207
+
208
+ export function installAgents(options = {}) {
209
+ return installProviderAgents("codex", options);
210
+ }
211
+
212
+ export function installClaudeAgents(options = {}) {
213
+ return installProviderAgents("claude", options);
214
+ }
215
+
216
+ function questCliPathCheck(cwd, env, versions) {
217
+ const pathQuestVersion = runCmd("quest", ["--version"], { cwd, env });
218
+ const pathVersion = pathQuestVersion.status === 0 ? pathQuestVersion.stdout.trim() : null;
219
+ return check(
220
+ "quest-cli-path",
221
+ pathQuestVersion.status !== 0 || pathVersion === versions.package,
222
+ pathQuestVersion.status === 0
223
+ ? (pathVersion === versions.package
224
+ ? `quest on PATH=${pathVersion}`
225
+ : `quest on PATH=${pathVersion}, package=${versions.package}; update with \`npm install -g quest-loop@${versions.package}\` or run the checkout binary explicitly`)
226
+ : `quest not found on PATH (${(pathQuestVersion.stderr || pathQuestVersion.error?.message || "not found").trim()}); current package=${versions.package}`,
227
+ { path_version: pathVersion, expected_version: versions.package },
228
+ );
229
+ }
230
+
231
+ function nativeAgentsCheck(provider, cwd, env) {
232
+ const cfg = providerConfig(provider);
233
+ const agentDirs = installedAgentDirs(provider, cwd, env);
234
+ const missing = [];
235
+ const stale = [];
236
+ const found = {};
237
+ for (const name of AGENTS) {
238
+ const bundled = readFileSync(join(PLUGIN_ROOT, "agents", `${name}.${cfg.extension}`), "utf8");
239
+ const path = agentDirs.map((dir) => join(dir, `${name}.${cfg.extension}`)).find((p) => existsSync(p));
240
+ if (!path) {
241
+ missing.push(name);
242
+ continue;
243
+ }
244
+ found[name] = path;
245
+ // Existence alone can mask a stale user-scope copy shadowing an out-of-date
246
+ // project install — compare against the bundled definition.
247
+ if (!sameFile(path, bundled)) stale.push(name);
248
+ }
249
+ const agentDetail = [
250
+ missing.length ? `missing: ${missing.join(", ")}` : null,
251
+ stale.length ? `stale (run install-agents --force): ${stale.join(", ")}` : null,
252
+ ].filter(Boolean).join("; ") || `installed ${cfg.label} templates current: ${Object.values(found).join(", ")}`;
253
+ return check(
254
+ "native-agents",
255
+ missing.length === 0 && stale.length === 0,
256
+ agentDetail,
257
+ { found, missing, stale, provider },
258
+ );
145
259
  }
146
260
 
147
261
  export function doctor({ cwd = process.cwd(), env = process.env } = {}) {
@@ -163,6 +277,8 @@ export function doctor({ cwd = process.cwd(), env = process.env } = {}) {
163
277
  { versions },
164
278
  ));
165
279
 
280
+ checks.push(questCliPathCheck(cwd, env, versions));
281
+
166
282
  const codexVersion = runCmd("codex", ["--version"], { cwd, env });
167
283
  checks.push(check(
168
284
  "codex-cli",
@@ -188,7 +304,35 @@ export function doctor({ cwd = process.cwd(), env = process.env } = {}) {
188
304
  checks.push(check(
189
305
  "plugin-version",
190
306
  Boolean(installed && installed.version === manifestVersion),
191
- installed ? `installed=${installed.version}, manifest=${manifestVersion}` : `manifest=${manifestVersion}`,
307
+ installed
308
+ ? (installed.version === manifestVersion
309
+ ? `installed=${installed.version}, manifest=${manifestVersion}`
310
+ : `installed=${installed.version}, manifest=${manifestVersion}; ${upgradeHint()}`)
311
+ : `manifest=${manifestVersion}; ${upgradeHint()}`,
312
+ ));
313
+
314
+ const features = runCmd("codex", ["features", "list"], { cwd, env });
315
+ const multiAgent = features.status === 0 && featureEnabled(features.stdout, "multi_agent");
316
+ const goals = features.status === 0 && featureEnabled(features.stdout, "goals");
317
+ checks.push(check(
318
+ "multi-agent-feature",
319
+ multiAgent,
320
+ features.status === 0
321
+ ? (multiAgent
322
+ ? "multi_agent enabled; native Codex quest-executor dispatch available"
323
+ : "multi_agent is not enabled; use quest-run fallback")
324
+ : ((features.stderr || features.error?.message || "could not inspect Codex feature flags").trim() + "; use quest-run fallback"),
325
+ { feature: "multi_agent" },
326
+ ));
327
+ checks.push(check(
328
+ "goals-feature",
329
+ goals,
330
+ features.status === 0
331
+ ? (goals
332
+ ? "goals enabled; native create_goal/get_goal dispatch available"
333
+ : "goals is not enabled; native goal-mode dispatch requires create_goal/get_goal")
334
+ : ((features.stderr || features.error?.message || "could not inspect Codex feature flags").trim() + "; native goal-mode dispatch unavailable"),
335
+ { feature: "goals" },
192
336
  ));
193
337
 
194
338
  const debug = runCmd("codex", ["debug", "prompt-input", "noop"], { cwd: tmpdir(), env });
@@ -207,40 +351,75 @@ export function doctor({ cwd = process.cwd(), env = process.env } = {}) {
207
351
  const missingSkills = requiredSkills.filter((name) => !entries.some((entry) => entry.name === name));
208
352
  const paths = entries.map((entry) => entry.path);
209
353
  const roots = [...new Set(paths.map(pathRootForQuestSkill))];
354
+ const duplicateSkills = duplicateSkillNames(entries);
210
355
  checks.push(check(
211
356
  "single-neutral-skill-root",
212
- missingSkills.length === 0 && roots.length === 1,
213
- paths.length ? `${entries.length} quest skills across ${roots.length} root(s)` : "quest skills not found in neutral prompt-input",
214
- { skills: entries, missing_skills: missingSkills, skill_paths: paths, skill_roots: roots },
357
+ missingSkills.length === 0 && roots.length === 1 && duplicateSkills.length === 0,
358
+ paths.length
359
+ ? `${entries.length} quest skill entries across ${roots.length} root(s)` +
360
+ (duplicateSkills.length ? `; duplicate skill names: ${duplicateSkills.join(", ")}; ${upgradeHint()}` : "")
361
+ : "quest skills not found in neutral prompt-input",
362
+ { skills: entries, missing_skills: missingSkills, duplicate_skills: duplicateSkills, skill_paths: paths, skill_roots: roots },
215
363
  ));
216
364
  }
217
365
 
218
- const agentDirs = installedAgentDirs(cwd, env);
219
- const missing = [];
220
- const stale = [];
221
- const found = {};
222
- for (const name of AGENTS) {
223
- const bundled = readFileSync(join(PLUGIN_ROOT, "agents", `${name}.toml`), "utf8");
224
- const path = agentDirs.map((dir) => join(dir, `${name}.toml`)).find((p) => existsSync(p));
225
- if (!path) {
226
- missing.push(name);
227
- continue;
228
- }
229
- found[name] = path;
230
- // Existence alone can mask a stale user-scope copy shadowing an out-of-date
231
- // project install — compare against the bundled definition.
232
- if (!sameFile(path, bundled)) stale.push(name);
233
- }
234
- const agentDetail = [
235
- missing.length ? `missing: ${missing.join(", ")}` : null,
236
- stale.length ? `stale (run install-agents --force): ${stale.join(", ")}` : null,
237
- ].filter(Boolean).join("; ") || `found: ${Object.values(found).join(", ")}`;
366
+ checks.push(nativeAgentsCheck("codex", cwd, env));
367
+
368
+ return { ok: checks.every((c) => c.ok), checks };
369
+ }
370
+
371
+ export function claudeDoctor({ cwd = process.cwd(), env = process.env } = {}) {
372
+ const checks = [];
373
+ const versions = versionInfo();
374
+ const manifestVersion = versions.claude ?? versions.package;
375
+ const presentManifests = [["codex", versions.codex], ["claude", versions.claude]].filter(([, v]) => v != null);
376
+ const absent = ["codex", "claude"].filter((k) => versions[k] == null);
377
+
238
378
  checks.push(check(
239
- "native-agents",
240
- missing.length === 0 && stale.length === 0,
241
- agentDetail,
242
- { found, missing, stale },
379
+ "version-sync",
380
+ presentManifests.every(([, v]) => v === versions.package),
381
+ `package=${versions.package}, codex=${versions.codex ?? "absent"}, claude=${versions.claude ?? "absent"}` +
382
+ (absent.length ? ` (${absent.join(", ")} manifest absent — CLI-only install)` : ""),
383
+ { versions },
243
384
  ));
244
385
 
386
+ checks.push(questCliPathCheck(cwd, env, versions));
387
+
388
+ const claudeVersion = runCmd("claude", ["--version"], { cwd, env });
389
+ checks.push(check(
390
+ "claude-cli",
391
+ claudeVersion.status === 0,
392
+ claudeVersion.status === 0 ? claudeVersion.stdout.trim() : (claudeVersion.stderr || claudeVersion.error?.message || "claude not found").trim(),
393
+ ));
394
+
395
+ if (claudeVersion.status === 0) {
396
+ const list = runCmd("claude", ["plugin", "list", "--json"], { cwd, env });
397
+ let installed;
398
+ try {
399
+ const parsed = JSON.parse(list.stdout || "[]");
400
+ const plugins = Array.isArray(parsed) ? parsed : (parsed.installed || []);
401
+ installed = plugins.find((p) => p.id === "quest@quest" || p.pluginId === "quest@quest" || (p.name === "quest" && p.marketplaceName === "quest"));
402
+ } catch {
403
+ installed = null;
404
+ }
405
+ checks.push(check(
406
+ "plugin-installed",
407
+ list.status === 0 && installed?.enabled === true,
408
+ installed ? `quest@quest ${installed.version} enabled=${installed.enabled}` : (list.stderr || "quest@quest not installed").trim(),
409
+ { installed: installed ?? null },
410
+ ));
411
+ checks.push(check(
412
+ "plugin-version",
413
+ Boolean(installed && installed.version === manifestVersion),
414
+ installed
415
+ ? (installed.version === manifestVersion
416
+ ? `installed=${installed.version}, manifest=${manifestVersion}`
417
+ : `installed=${installed.version}, manifest=${manifestVersion}; run \`claude plugin update quest@quest\`, then restart Claude Code`)
418
+ : `manifest=${manifestVersion}; run \`claude plugin install quest@quest\`, then restart Claude Code`,
419
+ ));
420
+ }
421
+
422
+ checks.push(nativeAgentsCheck("claude", cwd, env));
423
+
245
424
  return { ok: checks.every((c) => c.ok), checks };
246
425
  }
package/lib/help.mjs CHANGED
@@ -9,12 +9,13 @@ iterations that end in checkpoints; \`quest-run\` drives headless workers.`;
9
9
 
10
10
  export const COMMANDS = {
11
11
  init: {
12
- purpose: "Create a quest store (.quests/) in the current directory",
13
- usage: "quest init [--backend local|github] [--repo owner/name] [--agents-md]",
12
+ purpose: "Create a quest store (.quests/) and install project agent templates",
13
+ usage: "quest init [--backend local|github] [--repo owner/name] [--agents-md] [--no-agents]",
14
14
  flags: [
15
15
  ["--backend <b>", "record storage: local files (default) or github issues"],
16
16
  ["--repo <owner/name>", "required with --backend github"],
17
17
  ["--agents-md", "append a quest orientation section to ./AGENTS.md"],
18
+ ["--no-agents", "skip installing .codex/agents and .claude/agents templates"],
18
19
  ],
19
20
  example: "quest init",
20
21
  },
@@ -140,7 +141,7 @@ export const COMMANDS = {
140
141
  purpose: "Inspect or install Quest's native Codex integration",
141
142
  usage: "quest codex doctor [--json] | quest codex install-agents [--scope project|user] [--dry-run] [--force] [--json]",
142
143
  flags: [
143
- ["doctor", "check Codex CLI, plugin install/version, hooks, skill roots, and native agents"],
144
+ ["doctor", "check Codex CLI, multi-agent support, plugin install/version, hooks, skill roots, and native-agent templates"],
144
145
  ["install-agents", "install quest-executor and quest-reviewer as native Codex custom agents"],
145
146
  ["--scope <s>", "project (default: .codex/agents at repo root) or user (~/.codex/agents)"],
146
147
  ["--dry-run", "show intended agent writes without changing files"],
@@ -149,6 +150,19 @@ export const COMMANDS = {
149
150
  ],
150
151
  example: "quest codex install-agents --scope project && quest codex doctor",
151
152
  },
153
+ claude: {
154
+ purpose: "Inspect or install Quest's native Claude Code integration",
155
+ usage: "quest claude doctor [--json] | quest claude install-agents [--scope project|user] [--dry-run] [--force] [--json]",
156
+ flags: [
157
+ ["doctor", "check Claude CLI, plugin install/version, and native-agent templates"],
158
+ ["install-agents", "install quest-executor and quest-reviewer as native Claude Code custom agents"],
159
+ ["--scope <s>", "project (default: .claude/agents at repo root) or user (~/.claude/agents)"],
160
+ ["--dry-run", "show intended agent writes without changing files"],
161
+ ["--force", "replace existing agent files"],
162
+ ["--json", "machine-readable output"],
163
+ ],
164
+ example: "quest claude install-agents --scope project && quest claude doctor",
165
+ },
152
166
  };
153
167
 
154
168
  export function renderCommandHelp(name) {
@@ -202,13 +216,15 @@ export function renderNoStore() {
202
216
  ].join("\n");
203
217
  }
204
218
 
205
- export function renderInitNextSteps(backend) {
219
+ export function renderInitNextSteps(backend, { agentsInstalled = true } = {}) {
206
220
  return [
207
221
  "",
208
222
  "Store created. Next steps:",
223
+ ...(agentsInstalled ? [" 0. Restart agent sessions so newly installed project templates are loaded"] : []),
209
224
  " 1. Author a quest: quest create --help (or $quest:plan in your agent session)",
210
225
  " 2. Check it: quest lint --all",
211
226
  " 3. Work it: $quest:work <id> in-session, or quest-run <id> headless",
227
+ ...(agentsInstalled ? [] : [" Agent templates skipped; install later with `quest codex install-agents --scope project` and `quest claude install-agents --scope project`."]),
212
228
  ...(backend === "github" ? ["", "Records live as GitHub issues; config and amendments stay local in .quests/."] : []),
213
229
  ].join("\n");
214
230
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quest-loop",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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" },
@@ -21,26 +21,50 @@ trails, and escalations only where a human ruling is genuinely needed.
21
21
  quest list --ready --json # the dispatch queue (deps met, priority order)
22
22
  quest runs --active # headless runners that outlived prior sessions
23
23
  ```
24
- 2. **Dispatch** each ready quest per its record:
25
- - In Codex, prefer the native `quest-executor` custom agent for an
26
- interactive single quest; prompt = "Work quest <id> per $quest:work." If
27
- the agent is missing, run `quest codex install-agents --scope project` (or
28
- `$quest:setup`) before dispatching.
24
+ For an implementation accepted from `$quest:plan` in Codex Plan Mode, stay in
25
+ this orchestrator role. Do not implement product code inline; create/lint the
26
+ quest records if needed, then dispatch workers.
27
+ 2. **Pin the wave with native goal mode:**
28
+ - In Codex, call `create_goal` with this stopping condition:
29
+ "every quest in the scoped wave shows complete, blocked, or cancelled in
30
+ `quest list --json` output"; verify it with `get_goal`.
31
+ - In Claude Code, start the turn with `/goal` using the same condition.
32
+ If native goal mode is unavailable, say so and keep the Quest checkpoint
33
+ trail as the hard stop signal; do not pretend a goal was set.
34
+ 3. **Dispatch** each ready quest per its record:
35
+ - In Codex, the default path is native subagents for both serial and
36
+ parallel waves. If `spawn_agent` is not visible, call `tool_search` once
37
+ for subagent tools before choosing any fallback. When available, spawn:
38
+ `agent_type: "quest-executor"` and prompt =
39
+ `First call create_goal with: quest <id> has a new checkpoint whose
40
+ quest_status is complete or blocked in \`quest show <id> --json\`; verify
41
+ with get_goal; work quest <id> per $quest:work; only call
42
+ update_goal(status="complete") after the checkpoint exists.`
43
+ If the agent template is missing, run `quest codex install-agents --scope
44
+ project` (or `$quest:setup`) before dispatching.
29
45
  - In Claude Code, spawn the `quest-executor` subagent with the record's
30
- `model`/`effort` as the dispatch override and the same prompt.
31
- - For headless Codex/Claude work, parallel batches, or anything long-running,
32
- run `quest-run <id>` in **background Bash** and keep working; you'll be
33
- notified when it exits. Parallel file-disjoint quests:
34
- `quest-run --ready --parallel 3` (add `--isolate worktree` when they touch
35
- the same files).
36
- 3. **Verify before you believe:** when a worker stops, run
46
+ `model`/`effort` as the dispatch override and prompt =
47
+ `/goal quest <id> has a new checkpoint whose quest_status is complete or
48
+ blocked in \`quest show <id> --json\`\nWork quest <id> per $quest:work.`
49
+ - Use `quest-run` only when native subagents are still unavailable after
50
+ `tool_search`, or when the user explicitly asks for headless/background
51
+ execution. Codex fallback must require goal mode:
52
+ `quest-run <id> --worker codex --codex-goal-mode require`. Headless
53
+ file-disjoint waves can use `quest-run --ready --parallel 3
54
+ --codex-goal-mode require` (add `--isolate worktree` when they touch the
55
+ same files). Claude headless runs already enter native `/goal` mode.
56
+ 4. **Verify before you believe:** when a worker stops, run
37
57
  `quest show <id> --json`. A stop WITHOUT a new checkpoint is a protocol
38
58
  violation — redispatch with exactly that instruction. Never accept a chat
39
59
  summary in place of a recorded checkpoint.
40
- 4. **Review before accepting complete:** for non-trivial quests, spawn
41
- `quest-reviewer` on the diff + checkpoint evidence. Every finding gets a
42
- disposition: fixed / follow-up quest filed / rejected-with-reason.
43
- 5. **Rule** (quote evidence, never adjectives):
60
+ 5. **Review before accepting complete:** for non-trivial quests, spawn
61
+ `quest-reviewer` on the diff + checkpoint evidence in the same harness. Give
62
+ it a goal: return an `accept` or `iterate` verdict with evidence. In Codex,
63
+ ask it to call `create_goal`/`get_goal` and only `update_goal` after the
64
+ verdict exists; in Claude Code, prefix the reviewer prompt with `/goal`.
65
+ Every finding gets a disposition: fixed / follow-up quest filed /
66
+ rejected-with-reason.
67
+ 6. **Rule** (quote evidence, never adjectives):
44
68
  - **accept** — the validation_summary's commands actually discharge the
45
69
  Done-when items.
46
70
  - **iterate-with-feedback** — send the specific gap back (continue the
@@ -49,7 +73,7 @@ trails, and escalations only where a human ruling is genuinely needed.
49
73
  re-parent the original honestly.
50
74
  - **escalate-to-human** — surface human-only decisions verbatim. Never
51
75
  guess a ruling the human should make.
52
- 6. **Wave done?** When `quest list --ready` empties and nothing is in flight:
76
+ 7. **Wave done?** When `quest list --ready` empties and nothing is in flight:
53
77
  run `$quest:retro` before starting the next wave.
54
78
 
55
79
  ## Closing an epic
@@ -105,7 +129,7 @@ if so, reopen the epic too. `cancelled` is fully terminal — file a new quest.
105
129
  For an unattended wave, pin your own session to the outcome with a native goal:
106
130
 
107
131
  ```
108
- every quest in this wave shows complete or blocked in `quest list --json` output
132
+ every quest in this wave shows complete, blocked, or cancelled in `quest list --json` output
109
133
  ```
110
134
 
111
135
  In Codex, use the native goal tool when available; in Claude Code, use
@@ -116,8 +140,8 @@ genuinely done.
116
140
 
117
141
  ```bash
118
142
  quest list --ready --json # → [{"id":12,"worker":"claude"…},{"id":13,"worker":"codex"…}]
119
- # 12 → dispatch quest-executor subagent (model/effort from the record)
120
- # 13 → background Bash: quest-run 13
143
+ # 12 → spawn quest-executor with a child /goal or create_goal prompt
144
+ # 13 → spawn quest-executor too; use quest-run only if native subagents are unavailable
121
145
  # …executor stops →
122
146
  quest show 12 --json # new checkpoint? quest_status? evidence?
123
147
  # reviewer on 12's diff → findings dispositioned → accept
@@ -22,6 +22,28 @@ extra context.
22
22
  | Medium | 1 quest, dispatch an executor | needs iterations, fits one Objective |
23
23
  | Large | epic parent + child quests in waves | multiple objectives; order via `depends_on` |
24
24
 
25
+ ## Plan Mode handoff
26
+
27
+ In Codex Plan Mode, do **not** implement product code after the user accepts a
28
+ plan. Your role is to make the quest records real, then ask before orchestration:
29
+
30
+ 1. Create or confirm the quest records with `quest create` and `quest lint`.
31
+ 2. If the user accepted a plan and asked to implement it, your next role is
32
+ `$quest:orchestrate`, not `$quest:work`. Do not start editing product code in
33
+ the parent session.
34
+ 3. If the user only asked to create quests, ask whether to enter
35
+ `$quest:orchestrate` and dispatch the ready quests now. Do not silently
36
+ switch modes.
37
+ 4. In `$quest:orchestrate`, set the orchestrator goal for the wave, then spawn
38
+ goal-mode workers. If the user declines orchestration, stop after listing the
39
+ ready quest ids and validation commands.
40
+
41
+ The parent session owns dispatch, checkpoint verification, reviewer rulings, and
42
+ epic closure. The spawned executor owns implementation for exactly one quest.
43
+ The parent session never uses `$quest:work <id>` after a Plan Mode handoff
44
+ unless the user explicitly asks to bypass orchestration for a genuinely small
45
+ single quest.
46
+
25
47
  For epics: create the parent first, then children with `--parent <id>` and
26
48
  `--depends-on` expressing the real order. `quest list --ready` becomes the
27
49
  dispatch queue — that is the whole wave mechanic.
@@ -51,8 +73,12 @@ Every field earns its place:
51
73
  - **Context**: files + symbols (`resolveConfig` in `lib/config.mjs`), related
52
74
  quests. NEVER bare line numbers — they rot.
53
75
  - **Out of scope**: the adjacent work you are explicitly not doing.
54
- - `--worker` / `--model` / `--effort` / `--max-iterations`: match the tier to
55
- the difficulty; the defaults come from `.quests/config.json`.
76
+ - `--worker`, `--model`, and `--effort`: always specify all three explicitly in
77
+ every `quest create` command you author. Pick them deliberately from the task's
78
+ risk, ambiguity, context size, and validation cost so the worker starts with
79
+ an intentional execution profile instead of inheriting blindly from defaults.
80
+ - `--max-iterations`: match the loop budget to the difficulty; defaults in
81
+ `.quests/config.json` are fallback behavior, not a planning substitute.
56
82
 
57
83
  **Anti-patterns** (lint catches some, you catch the rest): adjective done-whens
58
84
  ("fast", "clean"); validation loops that are prose, not commands; objectives
@@ -61,7 +87,8 @@ hiding three objectives; context by line number; budgets so big they never bind.
61
87
  ## Worked example
62
88
 
63
89
  ```bash
64
- quest create --title "Add dark mode to settings" \
90
+ quest create --worker codex --model gpt-5.5 --effort medium --max-iterations 4 \
91
+ --title "Add dark mode to settings" \
65
92
  --objective "The settings page offers a dark theme that persists across reloads." \
66
93
  --done-when "toggling theme switches the UI and survives reload" \
67
94
  --done-when "\`npm test\` passes including new theme tests" \
@@ -74,5 +101,6 @@ quest create --title "Add dark mode to settings" \
74
101
  quest lint 12 # always, before dispatch
75
102
  ```
76
103
 
77
- **Next:** dispatch with `$quest:orchestrate` (or work it yourself via
78
- `$quest:work <id>`). Rules and vocabulary: `$quest:protocol`.
104
+ **Next:** dispatch with `$quest:orchestrate`. Only work it yourself via
105
+ `$quest:work <id>` for genuinely small inline work outside Plan Mode and outside
106
+ an accepted Plan Mode handoff. Rules and vocabulary: `$quest:protocol`.
@@ -1,12 +1,31 @@
1
1
  ---
2
2
  name: setup
3
- description: Use when setting up, validating, updating, or troubleshooting Quest's native Codex plugin integration.
4
- argument-hint: "[doctor|install-agents]"
3
+ description: Use when setting up, validating, updating, or troubleshooting Quest's native Codex or Claude plugin integration.
4
+ argument-hint: "[doctor|install-agents|init]"
5
5
  ---
6
6
 
7
- # Set up Quest for Codex
7
+ # Set up Quest native agents
8
8
 
9
- Use this for Codex integration work, not for ordinary quest execution.
9
+ Use this for Codex or Claude integration work, not for ordinary quest execution.
10
+
11
+ ## Project Init
12
+
13
+ `quest init` creates `.quests/` and, by default, installs project-scoped native
14
+ agent templates for both providers:
15
+
16
+ - `.codex/agents/quest-executor.toml`
17
+ - `.codex/agents/quest-reviewer.toml`
18
+ - `.claude/agents/quest-executor.md`
19
+ - `.claude/agents/quest-reviewer.md`
20
+
21
+ Use `quest init --no-agents` when you only want the quest store. If an existing
22
+ project template conflicts, init fails before creating `.quests/`; inspect the
23
+ file, run the explicit provider install command with `--force` only when you
24
+ intend replacement, then rerun `quest init`.
25
+
26
+ Project-scoped agent templates install at the Git repository root. For a nested
27
+ quest store, use `quest init --no-agents` in the nested directory and set
28
+ `QUEST_DIR` for agents launched from elsewhere, or initialize from the repo root.
10
29
 
11
30
  ## Doctor
12
31
 
@@ -16,26 +35,43 @@ Check the installed Codex-facing state from the actual Codex surfaces:
16
35
  quest codex doctor
17
36
  ```
18
37
 
19
- This verifies the Codex CLI, installed `quest@quest` plugin version, hook parser
20
- health, neutral-directory skill roots, and whether `quest-executor` plus
21
- `quest-reviewer` are available as native Codex custom agents.
38
+ This verifies the Codex CLI, `quest` binary on PATH, `multi_agent` and `goals`
39
+ feature flags, installed `quest@quest` plugin version, hook parser health,
40
+ duplicate/stale Quest skill roots, and whether the `quest-executor` plus
41
+ `quest-reviewer` native-agent templates are installed and current. Runtime
42
+ dispatch still happens from the parent Codex session via native subagent tools;
43
+ use `quest-run --codex-goal-mode require` only as the headless fallback.
44
+
45
+ Check the installed Claude-facing state from the actual Claude surfaces:
46
+
47
+ ```bash
48
+ quest claude doctor
49
+ ```
22
50
 
23
51
  ## Install Native Agents
24
52
 
25
- Install the custom agents for this repository:
53
+ Install the Codex custom agents for this repository:
26
54
 
27
55
  ```bash
28
56
  quest codex install-agents --scope project
29
57
  ```
30
58
 
59
+ Install the Claude custom agents for this repository:
60
+
61
+ ```bash
62
+ quest claude install-agents --scope project
63
+ ```
64
+
31
65
  Use user scope only when you want the agents available everywhere:
32
66
 
33
67
  ```bash
34
68
  quest codex install-agents --scope user
69
+ quest claude install-agents --scope user
35
70
  ```
36
71
 
37
72
  If an existing file conflicts, inspect it first. Use `--force` only when you
38
- intend to replace that custom agent with Quest's bundled definition.
73
+ intend to replace that custom agent with Quest's bundled definition. Symlinked
74
+ agent directories or files are refused rather than overwritten.
39
75
 
40
76
  ## Update Installed Plugin
41
77