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 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
- in native goal mode with deterministic budgets and notifications.
16
- - Five skills — `/quest:plan`, `/quest:work`, `/quest:orchestrate`,
17
- `/quest:retro`, `/quest:protocol`.
18
- - Two agents — `quest-executor`, `quest-reviewer`.
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: /quest:plan add dark mode to the settings page
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: /quest:orchestrate
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, agents, and hooks on top.
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 and discovers its skills from the
66
- repo checkout (`skills/`, surfaced under `.agents/` for Codex). Until a public
67
- Codex marketplace listing is approved upstream, wire it up from a clone or
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
- git clone https://github.com/robertsreberski/quest
72
- # add the checkout to your Codex plugin/skill discovery path (clone or submodule)
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
- /quest:work 12
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 run in **native goal mode** with a machine-verifiable completion
159
- condition.
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
- > `/quest:work` in-session instead.
233
+ > `$quest:work` in-session instead.
184
234
 
185
235
  ## Store backends
186
236
 
@@ -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 /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>
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 (`/quest:work`) exactly: smallest unfinished
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
- { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs\"" }
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
- { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs\"" }
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
- { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs\"" }
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
- { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/subagent-stop.mjs\"" }
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
  ]
@@ -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 contains a
7
- // `quest show <id> --json` invocation (the mandatory orientation marker). We take
8
- // the FIRST id. No marker not our concern, allow silently. We then compare the
9
- // quest's latest checkpoint against the subagent's start time (the transcript's
10
- // first timestamp): a checkpoint newer than start clears the stop; so does a
11
- // terminal store status (complete/blocked/cancelled) reached during the run.
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
- const MARKER = /quest\s+show\s+(\d+)\s+--json/;
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 m = MARKER.exec(transcript);
73
- if (!m) allow(); // not a quest-executor subagent — leave it entirely alone, silently
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
  };