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 +47 -0
- package/README.md +56 -6
- package/agents/quest-executor.md +10 -0
- package/agents/quest-executor.toml +13 -6
- package/agents/quest-reviewer.md +9 -0
- package/agents/quest-reviewer.toml +11 -7
- package/lib/cli.mjs +99 -57
- package/lib/codex-native.mjs +227 -48
- package/lib/help.mjs +20 -4
- package/package.json +1 -1
- package/skills/orchestrate/SKILL.md +44 -20
- package/skills/plan/SKILL.md +33 -5
- package/skills/setup/SKILL.md +45 -9
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,
|
|
85
|
-
|
|
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
|
-
|
|
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/`)
|
|
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
|
|
package/agents/quest-executor.md
CHANGED
|
@@ -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: `
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
`quest
|
|
13
|
-
|
|
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
|
"""
|
package/agents/quest-reviewer.md
CHANGED
|
@@ -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:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
};
|
package/lib/codex-native.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
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
|
|
49
|
-
|
|
50
|
-
if (
|
|
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),
|
|
57
|
-
|
|
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 =
|
|
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.
|
|
92
|
-
return
|
|
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
|
-
|
|
116
|
-
const
|
|
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}.
|
|
122
|
-
const dest = join(dir, `${name}.
|
|
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 (
|
|
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 =
|
|
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
|
|
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
|
|
214
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
"
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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/)
|
|
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
|
|
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.
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
`quest-reviewer` on the diff + checkpoint evidence
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
|
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 →
|
|
120
|
-
# 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
|
package/skills/plan/SKILL.md
CHANGED
|
@@ -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
|
|
55
|
-
|
|
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 --
|
|
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
|
|
78
|
-
`$quest:work <id>`
|
|
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`.
|
package/skills/setup/SKILL.md
CHANGED
|
@@ -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
|
|
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,
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|