quest-loop 0.1.0 → 0.3.0
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 +90 -0
- package/README.md +67 -17
- package/agents/quest-executor.md +2 -2
- package/hooks/hooks.json +40 -5
- package/hooks/subagent-stop.mjs +140 -10
- package/lib/cli.mjs +98 -1
- package/lib/codex-native.mjs +246 -0
- package/lib/config.mjs +1 -1
- package/lib/contract.mjs +22 -2
- package/lib/help.mjs +23 -4
- package/lib/runner.mjs +59 -3
- package/lib/store-github.mjs +34 -2
- package/lib/store-local.mjs +32 -2
- package/lib/store.mjs +2 -0
- package/lib/workers.mjs +26 -12
- package/package.json +3 -3
- package/skills/orchestrate/SKILL.md +64 -11
- package/skills/orchestrate/agents/openai.yaml +1 -1
- package/skills/plan/SKILL.md +11 -2
- package/skills/plan/agents/openai.yaml +1 -1
- package/skills/protocol/SKILL.md +2 -2
- package/skills/protocol/agents/openai.yaml +1 -1
- package/skills/protocol/references/contract-spec.md +14 -5
- package/skills/protocol/references/protocol.md +12 -4
- package/skills/retro/SKILL.md +2 -2
- package/skills/retro/agents/openai.yaml +1 -1
- package/skills/setup/SKILL.md +56 -0
- package/skills/setup/agents/openai.yaml +7 -0
- package/skills/work/SKILL.md +3 -3
- package/skills/work/agents/openai.yaml +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,96 @@ 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
|
+
## [0.3.0] — 2026-07-08
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `$quest:setup` skill plus `quest codex doctor` for native Codex validation:
|
|
11
|
+
checks Codex CLI availability, installed `quest@quest` version, hook parser
|
|
12
|
+
health, neutral-directory Quest skill roots, and native `quest-executor` /
|
|
13
|
+
`quest-reviewer` custom-agent availability.
|
|
14
|
+
- `quest codex install-agents --scope project|user` installs Quest's bundled
|
|
15
|
+
agent TOML files into Codex's native `.codex/agents` or `~/.codex/agents`
|
|
16
|
+
locations. The command is idempotent, supports `--dry-run`, and requires
|
|
17
|
+
`--force` before replacing different existing agent files.
|
|
18
|
+
- `quest --version` reports the package version.
|
|
19
|
+
- `quest-run --codex-goal-mode auto|require|off` controls how headless Codex
|
|
20
|
+
runs treat goal tools. `auto` is the default and uses documented
|
|
21
|
+
`codex exec --json --output-schema` output as the contract; `require` blocks
|
|
22
|
+
honestly when `create_goal` is not observed; `off` avoids goal-tool prompts.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Codex is now the first-class plugin path in docs and skill UI metadata:
|
|
26
|
+
examples use `$quest:*`, the Codex setup flow runs `quest codex doctor`, and
|
|
27
|
+
`package.json`, `.claude-plugin/plugin.json`, and `.codex-plugin/plugin.json`
|
|
28
|
+
now share the same version.
|
|
29
|
+
- `codex exec resume` invocations no longer pass unsupported `-C`; the runner
|
|
30
|
+
relies on the child process working directory and original Codex session.
|
|
31
|
+
- Bundled hooks now prefer `CODEX_PLUGIN_ROOT` with a `CLAUDE_PLUGIN_ROOT`
|
|
32
|
+
fallback, include a `resume` SessionStart matcher, status messages, timeouts,
|
|
33
|
+
and Windows command variants.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- SubagentStop hook now recognizes Codex JSONL `command_execution` entries in
|
|
37
|
+
addition to Claude-style `tool_use.input.command` blocks, while preserving the
|
|
38
|
+
mutating-verb-only detection rule.
|
|
39
|
+
|
|
40
|
+
## [0.2.0] — 2026-07-08
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
- `quest reopen <id> --reason <why>` — the audited exit from `complete`. Flips a
|
|
44
|
+
completed quest back to `in_progress` on both backends, recording the reason in
|
|
45
|
+
a real checkpoint via the new optional `reopen_reason` field; the GitHub backend
|
|
46
|
+
reopens the mirrored issue and swaps `quest:complete → quest:in-progress`.
|
|
47
|
+
`TRANSITIONS.complete` stays empty — a checkpoint can never resurrect a complete
|
|
48
|
+
quest; only the explicit verb can, and `cancelled` remains fully terminal.
|
|
49
|
+
Reopening a child of a complete parent epic warns on stderr that the epic's
|
|
50
|
+
verdict may be falsified. Reopened quests do not re-enter `quest list --ready`;
|
|
51
|
+
they are dispatched directly by id. `quest edit` on a complete or cancelled
|
|
52
|
+
quest now errors with a reopen-first / file-a-new-quest hint, and `quest-run`
|
|
53
|
+
on an already-complete quest suggests the verb instead of silently no-opping.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
- Wave composition now treats epics (parent quests) as orchestrator-closed, not
|
|
57
|
+
worker-dispatched. `quest list --ready` excludes any quest with a non-terminal
|
|
58
|
+
child (a child in complete or cancelled is terminal, so a cancelled child no
|
|
59
|
+
longer wedges its epic), in both the local and GitHub backends. `quest-run
|
|
60
|
+
--ready` additionally refuses to auto-dispatch a quest that has children even
|
|
61
|
+
once they are all terminal, logging an actionable "is an epic — close it inline
|
|
62
|
+
per $quest:orchestrate" line; a direct `quest-run <id>` on an epic stays
|
|
63
|
+
allowed. The orchestrate skill gains a "Closing an epic" procedure and the plan
|
|
64
|
+
skill documents that epic contracts are integration-level only.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
- SubagentStop hook now keys quest-executor detection to real *mutating*
|
|
68
|
+
invocations — `quest start <id>` or `quest checkpoint <id>` (under any binary
|
|
69
|
+
prefix: `quest`, `./bin/quest`, `node bin/quest`) — instead of the read-only
|
|
70
|
+
`quest show <id> --json` orientation call. Reviewers and orchestrators that run
|
|
71
|
+
only read verbs (show / list / protocol / runs) never owned a quest, so they are
|
|
72
|
+
allowed silently; this removes a false positive where a read-only quest-reviewer
|
|
73
|
+
was blocked at stop for merely inspecting a terminal quest. A `quest checkpoint`
|
|
74
|
+
invocation counts too, so an executor resuming an already-in_progress quest
|
|
75
|
+
(which skips `quest start`) is still detected. The first mutating invocation wins
|
|
76
|
+
(deterministic); the executor block, per-entry parsing that keeps skill-text
|
|
77
|
+
examples from keying detection, and conservative allow-on-ambiguity are all
|
|
78
|
+
preserved.
|
|
79
|
+
- SubagentStop hook now derives the quest-executor id by parsing the transcript
|
|
80
|
+
JSONL per-entry and matching its marker only inside real tool_use command
|
|
81
|
+
invocations — never prose, examples, or echoed file contents. This removes a
|
|
82
|
+
false positive where skill-text examples keyed the detection ahead of the
|
|
83
|
+
executor's real call. The first real invocation wins (deterministic);
|
|
84
|
+
conservative allow-on-ambiguity is preserved.
|
|
85
|
+
|
|
86
|
+
## [0.1.1] — 2026-07-07
|
|
87
|
+
|
|
88
|
+
### Fixed
|
|
89
|
+
- Codex plugin hook config now uses the strict top-level `hooks` schema that
|
|
90
|
+
Codex accepts, removing the startup parse warning caused by an extra
|
|
91
|
+
`description` field.
|
|
92
|
+
|
|
93
|
+
### Changed
|
|
94
|
+
- Manifest validation now checks `hooks/hooks.json` for Codex-compatible
|
|
95
|
+
top-level fields before packaging or release.
|
|
96
|
+
|
|
7
97
|
## [0.1.0] — 2026-07-07
|
|
8
98
|
|
|
9
99
|
First public release. The build of this release was itself executed as quests
|
package/README.md
CHANGED
|
@@ -12,17 +12,17 @@ actually read.
|
|
|
12
12
|
- `quest` — the quest store: contracts, checkpoints, wave scheduling. Local
|
|
13
13
|
markdown files by default; GitHub Issues opt-in.
|
|
14
14
|
- `quest-run` — the headless runner: drives `claude -p` or `codex exec` workers
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
- Two
|
|
15
|
+
with deterministic budgets, notifications, and checkpoint-based stop checks.
|
|
16
|
+
- Six skills — `$quest:plan`, `$quest:work`, `$quest:orchestrate`,
|
|
17
|
+
`$quest:retro`, `$quest:protocol`, `$quest:setup`.
|
|
18
|
+
- Two native-agent templates — `quest-executor`, `quest-reviewer`.
|
|
19
19
|
|
|
20
20
|
## The idea in 30 seconds
|
|
21
21
|
|
|
22
22
|
```
|
|
23
|
-
you:
|
|
23
|
+
you: $quest:plan add dark mode to the settings page
|
|
24
24
|
agent: creates quest 12 — Objective, Done-when, Validation loop… (quest create)
|
|
25
|
-
you:
|
|
25
|
+
you: $quest:orchestrate
|
|
26
26
|
agent: dispatches a worker on quest 12; it iterates: milestone → validate →
|
|
27
27
|
commit → checkpoint. You review evidence, not vibes.
|
|
28
28
|
```
|
|
@@ -39,7 +39,7 @@ npm install -g quest-loop
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Puts `quest` and `quest-run` on your PATH everywhere — no harness required.
|
|
42
|
-
The plugin installs below add the skills,
|
|
42
|
+
The plugin installs below add the skills, hooks, and native-agent setup on top.
|
|
43
43
|
|
|
44
44
|
### Claude Code
|
|
45
45
|
|
|
@@ -62,14 +62,46 @@ claude --plugin-dir .
|
|
|
62
62
|
|
|
63
63
|
### Codex
|
|
64
64
|
|
|
65
|
-
The plugin ships a `.codex-plugin/` manifest
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
submodule:
|
|
65
|
+
The plugin ships a `.codex-plugin/` manifest plus the same Git marketplace
|
|
66
|
+
metadata used by Claude Code. Install the marketplace once, then install the
|
|
67
|
+
plugin from that marketplace:
|
|
69
68
|
|
|
70
69
|
```bash
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
codex plugin marketplace add robertsreberski/quest
|
|
71
|
+
codex plugin add quest@quest
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Start a new Codex thread after installing so the skills and hooks are loaded.
|
|
75
|
+
Verify the install with:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
codex plugin list --marketplace quest
|
|
79
|
+
quest codex install-agents --scope project
|
|
80
|
+
quest codex doctor
|
|
81
|
+
codex debug prompt-input "noop"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`quest codex doctor` checks the installed plugin version, hook parser, neutral
|
|
85
|
+
skill roots, and native Codex custom agents. `codex debug prompt-input "noop"`
|
|
86
|
+
should not print any hook parse warnings.
|
|
87
|
+
|
|
88
|
+
#### Updating the Codex plugin
|
|
89
|
+
|
|
90
|
+
Codex installs plugins from a marketplace snapshot. Pulling this Git repo or
|
|
91
|
+
publishing a new tag is not enough to update the already-installed plugin cache.
|
|
92
|
+
Refresh the marketplace snapshot, then reinstall the plugin from it:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
codex plugin marketplace upgrade quest
|
|
96
|
+
codex plugin add quest@quest
|
|
97
|
+
codex plugin list --marketplace quest
|
|
98
|
+
quest codex doctor
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Then start a new Codex thread. If the update contains hook changes, re-run:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
codex debug prompt-input "noop"
|
|
73
105
|
```
|
|
74
106
|
|
|
75
107
|
The CLI (`quest`, `quest-run`) is harness-agnostic — it works the same whether
|
|
@@ -100,9 +132,12 @@ quest list --ready
|
|
|
100
132
|
Work the quest **in-session** with the skill:
|
|
101
133
|
|
|
102
134
|
```
|
|
103
|
-
|
|
135
|
+
$quest:work 12
|
|
104
136
|
```
|
|
105
137
|
|
|
138
|
+
In Claude Code, use the same bundled skills through the `/quest:*` slash-command
|
|
139
|
+
form.
|
|
140
|
+
|
|
106
141
|
…or **headless** with the runner:
|
|
107
142
|
|
|
108
143
|
```bash
|
|
@@ -144,19 +179,21 @@ full base protocol lives in
|
|
|
144
179
|
| `quest start` | Mark a quest in_progress (todo → in_progress) |
|
|
145
180
|
| `quest checkpoint` | Record iteration evidence and drive the quest's status |
|
|
146
181
|
| `quest cancel` | Cancel a quest (terminal; reason is recorded) |
|
|
182
|
+
| `quest reopen` | Reopen a complete quest back into the loop (complete → in_progress; reason recorded) |
|
|
147
183
|
| `quest edit` | Compatibly expand a quest (additions only; anchors are immutable) |
|
|
148
184
|
| `quest lint` | Check records against the contract spec |
|
|
149
185
|
| `quest amend` | Append a numbered protocol amendment (retro output) |
|
|
150
186
|
| `quest protocol` | Print the loop protocol + this store's local amendments |
|
|
151
187
|
| `quest runs` | Show headless runner activity (from `.quests/runs.ndjson`) |
|
|
188
|
+
| `quest codex` | Validate Codex-native setup and install native agent templates |
|
|
152
189
|
|
|
153
190
|
## quest-run (headless runner)
|
|
154
191
|
|
|
155
192
|
`quest-run <id>` drives a worker through the same loop without you in the chair:
|
|
156
193
|
|
|
157
194
|
- **Workers** — `claude` (`claude -p`) or `codex` (`codex exec`), selected per
|
|
158
|
-
quest. Both
|
|
159
|
-
|
|
195
|
+
quest. Both use a machine-verifiable Quest checkpoint completion condition;
|
|
196
|
+
Codex goal tools are optional by default and can be required explicitly.
|
|
160
197
|
- **Budgets** — deterministic iteration, cost, token, and per-session
|
|
161
198
|
wall-clock (`--session-timeout`, default 1800s) caps; two sessions without a
|
|
162
199
|
new checkpoint (a killed hung session counts as one) auto-writes a `blocked`
|
|
@@ -165,6 +202,12 @@ full base protocol lives in
|
|
|
165
202
|
goes through the `quest` CLI, and the runs journal stays local.
|
|
166
203
|
- **`--parallel N`** — with `--ready`, promotes and works newly-ready quests
|
|
167
204
|
across dependency waves, up to N at a time.
|
|
205
|
+
- **Epics are never auto-dispatched** — a quest with children stays out of
|
|
206
|
+
`--ready` until every child is terminal, and `quest-run --ready` refuses it
|
|
207
|
+
even then (it logs an "is an epic" skip line). Epics are closed by the
|
|
208
|
+
orchestrator inline per `$quest:orchestrate`, not by burning a worker run on
|
|
209
|
+
pure verification. A direct `quest-run <id>` on an epic still runs, if you
|
|
210
|
+
really mean to.
|
|
168
211
|
- **`--notify '<cmd>'`** — runs a templated command on run start/stop so you get
|
|
169
212
|
pinged; notify failures are isolated from the run.
|
|
170
213
|
- **`--codex-sandbox <mode>`** — selects the `codex exec` sandbox
|
|
@@ -175,12 +218,19 @@ full base protocol lives in
|
|
|
175
218
|
must commit has to opt into **`danger-full-access`** — which also grants full
|
|
176
219
|
disk and network access. The runner never escalates the sandbox silently; the
|
|
177
220
|
safe `workspace-write` stays the default. (Claude workers ignore this flag.)
|
|
221
|
+
- **`--codex-goal-mode <mode>`** — selects how `quest-run` treats Codex goal
|
|
222
|
+
tools (`auto` | `require` | `off`; resolved as flag → config
|
|
223
|
+
`defaults.codex.goal_mode` → default `auto`). `auto` uses the documented
|
|
224
|
+
`codex exec --json --output-schema` stream as the contract and lets Codex use
|
|
225
|
+
goal tools if they are available. `require` blocks honestly if the exec
|
|
226
|
+
surface does not expose or invoke `create_goal`. `off` never asks for goal
|
|
227
|
+
tools.
|
|
178
228
|
|
|
179
229
|
Inspect activity with `quest runs --active`.
|
|
180
230
|
|
|
181
231
|
> **Note:** `quest-run` ships with this build's runner milestone (tracked as
|
|
182
232
|
> quest 5). On a pre-0.1.0 checkout where `bin/quest-run` isn't present yet, use
|
|
183
|
-
>
|
|
233
|
+
> `$quest:work` in-session instead.
|
|
184
234
|
|
|
185
235
|
## Store backends
|
|
186
236
|
|
package/agents/quest-executor.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: quest-executor
|
|
3
|
-
description: Works exactly one quest record iteratively per the quest protocol until complete or blocked, recording checkpoint evidence via the quest CLI. Dispatch with the quest id; pass the record's model/effort as the dispatch override. <example>Orchestrator sees quest 12 ready with worker claude → dispatches quest-executor with "Work quest 12 per
|
|
3
|
+
description: Works exactly one quest record iteratively per the quest protocol until complete or blocked, recording checkpoint evidence via the quest CLI. Dispatch with the quest id; pass the record's model/effort as the dispatch override. <example>Orchestrator sees quest 12 ready with worker claude → dispatches quest-executor with "Work quest 12 per $quest:work"; it iterates milestone-by-milestone and ends with a recorded checkpoint.</example> <example>A quest sits blocked after a human ruling → redispatch quest-executor with the ruling; it resumes from the checkpoint trail alone.</example>
|
|
4
4
|
model: opus
|
|
5
5
|
effort: xhigh
|
|
6
6
|
tools: Bash, Read, Edit, Write, Glob, Grep, WebFetch, WebSearch, NotebookEdit
|
|
@@ -18,7 +18,7 @@ quest protocol # the loop rules + this store's amendments
|
|
|
18
18
|
git log --oneline -5
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
Then follow the work skill (
|
|
21
|
+
Then follow the work skill (`$quest:work`) exactly: smallest unfinished
|
|
22
22
|
milestone → the quest's STATED validation loop → commit green → `quest
|
|
23
23
|
checkpoint`. The record is your entire spec; if you need context it doesn't
|
|
24
24
|
give you, read the code it points at — and if it's genuinely insufficient,
|
package/hooks/hooks.json
CHANGED
|
@@ -1,30 +1,65 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Quest goal-loop hooks: inject in-flight quest context at session start, and block quest-executor subagents that try to stop without recording a checkpoint.",
|
|
3
2
|
"hooks": {
|
|
4
3
|
"SessionStart": [
|
|
5
4
|
{
|
|
6
5
|
"matcher": "startup",
|
|
7
6
|
"hooks": [
|
|
8
|
-
{
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "root=\"${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}\"; [ -n \"$root\" ] && node \"$root/hooks/session-start.mjs\"",
|
|
10
|
+
"commandWindows": "if defined CODEX_PLUGIN_ROOT (node \"%CODEX_PLUGIN_ROOT%\\hooks\\session-start.mjs\") else if defined CLAUDE_PLUGIN_ROOT (node \"%CLAUDE_PLUGIN_ROOT%\\hooks\\session-start.mjs\")",
|
|
11
|
+
"timeout": 10,
|
|
12
|
+
"statusMessage": "Loading quest state"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"matcher": "resume",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "root=\"${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}\"; [ -n \"$root\" ] && node \"$root/hooks/session-start.mjs\"",
|
|
22
|
+
"commandWindows": "if defined CODEX_PLUGIN_ROOT (node \"%CODEX_PLUGIN_ROOT%\\hooks\\session-start.mjs\") else if defined CLAUDE_PLUGIN_ROOT (node \"%CLAUDE_PLUGIN_ROOT%\\hooks\\session-start.mjs\")",
|
|
23
|
+
"timeout": 10,
|
|
24
|
+
"statusMessage": "Loading quest state"
|
|
25
|
+
}
|
|
9
26
|
]
|
|
10
27
|
},
|
|
11
28
|
{
|
|
12
29
|
"matcher": "clear",
|
|
13
30
|
"hooks": [
|
|
14
|
-
{
|
|
31
|
+
{
|
|
32
|
+
"type": "command",
|
|
33
|
+
"command": "root=\"${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}\"; [ -n \"$root\" ] && node \"$root/hooks/session-start.mjs\"",
|
|
34
|
+
"commandWindows": "if defined CODEX_PLUGIN_ROOT (node \"%CODEX_PLUGIN_ROOT%\\hooks\\session-start.mjs\") else if defined CLAUDE_PLUGIN_ROOT (node \"%CLAUDE_PLUGIN_ROOT%\\hooks\\session-start.mjs\")",
|
|
35
|
+
"timeout": 10,
|
|
36
|
+
"statusMessage": "Loading quest state"
|
|
37
|
+
}
|
|
15
38
|
]
|
|
16
39
|
},
|
|
17
40
|
{
|
|
18
41
|
"matcher": "compact",
|
|
19
42
|
"hooks": [
|
|
20
|
-
{
|
|
43
|
+
{
|
|
44
|
+
"type": "command",
|
|
45
|
+
"command": "root=\"${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}\"; [ -n \"$root\" ] && node \"$root/hooks/session-start.mjs\"",
|
|
46
|
+
"commandWindows": "if defined CODEX_PLUGIN_ROOT (node \"%CODEX_PLUGIN_ROOT%\\hooks\\session-start.mjs\") else if defined CLAUDE_PLUGIN_ROOT (node \"%CLAUDE_PLUGIN_ROOT%\\hooks\\session-start.mjs\")",
|
|
47
|
+
"timeout": 10,
|
|
48
|
+
"statusMessage": "Loading quest state"
|
|
49
|
+
}
|
|
21
50
|
]
|
|
22
51
|
}
|
|
23
52
|
],
|
|
24
53
|
"SubagentStop": [
|
|
25
54
|
{
|
|
26
55
|
"hooks": [
|
|
27
|
-
{
|
|
56
|
+
{
|
|
57
|
+
"type": "command",
|
|
58
|
+
"command": "root=\"${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}\"; [ -n \"$root\" ] && node \"$root/hooks/subagent-stop.mjs\"",
|
|
59
|
+
"commandWindows": "if defined CODEX_PLUGIN_ROOT (node \"%CODEX_PLUGIN_ROOT%\\hooks\\subagent-stop.mjs\") else if defined CLAUDE_PLUGIN_ROOT (node \"%CLAUDE_PLUGIN_ROOT%\\hooks\\subagent-stop.mjs\")",
|
|
60
|
+
"timeout": 10,
|
|
61
|
+
"statusMessage": "Checking quest checkpoint"
|
|
62
|
+
}
|
|
28
63
|
]
|
|
29
64
|
}
|
|
30
65
|
]
|
package/hooks/subagent-stop.mjs
CHANGED
|
@@ -3,12 +3,24 @@
|
|
|
3
3
|
// recording a checkpoint — the protocol's "no stop without a checkpoint" rule,
|
|
4
4
|
// enforced deterministically.
|
|
5
5
|
//
|
|
6
|
-
// A subagent counts as a quest-executor iff its transcript
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
6
|
+
// A subagent counts as a quest-executor iff one of its transcript entries records
|
|
7
|
+
// an actual *mutating* quest invocation — `quest start <id>` or `quest checkpoint
|
|
8
|
+
// <id>` (under any binary prefix: `quest`, `./bin/quest`, `node bin/quest`) — as a
|
|
9
|
+
// tool_use block whose shell command carries the marker. Read-only verbs
|
|
10
|
+
// (`quest show <id> --json`, `list`, `protocol`, `runs`) are how reviewers and
|
|
11
|
+
// orchestrators inspect a quest without owning it, so they must NEVER key
|
|
12
|
+
// detection: an agent whose transcript holds only read verbs is allowed silently.
|
|
13
|
+
// The executor id comes from that first mutating invocation (deterministic,
|
|
14
|
+
// transcript order) — including `quest checkpoint <id>`, because an executor
|
|
15
|
+
// resuming an already-in_progress quest skips `quest start` and its first mutating
|
|
16
|
+
// verb is the checkpoint. We parse the JSONL per-entry and inspect only tool_use
|
|
17
|
+
// command inputs; prose, quoted skill text, examples, and echoed file contents live
|
|
18
|
+
// in text blocks and tool_result content (never in a tool_use command), so
|
|
19
|
+
// `quest start 12` / `quest checkpoint 12` examples in the skills can no longer key
|
|
20
|
+
// detection. No mutating invocation → not our concern, allow silently. We then
|
|
21
|
+
// compare the quest's latest checkpoint against the subagent's start time (the
|
|
22
|
+
// transcript's first timestamp): a checkpoint newer than start clears the stop; so
|
|
23
|
+
// does a terminal store status (complete/blocked/cancelled) reached during the run.
|
|
12
24
|
//
|
|
13
25
|
// Conservative by construction: any missing/unreadable input or parse failure →
|
|
14
26
|
// allow (exit 0), with a one-line stderr diagnostic. We never false-positive-block
|
|
@@ -22,7 +34,73 @@ import { readFileSync, writeSync } from "node:fs";
|
|
|
22
34
|
import { findStoreDir } from "../lib/config.mjs";
|
|
23
35
|
import { loadQuest } from "../lib/store-local.mjs";
|
|
24
36
|
|
|
25
|
-
|
|
37
|
+
// Mutating quest verbs only. Group 1 is the verb, group 2 the id. The marker is
|
|
38
|
+
// anchored to a COMMAND position — an optional `node ` launcher and/or path prefix
|
|
39
|
+
// then `quest <verb> <id>` at the head of a command — so `quest checkpoint 1`
|
|
40
|
+
// merely quoted inside another program's argument (e.g. `grep 'quest checkpoint 1'`,
|
|
41
|
+
// `git commit -m "quest checkpoint 1"`) does not falsely key executor detection.
|
|
42
|
+
// Read verbs (show/list/protocol/runs) are deliberately absent.
|
|
43
|
+
const MARKER = /^(?:node\s+)?(?:[.\w/@-]*\/)?quest\s+(start|checkpoint)\s+(\d+)/;
|
|
44
|
+
|
|
45
|
+
// Split a shell command into top-level segments on `&&`, `||`, `;`, `|`, and
|
|
46
|
+
// newline, IGNORING any separator that sits inside single or double quotes. This
|
|
47
|
+
// keeps quoted text (grep patterns, commit messages) from forging a command head.
|
|
48
|
+
// (Heredoc bodies on their own line remain a rare residual; a false "checkpoint"
|
|
49
|
+
// nudge is benign, and heredoc parsing is not worth the complexity in a hook.)
|
|
50
|
+
function splitTopLevel(s) {
|
|
51
|
+
const segs = [];
|
|
52
|
+
let buf = "";
|
|
53
|
+
let quote = null;
|
|
54
|
+
let escaped = false;
|
|
55
|
+
for (let i = 0; i < s.length; i++) {
|
|
56
|
+
const c = s[i];
|
|
57
|
+
if (escaped) {
|
|
58
|
+
buf += c;
|
|
59
|
+
escaped = false;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (quote !== "'" && c === "\\") {
|
|
63
|
+
buf += c;
|
|
64
|
+
escaped = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (quote) {
|
|
68
|
+
if (c === quote) quote = null;
|
|
69
|
+
buf += c;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (c === "'" || c === '"') { quote = c; buf += c; continue; }
|
|
73
|
+
if ((c === "&" && s[i + 1] === "&") || (c === "|" && s[i + 1] === "|")) { segs.push(buf); buf = ""; i++; continue; }
|
|
74
|
+
if (c === ";" || c === "|" || c === "\n") { segs.push(buf); buf = ""; continue; }
|
|
75
|
+
buf += c;
|
|
76
|
+
}
|
|
77
|
+
segs.push(buf);
|
|
78
|
+
return segs;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Strip a leading shell wrapper (`bash -lc '…'`, `sh -c "…"`) and split the command
|
|
82
|
+
// chain, then look for a quest invocation at the head of any segment. Returns the
|
|
83
|
+
// marker id or null.
|
|
84
|
+
function markerIdInCommand(cmd) {
|
|
85
|
+
if (typeof cmd !== "string") return null;
|
|
86
|
+
let s = cmd.trim();
|
|
87
|
+
const wrap = s.match(/^(?:sudo\s+)?(?:ba)?sh\s+-l?c\s+(['"])([\s\S]*)\1\s*$/);
|
|
88
|
+
if (wrap) s = wrap[2].trim();
|
|
89
|
+
for (const seg of splitTopLevel(s)) {
|
|
90
|
+
const m = MARKER.exec(seg.trim());
|
|
91
|
+
if (m) return Number(m[2]); // m[1] = verb, m[2] = id
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// A Codex `command_execution` item's shell command, across the JSONL envelope
|
|
97
|
+
// shapes we know: top-level `item`, legacy `msg` (the item sits directly at
|
|
98
|
+
// `entry.msg`), and `msg.item`. Returns the command string or null.
|
|
99
|
+
function commandExecutionCommand(node) {
|
|
100
|
+
if (!node || typeof node !== "object") return null;
|
|
101
|
+
if (node.type === "command_execution" && typeof node.command === "string") return node.command;
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
26
104
|
const TERMINAL = ["complete", "blocked", "cancelled"];
|
|
27
105
|
|
|
28
106
|
async function readStdin() {
|
|
@@ -57,6 +135,59 @@ function firstTimestamp(text) {
|
|
|
57
135
|
return null;
|
|
58
136
|
}
|
|
59
137
|
|
|
138
|
+
// A tool_use block's shell command, or null. Bash invocations carry the command
|
|
139
|
+
// under `input.command`; only that field is a real command invocation. We never
|
|
140
|
+
// scan other input fields (e.g. an Edit's new_string or a Read's file_path), which
|
|
141
|
+
// could echo skill text and re-introduce the false positive.
|
|
142
|
+
function commandOf(input) {
|
|
143
|
+
return input && typeof input === "object" && typeof input.command === "string" ? input.command : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// The mutating-verb marker id from one transcript entry, considering only tool_use
|
|
147
|
+
// command invocations. Assistant messages carry an array of content blocks; string
|
|
148
|
+
// content (plain prose) and tool_result blocks (echoed output/file contents) are
|
|
149
|
+
// ignored. Matches `quest start <id>` or `quest checkpoint <id>` and returns the id.
|
|
150
|
+
function markerIdInEntry(entry) {
|
|
151
|
+
if (!entry || typeof entry !== "object") return null;
|
|
152
|
+
|
|
153
|
+
// Codex JSONL: a `command_execution` item under `item`, the legacy `msg`, or
|
|
154
|
+
// `msg.item`. Handling all three keeps executor detection robust across shapes.
|
|
155
|
+
for (const cand of [entry.item, entry.msg, entry.msg?.item]) {
|
|
156
|
+
const cmd = commandExecutionCommand(cand);
|
|
157
|
+
if (cmd != null) {
|
|
158
|
+
const id = markerIdInCommand(cmd);
|
|
159
|
+
if (id != null) return id;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Claude assistant messages: tool_use blocks carrying a shell command. String
|
|
164
|
+
// content (plain prose) and tool_result blocks are ignored.
|
|
165
|
+
const msg = entry.message;
|
|
166
|
+
const content = msg && typeof msg === "object" ? msg.content : null;
|
|
167
|
+
if (!Array.isArray(content)) return null; // string content is prose, never an invocation
|
|
168
|
+
for (const block of content) {
|
|
169
|
+
if (!block || typeof block !== "object" || block.type !== "tool_use") continue;
|
|
170
|
+
const id = markerIdInCommand(commandOf(block.input));
|
|
171
|
+
if (id != null) return id;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// The executor's quest id = the FIRST mutating invocation (`quest start <id>` or
|
|
177
|
+
// `quest checkpoint <id>`) that appears as a real command invocation, scanning
|
|
178
|
+
// entries in transcript order. Returns id or null. Per-entry parsing is what keeps
|
|
179
|
+
// skill-text examples from keying detection; read verbs never match at all.
|
|
180
|
+
function executorQuestId(text) {
|
|
181
|
+
for (const line of text.split("\n")) {
|
|
182
|
+
if (!line.trim()) continue;
|
|
183
|
+
let entry;
|
|
184
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
185
|
+
const id = markerIdInEntry(entry);
|
|
186
|
+
if (id != null) return id;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
60
191
|
try {
|
|
61
192
|
const raw = await readStdin();
|
|
62
193
|
let payload = {};
|
|
@@ -69,9 +200,8 @@ try {
|
|
|
69
200
|
try { transcript = readFileSync(transcriptPath, "utf8"); }
|
|
70
201
|
catch (err) { diag(`cannot read transcript (${err.code || err.message}); allowing`); allow(); }
|
|
71
202
|
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
const id = Number(m[1]);
|
|
203
|
+
const id = executorQuestId(transcript);
|
|
204
|
+
if (id == null) allow(); // no mutating quest invocation (read-only agent) — leave it alone, silently
|
|
75
205
|
|
|
76
206
|
// Prefer an explicit start field if a future payload carries one; else the
|
|
77
207
|
// transcript's first timestamp.
|
package/lib/cli.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import * as local from "./store-local.mjs";
|
|
|
11
11
|
import * as github from "./store-github.mjs";
|
|
12
12
|
import { openStore } from "./store.mjs";
|
|
13
13
|
import { COMMANDS, renderCommandHelp, renderGeneralHelp, renderInitNextSteps, renderNoStore, renderStatusOverview } from "./help.mjs";
|
|
14
|
+
import { doctor as codexDoctor, installAgents as installCodexAgents, versionInfo } from "./codex-native.mjs";
|
|
14
15
|
|
|
15
16
|
const PLUGIN_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
16
17
|
|
|
@@ -77,6 +78,12 @@ async function dispatch(argv, ctx) {
|
|
|
77
78
|
const [command, ...rest] = argv;
|
|
78
79
|
const { out, errOut, cwd, env } = ctx;
|
|
79
80
|
|
|
81
|
+
if (command === "--version" || command === "-V" || command === "version") {
|
|
82
|
+
const versions = versionInfo();
|
|
83
|
+
out(versions.package);
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
80
87
|
if (!command) {
|
|
81
88
|
const storeDir = findStoreDir(cwd, env);
|
|
82
89
|
if (!storeDir) {
|
|
@@ -127,7 +134,7 @@ const HANDLERS = {
|
|
|
127
134
|
const config = {
|
|
128
135
|
backend,
|
|
129
136
|
...(p.values.repo ? { github: { repo: p.values.repo } } : {}),
|
|
130
|
-
defaults: { worker: "claude", claude: { model: "opus", effort: "xhigh" }, codex: { model: "gpt-5.5", reasoning_effort: "medium" }, max_iterations: 8, priority: "p2" },
|
|
137
|
+
defaults: { worker: "claude", claude: { model: "opus", effort: "xhigh" }, codex: { model: "gpt-5.5", reasoning_effort: "medium", goal_mode: "auto" }, max_iterations: 8, priority: "p2" },
|
|
131
138
|
notify: { command: "" },
|
|
132
139
|
};
|
|
133
140
|
writeFileSync(join(storeDir, "config.json"), JSON.stringify(config, null, 2) + "\n");
|
|
@@ -309,6 +316,38 @@ const HANDLERS = {
|
|
|
309
316
|
return 0;
|
|
310
317
|
},
|
|
311
318
|
|
|
319
|
+
async reopen(args, { out, errOut, cwd, env }) {
|
|
320
|
+
const p = parse("reopen", args, { reason: { type: "string" } }, { positionals: 1 });
|
|
321
|
+
if (p.help) return void out(renderCommandHelp("reopen")) ?? 0;
|
|
322
|
+
const id = requireId(p, "reopen");
|
|
323
|
+
// --reason is validated in the store (a contract requirement, exit 5), not
|
|
324
|
+
// as a usage error — mirrors `cancel`. reopenQuest throws if it is missing.
|
|
325
|
+
const config = getStore(cwd, env);
|
|
326
|
+
const store = openStore(config, { env });
|
|
327
|
+
const q = store.reopenQuest(id, p.values.reason);
|
|
328
|
+
// Reopening a child of a *complete* parent epic is allowed, never blocked —
|
|
329
|
+
// but the epic's completion verdict may now be falsified, so warn the
|
|
330
|
+
// orchestrator on stderr (the epic-falsification cue). Best-effort: a
|
|
331
|
+
// missing/unloadable parent (or any lookup failure) must never fail an
|
|
332
|
+
// already-successful reopen. Runs for both backends via this one handler.
|
|
333
|
+
if (q.front.parent !== undefined) {
|
|
334
|
+
try {
|
|
335
|
+
const parent = store.loadQuest(q.front.parent);
|
|
336
|
+
if (parent.front.status === "complete") {
|
|
337
|
+
errOut(`quest: warning — quest ${id}'s parent epic ${q.front.parent} is complete; reopening this child may falsify the epic's completion verdict. Rule on it, and \`quest reopen ${q.front.parent} --reason "…"\` too if the integration gate no longer holds.`);
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
/* best-effort: never fail the reopen on a parent-lookup problem */
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (p.values.json) out(JSON.stringify({ ok: true, id, status: q.front.status }));
|
|
344
|
+
else {
|
|
345
|
+
out(`Quest ${id} reopened — now in_progress (reason recorded in an audited checkpoint).`);
|
|
346
|
+
out(`Reopened quests are dispatched directly by id (\`quest-run ${id}\` or $quest:work ${id}); they do not re-enter \`quest list --ready\`.`);
|
|
347
|
+
}
|
|
348
|
+
return 0;
|
|
349
|
+
},
|
|
350
|
+
|
|
312
351
|
async edit(args, { out, cwd, env }) {
|
|
313
352
|
const p = parse("edit", args, {
|
|
314
353
|
"add-done-when": { type: "string", multiple: true },
|
|
@@ -398,4 +437,62 @@ const HANDLERS = {
|
|
|
398
437
|
else for (const r of runs) out(`${r.run_id} quest ${r.quest} ${r.worker} ${r.ended ? `ended ${r.final_status}` : "ACTIVE"} iters:${r.iterations} $${r.cost_usd.toFixed(2)}`);
|
|
399
438
|
return 0;
|
|
400
439
|
},
|
|
440
|
+
|
|
441
|
+
async codex(args, { out, cwd, env }) {
|
|
442
|
+
const [subcommand, ...rest] = args;
|
|
443
|
+
if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
444
|
+
out(renderCommandHelp("codex"));
|
|
445
|
+
return 0;
|
|
446
|
+
}
|
|
447
|
+
if (subcommand === "doctor") {
|
|
448
|
+
const p = parse("codex", rest, {}, { positionals: 0 });
|
|
449
|
+
if (p.help) {
|
|
450
|
+
out(renderCommandHelp("codex"));
|
|
451
|
+
return 0;
|
|
452
|
+
}
|
|
453
|
+
const result = codexDoctor({ cwd, env });
|
|
454
|
+
if (p.values.json) out(JSON.stringify(result));
|
|
455
|
+
else {
|
|
456
|
+
for (const c of result.checks) out(`${c.ok ? "OK " : "ERR"} ${c.name}: ${c.detail}`);
|
|
457
|
+
out(result.ok ? "codex doctor: OK" : "codex doctor: problems found");
|
|
458
|
+
}
|
|
459
|
+
// 1 (generic diagnostic failure), not 5 — exit 5 is reserved for
|
|
460
|
+
// ContractError (a quest contract violation), which a doctor finding is not.
|
|
461
|
+
return result.ok ? 0 : 1;
|
|
462
|
+
}
|
|
463
|
+
if (subcommand === "install-agents") {
|
|
464
|
+
const p = parse("codex", rest, {
|
|
465
|
+
scope: { type: "string", default: "project" },
|
|
466
|
+
"dry-run": { type: "boolean" },
|
|
467
|
+
force: { type: "boolean" },
|
|
468
|
+
}, { positionals: 0 });
|
|
469
|
+
if (p.help) {
|
|
470
|
+
out(renderCommandHelp("codex"));
|
|
471
|
+
return 0;
|
|
472
|
+
}
|
|
473
|
+
if (!["project", "user"].includes(p.values.scope)) throw new UsageError(`--scope must be project or user (got "${p.values.scope}")`, { command: "codex" });
|
|
474
|
+
const isDry = Boolean(p.values["dry-run"]);
|
|
475
|
+
const result = installCodexAgents({
|
|
476
|
+
scope: p.values.scope,
|
|
477
|
+
dryRun: isDry,
|
|
478
|
+
force: Boolean(p.values.force),
|
|
479
|
+
cwd,
|
|
480
|
+
env,
|
|
481
|
+
});
|
|
482
|
+
// A real run refuses to overwrite; a --dry-run PREVIEWS the conflict
|
|
483
|
+
// (its whole purpose) rather than erroring before showing the plan.
|
|
484
|
+
if (!result.ok && !isDry) {
|
|
485
|
+
const conflicts = result.conflicts.map((c) => c.path).join(", ");
|
|
486
|
+
throw new UsageError(`agent file already exists; pass --force to replace: ${conflicts}`, { command: "codex" });
|
|
487
|
+
}
|
|
488
|
+
if (p.values.json) out(JSON.stringify(result));
|
|
489
|
+
else {
|
|
490
|
+
out(`${isDry ? "Would install" : "Installed"} Codex agents to ${result.target}`);
|
|
491
|
+
for (const a of result.actions) out(` ${a.action.padEnd(9)} ${a.path}`);
|
|
492
|
+
if (!result.ok) out(` conflicts present — pass --force to replace: ${result.conflicts.map((c) => c.path).join(", ")}`);
|
|
493
|
+
}
|
|
494
|
+
return 0;
|
|
495
|
+
}
|
|
496
|
+
throw new UsageError(`unknown codex subcommand "${subcommand}"`, { command: "codex" });
|
|
497
|
+
},
|
|
401
498
|
};
|