quest-loop 0.3.3 → 0.3.5
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 +24 -0
- package/README.md +30 -14
- package/hooks/hooks.json +27 -0
- package/hooks/plan-exit-reminder.mjs +71 -0
- package/lib/cli.mjs +79 -14
- package/lib/codex-native.mjs +137 -14
- package/lib/help.mjs +17 -11
- package/package.json +1 -1
- package/skills/setup/SKILL.md +27 -10
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,30 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## [0.3.5] — 2026-07-08
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `quest codex doctor --fix` and `quest claude doctor --fix` now repair
|
|
13
|
+
Quest-owned native-agent templates and provider plugin install/version state,
|
|
14
|
+
then rerun the same doctor checks.
|
|
15
|
+
- `quest codex open` and `quest claude open` now run the provider health gate
|
|
16
|
+
before launching the interactive provider from the project root.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Stale Quest agent templates are replaced without `--force`; unrelated custom
|
|
20
|
+
files still require explicit replacement.
|
|
21
|
+
|
|
22
|
+
## [0.3.4] — 2026-07-08
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- `quest codex install-agents` and `quest claude install-agents` now allow
|
|
26
|
+
`--force` to write through symlinked agent template files, while continuing to
|
|
27
|
+
refuse symlinked provider agent directories.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- Added a Quest plan-exit reminder hook that nudges accepted `$quest:plan`
|
|
31
|
+
handoffs toward `$quest:orchestrate` rather than direct implementation.
|
|
32
|
+
|
|
9
33
|
## [0.3.3] — 2026-07-08
|
|
10
34
|
|
|
11
35
|
### Changed
|
package/README.md
CHANGED
|
@@ -57,8 +57,8 @@ Install Quest's project-scoped Claude agent templates and verify the local
|
|
|
57
57
|
Claude setup with:
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
|
-
quest claude
|
|
61
|
-
quest claude
|
|
60
|
+
quest claude doctor --fix
|
|
61
|
+
quest claude open
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
For local development against a checkout, point Claude Code at the repo directly
|
|
@@ -84,16 +84,19 @@ Verify the install with:
|
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
86
|
codex plugin list --marketplace quest
|
|
87
|
-
quest codex
|
|
88
|
-
quest codex
|
|
87
|
+
quest codex doctor --fix
|
|
88
|
+
quest codex open
|
|
89
89
|
codex debug prompt-input "noop"
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
`quest codex doctor`
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
`quest codex doctor --fix` and `quest claude doctor --fix` install or refresh
|
|
93
|
+
Quest-owned native-agent templates and repair the provider plugin install/version
|
|
94
|
+
when the provider CLI supports it, then rerun doctor. `quest codex open` and
|
|
95
|
+
`quest claude open` run the same health gate before launching the interactive
|
|
96
|
+
provider. `codex debug prompt-input "noop"` should not print any hook parse
|
|
97
|
+
warnings.
|
|
95
98
|
|
|
96
|
-
#### Updating
|
|
99
|
+
#### Updating provider plugins
|
|
97
100
|
|
|
98
101
|
Codex installs plugins from a marketplace snapshot. Pulling this Git repo or
|
|
99
102
|
publishing a new tag is not enough to update the already-installed plugin cache.
|
|
@@ -103,10 +106,18 @@ Refresh the marketplace snapshot, then reinstall the plugin from it:
|
|
|
103
106
|
codex plugin marketplace upgrade quest
|
|
104
107
|
codex plugin add quest@quest
|
|
105
108
|
codex plugin list --marketplace quest
|
|
106
|
-
quest codex doctor
|
|
109
|
+
quest codex doctor --fix
|
|
107
110
|
```
|
|
108
111
|
|
|
109
|
-
|
|
112
|
+
Claude updates through its plugin manager:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
claude plugin update quest@quest
|
|
116
|
+
quest claude doctor --fix
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Then start a new provider session. If the Codex update contains hook changes,
|
|
120
|
+
re-run:
|
|
110
121
|
|
|
111
122
|
```bash
|
|
112
123
|
codex debug prompt-input "noop"
|
|
@@ -145,12 +156,17 @@ both providers:
|
|
|
145
156
|
- `.claude/agents/quest-executor.md`
|
|
146
157
|
- `.claude/agents/quest-reviewer.md`
|
|
147
158
|
|
|
148
|
-
Use `quest init --no-agents` when you only want the `.quests/` store.
|
|
149
|
-
|
|
159
|
+
Use `quest init --no-agents` when you only want the `.quests/` store. Stale
|
|
160
|
+
Quest-owned agent templates are replaced automatically. If an existing file is
|
|
161
|
+
not recognizable as a Quest agent template, init fails before creating
|
|
150
162
|
`.quests/`; inspect the conflicting files, run the explicit provider install
|
|
151
163
|
command with `--force` only if you intend to replace them, then rerun
|
|
152
164
|
`quest init`.
|
|
153
165
|
|
|
166
|
+
Agent template files may be symlinks; when you use `--force`, the install writes
|
|
167
|
+
through those file symlinks. Keep `.codex/agents` and `.claude/agents` as real
|
|
168
|
+
directories rather than symlinking the whole directory.
|
|
169
|
+
|
|
154
170
|
Project-scoped agent templates install at the Git repository root. For a nested
|
|
155
171
|
quest store, run `quest init --no-agents` in the nested directory and set
|
|
156
172
|
`QUEST_DIR` for agents launched from elsewhere, or initialize from the repo root.
|
|
@@ -234,8 +250,8 @@ full base protocol lives in
|
|
|
234
250
|
| `quest amend` | Append a numbered protocol amendment (retro output) |
|
|
235
251
|
| `quest protocol` | Print the loop protocol + this store's local amendments |
|
|
236
252
|
| `quest runs` | Show headless runner activity (from `.quests/runs.ndjson`) |
|
|
237
|
-
| `quest codex` | Validate
|
|
238
|
-
| `quest claude` | Validate
|
|
253
|
+
| `quest codex` | Validate, repair, install, or open Codex-native setup |
|
|
254
|
+
| `quest claude` | Validate, repair, install, or open Claude-native setup |
|
|
239
255
|
|
|
240
256
|
## quest-run (headless runner)
|
|
241
257
|
|
package/hooks/hooks.json
CHANGED
|
@@ -62,6 +62,33 @@
|
|
|
62
62
|
}
|
|
63
63
|
]
|
|
64
64
|
}
|
|
65
|
+
],
|
|
66
|
+
"PostToolUse": [
|
|
67
|
+
{
|
|
68
|
+
"matcher": "ExitPlanMode",
|
|
69
|
+
"hooks": [
|
|
70
|
+
{
|
|
71
|
+
"type": "command",
|
|
72
|
+
"command": "root=\"${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}\"; [ -n \"$root\" ] && node \"$root/hooks/plan-exit-reminder.mjs\"",
|
|
73
|
+
"commandWindows": "if defined CODEX_PLUGIN_ROOT (node \"%CODEX_PLUGIN_ROOT%\\hooks\\plan-exit-reminder.mjs\") else if defined CLAUDE_PLUGIN_ROOT (node \"%CLAUDE_PLUGIN_ROOT%\\hooks\\plan-exit-reminder.mjs\")",
|
|
74
|
+
"timeout": 10,
|
|
75
|
+
"statusMessage": "Preparing quest orchestration reminder"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"Stop": [
|
|
81
|
+
{
|
|
82
|
+
"hooks": [
|
|
83
|
+
{
|
|
84
|
+
"type": "command",
|
|
85
|
+
"command": "root=\"${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}\"; [ -n \"$root\" ] && node \"$root/hooks/plan-exit-reminder.mjs\"",
|
|
86
|
+
"commandWindows": "if defined CODEX_PLUGIN_ROOT (node \"%CODEX_PLUGIN_ROOT%\\hooks\\plan-exit-reminder.mjs\") else if defined CLAUDE_PLUGIN_ROOT (node \"%CLAUDE_PLUGIN_ROOT%\\hooks\\plan-exit-reminder.mjs\")",
|
|
87
|
+
"timeout": 10,
|
|
88
|
+
"statusMessage": "Checking quest plan handoff"
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|
|
65
92
|
]
|
|
66
93
|
}
|
|
67
94
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Plan-exit reminder hook. When a Quest-generated plan handoff is accepted,
|
|
3
|
+
// remind the parent session that the next run is orchestration, not direct
|
|
4
|
+
// implementation. Anything uncertain is a silent no-op.
|
|
5
|
+
|
|
6
|
+
import { writeSync } from "node:fs";
|
|
7
|
+
|
|
8
|
+
const REMINDER = [
|
|
9
|
+
"Quest plan handoff accepted.",
|
|
10
|
+
"This run is about orchestration, not direct implementation:",
|
|
11
|
+
"create or confirm the quest records, run `quest lint`, then switch to `$quest:orchestrate` to dispatch workers, verify checkpoints, and close the wave.",
|
|
12
|
+
].join(" ");
|
|
13
|
+
|
|
14
|
+
async function readStdin() {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
17
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function hookEventName(payload) {
|
|
21
|
+
return payload?.hook_event_name ?? payload?.hookEventName ?? "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toolName(payload) {
|
|
25
|
+
return payload?.tool_name ?? payload?.toolName ?? payload?.tool?.name ?? "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function lastAssistantMessage(payload) {
|
|
29
|
+
const value = payload?.last_assistant_message ?? payload?.lastAssistantMessage;
|
|
30
|
+
if (typeof value === "string") return value;
|
|
31
|
+
if (value && typeof value === "object") return JSON.stringify(value);
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function questPlanHandoff(text) {
|
|
36
|
+
if (!text) return false;
|
|
37
|
+
const hasQuestPlan = /(?:\$?quest:plan\b|\bquest create\b|\bquest lint\b|<proposed_plan>)/i.test(text);
|
|
38
|
+
const hasOrchestrate = /(?:\$?quest:orchestrate\b|\borchestration\b|\borchestrator\b)/i.test(text);
|
|
39
|
+
return hasQuestPlan && hasOrchestrate;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function postToolUseText(payload) {
|
|
43
|
+
const parts = [];
|
|
44
|
+
for (const key of ["tool_input", "toolInput", "tool_response", "toolResponse"]) {
|
|
45
|
+
const value = payload?.[key];
|
|
46
|
+
if (typeof value === "string") parts.push(value);
|
|
47
|
+
else if (value && typeof value === "object") parts.push(JSON.stringify(value));
|
|
48
|
+
}
|
|
49
|
+
return parts.join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function shouldRemind(payload) {
|
|
53
|
+
const event = hookEventName(payload);
|
|
54
|
+
if (event === "PostToolUse" && toolName(payload) === "ExitPlanMode") {
|
|
55
|
+
return questPlanHandoff(postToolUseText(payload));
|
|
56
|
+
}
|
|
57
|
+
if (event === "Stop") {
|
|
58
|
+
return questPlanHandoff(lastAssistantMessage(payload));
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const raw = await readStdin();
|
|
65
|
+
let payload = {};
|
|
66
|
+
try { payload = raw.trim() ? JSON.parse(raw) : {}; } catch { payload = {}; }
|
|
67
|
+
if (shouldRemind(payload)) writeSync(1, JSON.stringify({ systemMessage: REMINDER }) + "\n");
|
|
68
|
+
process.exit(0);
|
|
69
|
+
} catch {
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
package/lib/cli.mjs
CHANGED
|
@@ -5,13 +5,14 @@ import { parseArgs } from "node:util";
|
|
|
5
5
|
import { readFileSync, existsSync, writeFileSync, appendFileSync } from "node:fs";
|
|
6
6
|
import { join, dirname } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
8
9
|
import { ConfigError, findStoreDir, loadConfig } from "./config.mjs";
|
|
9
10
|
import { ContractError, lintRecord } from "./contract.mjs";
|
|
10
11
|
import * as local from "./store-local.mjs";
|
|
11
12
|
import * as github from "./store-github.mjs";
|
|
12
13
|
import { openStore } from "./store.mjs";
|
|
13
14
|
import { COMMANDS, renderCommandHelp, renderGeneralHelp, renderInitNextSteps, renderNoStore, renderStatusOverview } from "./help.mjs";
|
|
14
|
-
import { claudeDoctor, doctor as codexDoctor, installAgents as installCodexAgents, installClaudeAgents, versionInfo } from "./codex-native.mjs";
|
|
15
|
+
import { claudeDoctor, claudeDoctorFix, codexDoctorFix, doctor as codexDoctor, installAgents as installCodexAgents, installClaudeAgents, nativeProjectRoot, versionInfo } from "./codex-native.mjs";
|
|
15
16
|
|
|
16
17
|
const PLUGIN_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
18
|
|
|
@@ -92,28 +93,87 @@ function preflightInitAgentInstall(cwd, env) {
|
|
|
92
93
|
return plans;
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
function printDoctorResult(command, result, out) {
|
|
97
|
+
if (result.repairs?.length) {
|
|
98
|
+
out(`${command} doctor --fix: repairs`);
|
|
99
|
+
for (const r of result.repairs) out(` ${r.ok ? "OK " : "ERR"} ${r.name}: ${r.command} (${r.detail})`);
|
|
100
|
+
}
|
|
101
|
+
for (const c of result.checks) out(`${c.ok ? "OK " : "ERR"} ${c.name}: ${c.detail}`);
|
|
102
|
+
out(result.ok ? `${command} doctor: OK` : `${command} doctor: problems found`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseOpenArgs(args) {
|
|
106
|
+
const forwarded = [];
|
|
107
|
+
let dryRun = false;
|
|
108
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
109
|
+
const arg = args[i];
|
|
110
|
+
if (arg === "--") {
|
|
111
|
+
forwarded.push(...args.slice(i + 1));
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
if (arg === "--dry-run") {
|
|
115
|
+
dryRun = true;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (arg === "--help" || arg === "-h") return { help: true };
|
|
119
|
+
forwarded.push(arg);
|
|
120
|
+
}
|
|
121
|
+
return { dryRun, forwarded };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function shellQuote(arg) {
|
|
125
|
+
return /^[A-Za-z0-9_./:=@+-]+$/.test(arg) ? arg : `'${String(arg).replaceAll("'", "'\\''")}'`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function openInvocation(command, cwd, env, forwarded) {
|
|
129
|
+
const root = nativeProjectRoot(cwd, env);
|
|
130
|
+
if (command === "codex") return { cmd: "codex", args: ["-C", root, ...forwarded], cwd };
|
|
131
|
+
if (command === "claude") return { cmd: "claude", args: forwarded, cwd: root };
|
|
132
|
+
throw new Error(`unknown provider "${command}"`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function runNativeSetupCommand(command, args, { out, cwd, env }, { label, doctor, doctorFix, install }) {
|
|
96
136
|
const [subcommand, ...rest] = args;
|
|
97
137
|
if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
98
138
|
out(renderCommandHelp(command));
|
|
99
139
|
return 0;
|
|
100
140
|
}
|
|
101
141
|
if (subcommand === "doctor") {
|
|
102
|
-
const p = parse(command, rest, {}, { positionals: 0 });
|
|
142
|
+
const p = parse(command, rest, { fix: { type: "boolean" } }, { positionals: 0 });
|
|
103
143
|
if (p.help) {
|
|
104
144
|
out(renderCommandHelp(command));
|
|
105
145
|
return 0;
|
|
106
146
|
}
|
|
107
|
-
const result = doctor({ cwd, env });
|
|
147
|
+
const result = p.values.fix ? doctorFix({ cwd, env }) : doctor({ cwd, env });
|
|
108
148
|
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
|
-
}
|
|
149
|
+
else printDoctorResult(command, result, out);
|
|
113
150
|
// 1 (generic diagnostic failure), not 5 — exit 5 is reserved for
|
|
114
151
|
// ContractError (a quest contract violation), which a doctor finding is not.
|
|
115
152
|
return result.ok ? 0 : 1;
|
|
116
153
|
}
|
|
154
|
+
if (subcommand === "open") {
|
|
155
|
+
const p = parseOpenArgs(rest);
|
|
156
|
+
if (p.help) {
|
|
157
|
+
out(renderCommandHelp(command));
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
const result = doctorFix({ cwd, env });
|
|
161
|
+
if (!result.ok) {
|
|
162
|
+
printDoctorResult(command, result, out);
|
|
163
|
+
out(`${command} open: not launching because doctor still reports problems`);
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
const invocation = openInvocation(command, cwd, env, p.forwarded);
|
|
167
|
+
const rendered = [invocation.cmd, ...invocation.args].map(shellQuote).join(" ");
|
|
168
|
+
if (p.dryRun) {
|
|
169
|
+
out(`${command} open: health OK`);
|
|
170
|
+
out(`Would run: ${rendered}`);
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
out(`${command} open: health OK; launching ${rendered}`);
|
|
174
|
+
const res = spawnSync(invocation.cmd, invocation.args, { cwd: invocation.cwd, env, stdio: "inherit" });
|
|
175
|
+
return typeof res.status === "number" ? res.status : 1;
|
|
176
|
+
}
|
|
117
177
|
if (subcommand === "install-agents") {
|
|
118
178
|
const p = parse(command, rest, {
|
|
119
179
|
scope: { type: "string", default: "project" },
|
|
@@ -136,9 +196,9 @@ async function runNativeSetupCommand(command, args, { out, cwd, env }, { label,
|
|
|
136
196
|
// A real run refuses to overwrite; a --dry-run PREVIEWS the conflict
|
|
137
197
|
// (its whole purpose) rather than erroring before showing the plan.
|
|
138
198
|
if (!result.ok && !isDry) {
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
141
|
-
throw new UsageError(`refusing to write agent templates through symlinked
|
|
199
|
+
const symlinkDirConflicts = result.conflicts.filter((c) => c.reason === "symlink-directory");
|
|
200
|
+
if (symlinkDirConflicts.length) {
|
|
201
|
+
throw new UsageError(`refusing to write agent templates through symlinked directory: ${symlinkDirConflicts.map((c) => c.path).join(", ")}`, { command });
|
|
142
202
|
}
|
|
143
203
|
const conflicts = result.conflicts.map((c) => c.path).join(", ");
|
|
144
204
|
throw new UsageError(`agent file already exists; pass --force to replace: ${conflicts}`, { command });
|
|
@@ -147,7 +207,12 @@ async function runNativeSetupCommand(command, args, { out, cwd, env }, { label,
|
|
|
147
207
|
else {
|
|
148
208
|
out(`${isDry ? "Would install" : "Installed"} ${label} agents to ${result.target}`);
|
|
149
209
|
for (const a of result.actions) out(` ${a.action.padEnd(9)} ${a.path}`);
|
|
150
|
-
if (!result.ok)
|
|
210
|
+
if (!result.ok) {
|
|
211
|
+
const symlinkDirConflicts = result.conflicts.filter((c) => c.reason === "symlink-directory");
|
|
212
|
+
if (symlinkDirConflicts.length) out(` symlinked directories must be real directories: ${symlinkDirConflicts.map((c) => c.path).join(", ")}`);
|
|
213
|
+
const fileConflicts = result.conflicts.filter((c) => c.reason !== "symlink-directory");
|
|
214
|
+
if (fileConflicts.length) out(` conflicts present — pass --force to replace: ${fileConflicts.map((c) => c.path).join(", ")}`);
|
|
215
|
+
}
|
|
151
216
|
}
|
|
152
217
|
return 0;
|
|
153
218
|
}
|
|
@@ -531,10 +596,10 @@ const HANDLERS = {
|
|
|
531
596
|
},
|
|
532
597
|
|
|
533
598
|
async codex(args, { out, cwd, env }) {
|
|
534
|
-
return runNativeSetupCommand("codex", args, { out, cwd, env }, { label: "Codex", doctor: codexDoctor, install: installCodexAgents });
|
|
599
|
+
return runNativeSetupCommand("codex", args, { out, cwd, env }, { label: "Codex", doctor: codexDoctor, doctorFix: codexDoctorFix, install: installCodexAgents });
|
|
535
600
|
},
|
|
536
601
|
|
|
537
602
|
async claude(args, { out, cwd, env }) {
|
|
538
|
-
return runNativeSetupCommand("claude", args, { out, cwd, env }, { label: "Claude", doctor: claudeDoctor, install: installClaudeAgents });
|
|
603
|
+
return runNativeSetupCommand("claude", args, { out, cwd, env }, { label: "Claude", doctor: claudeDoctor, doctorFix: claudeDoctorFix, install: installClaudeAgents });
|
|
539
604
|
},
|
|
540
605
|
};
|
package/lib/codex-native.mjs
CHANGED
|
@@ -9,6 +9,7 @@ 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 QUEST_CODEX_MARKETPLACE_SOURCE = ["roberts", "reberski/quest"].join("");
|
|
12
13
|
const PROVIDERS = {
|
|
13
14
|
codex: {
|
|
14
15
|
label: "Codex",
|
|
@@ -82,7 +83,6 @@ function installedAgentDirs(provider, cwd, env) {
|
|
|
82
83
|
|
|
83
84
|
function sameFile(path, text) {
|
|
84
85
|
try {
|
|
85
|
-
if (lstatSync(path).isSymbolicLink()) return false;
|
|
86
86
|
return readFileSync(path, "utf8") === text;
|
|
87
87
|
} catch {
|
|
88
88
|
return false;
|
|
@@ -161,13 +161,25 @@ export function versionInfo() {
|
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
function
|
|
164
|
+
function existingAgentMatchesName(provider, name, path) {
|
|
165
|
+
let text;
|
|
166
|
+
try {
|
|
167
|
+
text = readFileSync(path, "utf8");
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (provider === "codex") return new RegExp(`^\\s*name\\s*=\\s*"${name}"\\s*$`, "m").test(text);
|
|
172
|
+
if (provider === "claude") return new RegExp(`^\\s*name:\\s*${name}\\s*$`, "m").test(text);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function installProviderAgents(provider, { scope = "project", dryRun = false, force = false, replaceStale = true, cwd = process.cwd(), env = process.env } = {}) {
|
|
165
177
|
const cfg = providerConfig(provider);
|
|
166
178
|
const dir = targetDir(provider, scope, cwd, env);
|
|
167
179
|
const dirConflicts = [dirname(dir), dir]
|
|
168
180
|
.filter((path, index, all) => all.indexOf(path) === index)
|
|
169
|
-
.filter((path) =>
|
|
170
|
-
.map((path) => ({ name: null, path, reason: "symlink" }));
|
|
181
|
+
.filter((path) => isSymlink(path))
|
|
182
|
+
.map((path) => ({ name: null, path, reason: "symlink-directory" }));
|
|
171
183
|
|
|
172
184
|
// Plan every agent first; only touch disk once the whole set is known to be
|
|
173
185
|
// conflict-free. A mixed create+conflict run must not leave a partial install.
|
|
@@ -175,29 +187,26 @@ function installProviderAgents(provider, { scope = "project", dryRun = false, fo
|
|
|
175
187
|
const src = join(PLUGIN_ROOT, "agents", `${name}.${cfg.extension}`);
|
|
176
188
|
const dest = join(dir, `${name}.${cfg.extension}`);
|
|
177
189
|
const text = readFileSync(src, "utf8");
|
|
178
|
-
const exists = existsSync(dest);
|
|
179
|
-
const
|
|
190
|
+
const exists = existsSync(dest) || isSymlink(dest);
|
|
191
|
+
const staleQuestAgent = exists && !sameFile(dest, text) && existingAgentMatchesName(provider, name, dest);
|
|
180
192
|
let action;
|
|
181
|
-
if (
|
|
182
|
-
else if (exists &&
|
|
183
|
-
else if (exists && !force) action = "conflict";
|
|
193
|
+
if (exists && sameFile(dest, text)) action = "unchanged";
|
|
194
|
+
else if (exists && !force && !(replaceStale && staleQuestAgent)) action = "conflict";
|
|
184
195
|
else action = exists ? "replace" : "create";
|
|
185
|
-
return { name, path: dest, action, text
|
|
196
|
+
return { name, path: dest, action, text };
|
|
186
197
|
});
|
|
187
198
|
|
|
188
199
|
const conflicts = [
|
|
189
200
|
...dirConflicts,
|
|
190
|
-
...plans.filter((p) => p.action === "conflict").map(({ name, path
|
|
201
|
+
...plans.filter((p) => p.action === "conflict").map(({ name, path }) => ({ name, path })),
|
|
191
202
|
];
|
|
192
203
|
const actions = plans.map(({ name, path, action }) => ({ name, path, action }));
|
|
193
204
|
const ok = conflicts.length === 0;
|
|
194
205
|
|
|
195
206
|
if (ok && !dryRun) {
|
|
196
|
-
if (dirConflicts.length) throw new Error(`refusing to write agent templates through symlinked path: ${dirConflicts.map((c) => c.path).join(", ")}`);
|
|
197
207
|
for (const p of plans) {
|
|
198
208
|
if (p.action === "unchanged") continue;
|
|
199
209
|
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}`);
|
|
201
210
|
writeFileSync(p.path, p.text);
|
|
202
211
|
}
|
|
203
212
|
}
|
|
@@ -213,6 +222,10 @@ export function installClaudeAgents(options = {}) {
|
|
|
213
222
|
return installProviderAgents("claude", options);
|
|
214
223
|
}
|
|
215
224
|
|
|
225
|
+
export function nativeProjectRoot(cwd = process.cwd(), env = process.env) {
|
|
226
|
+
return projectRoot(cwd, env);
|
|
227
|
+
}
|
|
228
|
+
|
|
216
229
|
function questCliPathCheck(cwd, env, versions) {
|
|
217
230
|
const pathQuestVersion = runCmd("quest", ["--version"], { cwd, env });
|
|
218
231
|
const pathVersion = pathQuestVersion.status === 0 ? pathQuestVersion.stdout.trim() : null;
|
|
@@ -248,7 +261,7 @@ function nativeAgentsCheck(provider, cwd, env) {
|
|
|
248
261
|
}
|
|
249
262
|
const agentDetail = [
|
|
250
263
|
missing.length ? `missing: ${missing.join(", ")}` : null,
|
|
251
|
-
stale.length ? `stale (run install-agents
|
|
264
|
+
stale.length ? `stale (run doctor --fix or install-agents): ${stale.join(", ")}` : null,
|
|
252
265
|
].filter(Boolean).join("; ") || `installed ${cfg.label} templates current: ${Object.values(found).join(", ")}`;
|
|
253
266
|
return check(
|
|
254
267
|
"native-agents",
|
|
@@ -423,3 +436,113 @@ export function claudeDoctor({ cwd = process.cwd(), env = process.env } = {}) {
|
|
|
423
436
|
|
|
424
437
|
return { ok: checks.every((c) => c.ok), checks };
|
|
425
438
|
}
|
|
439
|
+
|
|
440
|
+
function checkByName(result, name) {
|
|
441
|
+
return result.checks.find((c) => c.name === name);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function cmdDetail(res) {
|
|
445
|
+
const text = [res.stdout, res.stderr, res.error?.message].filter(Boolean).join("\n").trim();
|
|
446
|
+
return text || `exit ${res.status ?? "unknown"}`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function repairRecord(name, command, res, extra = {}) {
|
|
450
|
+
return {
|
|
451
|
+
name,
|
|
452
|
+
command: command.join(" "),
|
|
453
|
+
ok: res.status === 0,
|
|
454
|
+
detail: cmdDetail(res),
|
|
455
|
+
...extra,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function runRepairCommand(cmd, args, { cwd, env }) {
|
|
460
|
+
return runCmd(cmd, args, { cwd, env });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function codexMarketplacePresent(cwd, env) {
|
|
464
|
+
const res = runCmd("codex", ["plugin", "marketplace", "list", "--json"], { cwd, env });
|
|
465
|
+
if (res.status !== 0) return { ok: false, present: false, res };
|
|
466
|
+
try {
|
|
467
|
+
const parsed = JSON.parse(res.stdout || "{}");
|
|
468
|
+
const marketplaces = Array.isArray(parsed.marketplaces) ? parsed.marketplaces : [];
|
|
469
|
+
return { ok: true, present: marketplaces.some((m) => m.name === "quest" || m.root?.includes("/quest")) };
|
|
470
|
+
} catch {
|
|
471
|
+
return { ok: false, present: false, res };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function providerPluginNeedsRepair(provider, result) {
|
|
476
|
+
const names = provider === "codex"
|
|
477
|
+
? ["plugin-installed", "plugin-version", "hook-parser", "single-neutral-skill-root"]
|
|
478
|
+
: ["plugin-installed", "plugin-version"];
|
|
479
|
+
return names.some((name) => checkByName(result, name)?.ok === false);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function repairProviderPlugin(provider, before, { cwd, env }) {
|
|
483
|
+
const cliName = provider === "codex" ? "codex-cli" : "claude-cli";
|
|
484
|
+
if (!checkByName(before, cliName)?.ok || !providerPluginNeedsRepair(provider, before)) return [];
|
|
485
|
+
|
|
486
|
+
const repairs = [];
|
|
487
|
+
if (provider === "codex") {
|
|
488
|
+
const marketplace = codexMarketplacePresent(cwd, env);
|
|
489
|
+
if (!marketplace.ok) {
|
|
490
|
+
repairs.push(repairRecord("plugin-marketplace-list", ["codex", "plugin", "marketplace", "list", "--json"], marketplace.res));
|
|
491
|
+
return repairs;
|
|
492
|
+
}
|
|
493
|
+
if (!marketplace.present) {
|
|
494
|
+
const addMarketplaceArgs = ["plugin", "marketplace", "add", QUEST_CODEX_MARKETPLACE_SOURCE];
|
|
495
|
+
const addMarketplace = runRepairCommand("codex", addMarketplaceArgs, { cwd, env });
|
|
496
|
+
repairs.push(repairRecord("plugin-marketplace-add", ["codex", ...addMarketplaceArgs], addMarketplace));
|
|
497
|
+
if (addMarketplace.status !== 0) return repairs;
|
|
498
|
+
}
|
|
499
|
+
const upgrade = runRepairCommand("codex", ["plugin", "marketplace", "upgrade", "quest"], { cwd, env });
|
|
500
|
+
repairs.push(repairRecord("plugin-marketplace-upgrade", ["codex", "plugin", "marketplace", "upgrade", "quest"], upgrade));
|
|
501
|
+
if (upgrade.status !== 0) return repairs;
|
|
502
|
+
const addPlugin = runRepairCommand("codex", ["plugin", "add", "quest@quest"], { cwd, env });
|
|
503
|
+
repairs.push(repairRecord("plugin-add", ["codex", "plugin", "add", "quest@quest"], addPlugin));
|
|
504
|
+
return repairs;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const installed = checkByName(before, "plugin-installed")?.installed;
|
|
508
|
+
const subcommand = installed ? "update" : "install";
|
|
509
|
+
const res = runRepairCommand("claude", ["plugin", subcommand, "quest@quest"], { cwd, env });
|
|
510
|
+
repairs.push(repairRecord(`plugin-${subcommand}`, ["claude", "plugin", subcommand, "quest@quest"], res));
|
|
511
|
+
return repairs;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function repairProviderAgents(provider, before, { cwd, env }) {
|
|
515
|
+
if (checkByName(before, "native-agents")?.ok !== false) return [];
|
|
516
|
+
const result = installProviderAgents(provider, { scope: "project", force: false, replaceStale: true, cwd, env });
|
|
517
|
+
return [{
|
|
518
|
+
name: "native-agents",
|
|
519
|
+
command: `quest ${provider} install-agents --scope project`,
|
|
520
|
+
ok: result.ok,
|
|
521
|
+
detail: result.ok ? `${result.actions.map((a) => `${a.action} ${a.path}`).join("; ")}` : `conflicts: ${result.conflicts.map((c) => c.path).join(", ")}`,
|
|
522
|
+
result,
|
|
523
|
+
}];
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function doctorForProvider(provider, options) {
|
|
527
|
+
if (provider === "codex") return doctor(options);
|
|
528
|
+
if (provider === "claude") return claudeDoctor(options);
|
|
529
|
+
throw new Error(`unknown provider "${provider}"`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function doctorWithFix(provider, { cwd = process.cwd(), env = process.env } = {}) {
|
|
533
|
+
const before = doctorForProvider(provider, { cwd, env });
|
|
534
|
+
const repairs = [
|
|
535
|
+
...repairProviderAgents(provider, before, { cwd, env }),
|
|
536
|
+
...repairProviderPlugin(provider, before, { cwd, env }),
|
|
537
|
+
];
|
|
538
|
+
const after = doctorForProvider(provider, { cwd, env });
|
|
539
|
+
return { ok: after.ok, checks: after.checks, repairs, before };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function codexDoctorFix(options = {}) {
|
|
543
|
+
return doctorWithFix("codex", options);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export function claudeDoctorFix(options = {}) {
|
|
547
|
+
return doctorWithFix("claude", options);
|
|
548
|
+
}
|
package/lib/help.mjs
CHANGED
|
@@ -138,30 +138,34 @@ export const COMMANDS = {
|
|
|
138
138
|
example: "quest runs --active",
|
|
139
139
|
},
|
|
140
140
|
codex: {
|
|
141
|
-
purpose: "Inspect
|
|
142
|
-
usage: "quest codex doctor [--json] | quest codex install-agents [--scope project|user] [--dry-run] [--force] [--json]",
|
|
141
|
+
purpose: "Inspect, repair, install, or open Quest's native Codex integration",
|
|
142
|
+
usage: "quest codex doctor [--fix] [--json] | quest codex open [--dry-run] [-- <codex args>] | quest codex install-agents [--scope project|user] [--dry-run] [--force] [--json]",
|
|
143
143
|
flags: [
|
|
144
144
|
["doctor", "check Codex CLI, multi-agent support, plugin install/version, hooks, skill roots, and native-agent templates"],
|
|
145
|
+
["--fix", "repair missing/stale Quest agents and Quest plugin install/version when possible, then rerun doctor"],
|
|
146
|
+
["open", "repair/check setup, then launch interactive Codex from the project root"],
|
|
145
147
|
["install-agents", "install quest-executor and quest-reviewer as native Codex custom agents"],
|
|
146
148
|
["--scope <s>", "project (default: .codex/agents at repo root) or user (~/.codex/agents)"],
|
|
147
149
|
["--dry-run", "show intended agent writes without changing files"],
|
|
148
|
-
["--force", "replace existing agent files"],
|
|
150
|
+
["--force", "replace existing non-Quest agent files"],
|
|
149
151
|
["--json", "machine-readable output"],
|
|
150
152
|
],
|
|
151
|
-
example: "quest codex
|
|
153
|
+
example: "quest codex doctor --fix && quest codex open",
|
|
152
154
|
},
|
|
153
155
|
claude: {
|
|
154
|
-
purpose: "Inspect
|
|
155
|
-
usage: "quest claude doctor [--json] | quest claude install-agents [--scope project|user] [--dry-run] [--force] [--json]",
|
|
156
|
+
purpose: "Inspect, repair, install, or open Quest's native Claude Code integration",
|
|
157
|
+
usage: "quest claude doctor [--fix] [--json] | quest claude open [--dry-run] [-- <claude args>] | quest claude install-agents [--scope project|user] [--dry-run] [--force] [--json]",
|
|
156
158
|
flags: [
|
|
157
159
|
["doctor", "check Claude CLI, plugin install/version, and native-agent templates"],
|
|
160
|
+
["--fix", "repair missing/stale Quest agents and Quest plugin install/version when possible, then rerun doctor"],
|
|
161
|
+
["open", "repair/check setup, then launch interactive Claude Code from the project root"],
|
|
158
162
|
["install-agents", "install quest-executor and quest-reviewer as native Claude Code custom agents"],
|
|
159
163
|
["--scope <s>", "project (default: .claude/agents at repo root) or user (~/.claude/agents)"],
|
|
160
164
|
["--dry-run", "show intended agent writes without changing files"],
|
|
161
|
-
["--force", "replace existing agent files"],
|
|
165
|
+
["--force", "replace existing non-Quest agent files"],
|
|
162
166
|
["--json", "machine-readable output"],
|
|
163
167
|
],
|
|
164
|
-
example: "quest claude
|
|
168
|
+
example: "quest claude doctor --fix && quest claude open",
|
|
165
169
|
},
|
|
166
170
|
};
|
|
167
171
|
|
|
@@ -221,9 +225,11 @@ export function renderInitNextSteps(backend, { agentsInstalled = true } = {}) {
|
|
|
221
225
|
"",
|
|
222
226
|
"Store created. Next steps:",
|
|
223
227
|
...(agentsInstalled ? [" 0. Restart agent sessions so newly installed project templates are loaded"] : []),
|
|
224
|
-
" 1.
|
|
225
|
-
" 2.
|
|
226
|
-
"
|
|
228
|
+
...(agentsInstalled ? [" 1. Repair/check providers: quest codex doctor --fix (or quest claude doctor --fix)"] : []),
|
|
229
|
+
...(agentsInstalled ? [" 2. Open provider: quest codex open (or quest claude open)"] : []),
|
|
230
|
+
` ${agentsInstalled ? "3" : "1"}. Author a quest: quest create --help (or $quest:plan in your agent session)`,
|
|
231
|
+
` ${agentsInstalled ? "4" : "2"}. Check it: quest lint --all`,
|
|
232
|
+
` ${agentsInstalled ? "5" : "3"}. Work it: $quest:work <id> in-session, or quest-run <id> headless`,
|
|
227
233
|
...(agentsInstalled ? [] : [" Agent templates skipped; install later with `quest codex install-agents --scope project` and `quest claude install-agents --scope project`."]),
|
|
228
234
|
...(backend === "github" ? ["", "Records live as GitHub issues; config and amendments stay local in .quests/."] : []),
|
|
229
235
|
].join("\n");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quest-loop",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
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" },
|
package/skills/setup/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: setup
|
|
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]"
|
|
3
|
+
description: Use when setting up, validating, updating, opening, or troubleshooting Quest's native Codex or Claude plugin integration.
|
|
4
|
+
argument-hint: "[doctor|fix|open|install-agents|init]"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Set up Quest native agents
|
|
@@ -19,9 +19,10 @@ agent templates for both providers:
|
|
|
19
19
|
- `.claude/agents/quest-reviewer.md`
|
|
20
20
|
|
|
21
21
|
Use `quest init --no-agents` when you only want the quest store. If an existing
|
|
22
|
-
project template
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
project template is stale and still identifies itself as Quest's agent, init
|
|
23
|
+
replaces it. If another file conflicts, init fails before creating `.quests/`;
|
|
24
|
+
inspect the file, run the explicit provider install command with `--force` only
|
|
25
|
+
when you intend replacement, then rerun `quest init`.
|
|
25
26
|
|
|
26
27
|
Project-scoped agent templates install at the Git repository root. For a nested
|
|
27
28
|
quest store, use `quest init --no-agents` in the nested directory and set
|
|
@@ -32,7 +33,7 @@ quest store, use `quest init --no-agents` in the nested directory and set
|
|
|
32
33
|
Check the installed Codex-facing state from the actual Codex surfaces:
|
|
33
34
|
|
|
34
35
|
```bash
|
|
35
|
-
quest codex doctor
|
|
36
|
+
quest codex doctor --fix
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
This verifies the Codex CLI, `quest` binary on PATH, `multi_agent` and `goals`
|
|
@@ -45,7 +46,14 @@ use `quest-run --codex-goal-mode require` only as the headless fallback.
|
|
|
45
46
|
Check the installed Claude-facing state from the actual Claude surfaces:
|
|
46
47
|
|
|
47
48
|
```bash
|
|
48
|
-
quest claude doctor
|
|
49
|
+
quest claude doctor --fix
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Open an interactive provider only after the same repair/check gate passes:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
quest codex open
|
|
56
|
+
quest claude open
|
|
49
57
|
```
|
|
50
58
|
|
|
51
59
|
## Install Native Agents
|
|
@@ -70,8 +78,10 @@ quest claude install-agents --scope user
|
|
|
70
78
|
```
|
|
71
79
|
|
|
72
80
|
If an existing file conflicts, inspect it first. Use `--force` only when you
|
|
73
|
-
intend to replace that custom agent with Quest's bundled definition.
|
|
74
|
-
|
|
81
|
+
intend to replace that custom agent with Quest's bundled definition. Stale
|
|
82
|
+
Quest-owned templates are replaced without `--force`. Symlinked agent template
|
|
83
|
+
files can be written through with `--force`; keep the provider agent directories
|
|
84
|
+
themselves as real directories.
|
|
75
85
|
|
|
76
86
|
## Update Installed Plugin
|
|
77
87
|
|
|
@@ -81,7 +91,14 @@ refresh and reinstall, then start a new Codex thread:
|
|
|
81
91
|
```bash
|
|
82
92
|
codex plugin marketplace upgrade quest
|
|
83
93
|
codex plugin add quest@quest
|
|
84
|
-
quest codex doctor
|
|
94
|
+
quest codex doctor --fix
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Claude updates through its plugin manager. After updating, restart Claude Code:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
claude plugin update quest@quest
|
|
101
|
+
quest claude doctor --fix
|
|
85
102
|
```
|
|
86
103
|
|
|
87
104
|
If hooks changed, review/trust the updated hook definitions in Codex when
|