@jabworks/condux 0.5.2 → 0.7.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/README.md CHANGED
@@ -3,10 +3,11 @@
3
3
  OpenCode plugin for the [condux](https://github.com/jabworks/agentic-toolkit)
4
4
  workflow toolkit. One plugin line installs everything condux needs:
5
5
 
6
- - **Skills** — bundles the 12 condux workflow skills (`workflow`, `discovery`,
6
+ - **Skills** — bundles the 14 condux workflow skills (`workflow`, `discovery`,
7
7
  `draft-plan`, `test-first-development`, `subagent-execution`,
8
- `subagent-deployment`, `finalize`, `code-review`, `preflight`,
9
- `root-cause-analysis`, `plan-review`, `technical-spec`) and registers them on
8
+ `subagent-deployment`, `finalize`, `live-verification`, `code-review`,
9
+ `preflight`, `root-cause-analysis`, `plan-review`, `technical-spec`,
10
+ `condux-doctor`) and registers them on
10
11
  `config.skills.paths` via the `config` hook — no separate `npx skills add`
11
12
  step. A skill you install yourself under the same name still wins; OpenCode
12
13
  dedupes by name.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jabworks/condux",
3
- "version": "0.5.2",
3
+ "version": "0.7.0",
4
4
  "description": "OpenCode plugin for the condux workflow toolkit — bundles the condux skills and injects the specialist agents (coder, explorer, planner, researcher) plus an opt-in plan-review listener.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: code-review
3
- description: "One-shot code review. Produces a diagnostic report categorized by severity (Critical / Important / Minor). Never fixes issues automatically. On-request only — never auto-triggers. At workflow CP-2 (pre-finalize, on request) or after /finalize, ask once: \"Want a code review before merging?\" Trigger phrases: \"review this\", \"review before merge\", \"check this PR\". Not for reviewing a plan or spec before implementation — that is plan-review. Not for running the change to watch it work — that is live-verification."
3
+ description: "One-shot code review. Produces a diagnostic report categorized by severity (Critical / Important / Minor). Never fixes issues automatically. On-request only — never auto-triggers. At workflow CP-2 or after /finalize, ask once: \"Want a code review before merging?\" Trigger phrases: \"review this\", \"review before merge\", \"check this PR\". Not for a plan or spec before implementation (plan-review); not for running the change to watch it work (live-verification)."
4
4
  argument-hint: "<PR URL, diff, branch, or file path>"
5
5
  ---
6
6
 
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: condux-doctor
3
+ description: "Health check for the condux plugin on the host it is installed on. Probes the SessionStart routing hook on Claude Code and Codex by running it and checking the wire format each host expects, confirms plan-review's Codex Stop hook resolves, checks the OpenCode npm registration and its bundled agents and skills, verifies the four specialist agent definitions shipped, and compares the installed version against the local marketplace clone. Offline and read-only. Checking whether condux itself works on this machine: is condux working, the routing rule stopped appearing, /workflow is not being reached, did the SessionStart hook fire, check my condux install, the specialist agents are missing. Run it after installing or updating the plugin. Not for routing a dev task (that is workflow); not for diagnosing this repo's own build or dist drift (that is toolkit-debugging-playbook)."
4
+ argument-hint: "[--host claude|codex|opencode]"
5
+ ---
6
+
7
+ # /condux-doctor
8
+
9
+ Answers one question: **is condux actually working on this host?**
10
+
11
+ ```bash
12
+ node <skill-base>/doctor.mjs # every host, every probe
13
+ node <skill-base>/doctor.mjs --host codex # one host
14
+ node <skill-base>/doctor.mjs --quiet # only what is not fine
15
+ ```
16
+
17
+ Exit 0 when nothing is broken, 1 when something is, 2 when the doctor cannot
18
+ find condux's own hooks.
19
+
20
+ ## Reading the report
21
+
22
+ One row per probe: `host status detail`, fix on an indented continuation
23
+ line. `done` works · `broken` needs the fix · `absent` is not there and not a
24
+ failure · `skipped` deliberately needs nothing. Only `broken` affects the
25
+ exit code.
26
+
27
+ ## What it probes
28
+
29
+ - **The SessionStart routing hook, by running it.** Claude Code must get a
30
+ `hookSpecificOutput` envelope carrying non-empty context; Codex must get
31
+ the raw routing payload. Each host's manifest must use its own root
32
+ variable — `${CLAUDE_PLUGIN_ROOT}` or `${PLUGIN_ROOT}`, never the other.
33
+ - **plan-review's Codex Stop hook** — resolved, never executed. It blocks a
34
+ turn for up to four days waiting on a human by design.
35
+ - **OpenCode** — `@jabworks/condux` in the plugin array, and when a local
36
+ copy exists, that it ships its bundled `agents/` and `skills/`.
37
+ - **The four specialist agents** — `coder`, `explorer`, `planner`,
38
+ `researcher`. They live in a plugin-level directory reached by its own sync
39
+ step, which is exactly the mirror that drifted once before.
40
+ - **Version** — installed against the local marketplace clone, with that
41
+ clone's own last-fetch date printed beside it. Never fetches.
42
+
43
+ ## Why running the hook matters
44
+
45
+ `session-start.mjs` fails open on purpose: if its payload is missing it exits
46
+ 0 and prints nothing, because a hook that breaks the session is worse than a
47
+ hook that does nothing. Every static check passes in that state. Only
48
+ executing it reveals that condux's routing rule is silently absent — which is
49
+ the difference between `/workflow` being the entry point and being a lucky
50
+ guess from the catalog.
51
+
52
+ ## What it cannot prove
53
+
54
+ That the host *invoked* the hook. Nothing readable from a child process says
55
+ whether Claude Code or Codex loaded a registration this session. If every
56
+ probe is green and the routing rule still never appears, the fault is
57
+ host-side and this report is the evidence.
58
+
59
+ ## Boundaries
60
+
61
+ Routing an actual dev task is `workflow`. Problems with this *repo's* build —
62
+ dist drift, a manifest that will not parse, a skill that will not trigger —
63
+ belong to `toolkit-debugging-playbook`: that one needs the source tree, this
64
+ one needs only the install.
@@ -0,0 +1,425 @@
1
+ #!/usr/bin/env node
2
+ // condux's health check — "is condux actually working on this host?"
3
+ //
4
+ // Follows the toolkit health-check convention (toolkit-skill-standards):
5
+ // detect → probe → report → fix. Probes are static parse PLUS execution, and
6
+ // condux is the case that needs it — session-start.mjs fails open by design,
7
+ // so a hook whose payload has gone missing exits 0 and says nothing. Static
8
+ // checks call that healthy; running it does not.
9
+
10
+ import fs from 'node:fs';
11
+ import os from 'node:os';
12
+ import path from 'node:path';
13
+ import process from 'node:process';
14
+ import { spawnSync } from 'node:child_process';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ const SKILL_BASE = path.dirname(fileURLToPath(import.meta.url));
18
+ const PLUGIN_ROOT = path.resolve(SKILL_BASE, '..', '..', '..');
19
+ const HOME = os.homedir();
20
+ const EXEC_TIMEOUT = 5000;
21
+ const PACKAGE = '@jabworks/condux';
22
+
23
+ const USAGE = `usage: doctor [options]
24
+
25
+ --host <claude|codex|opencode> probe one host only
26
+ --quiet print only broken and absent rows
27
+ `;
28
+
29
+ function parseFlags(argv) {
30
+ const flags = {};
31
+
32
+ for (let i = 0; i < argv.length; i++) {
33
+ if (!argv[i].startsWith('--')) continue;
34
+ const key = argv[i].slice(2);
35
+ const next = argv[i + 1];
36
+
37
+ if (next !== undefined && !next.startsWith('--')) {
38
+ flags[key] = next;
39
+ i++;
40
+ } else {
41
+ flags[key] = true;
42
+ }
43
+ }
44
+
45
+ return flags;
46
+ }
47
+
48
+ function firstExisting(candidates) {
49
+ return candidates.find((candidate) => candidate && fs.existsSync(candidate)) ?? null;
50
+ }
51
+
52
+ // The hooks directory is plugin-level in an install and skill-level in the
53
+ // source tree (workflow owns the canonical copy), so search both.
54
+ function findHook(file) {
55
+ return firstExisting([
56
+ path.join(PLUGIN_ROOT, 'hooks', file),
57
+ path.join(SKILL_BASE, '..', 'workflow', 'hooks', file),
58
+ ]);
59
+ }
60
+
61
+ function detectHosts() {
62
+ const configHome = process.env.XDG_CONFIG_HOME || path.join(HOME, '.config');
63
+ const dirs = {
64
+ claude: path.join(HOME, '.claude'),
65
+ codex: path.join(HOME, '.codex'),
66
+ opencode: path.join(configHome, 'opencode'),
67
+ };
68
+
69
+ return Object.fromEntries(
70
+ Object.entries(dirs).map(([host, dir]) => [host, fs.existsSync(dir) ? dir : null]),
71
+ );
72
+ }
73
+
74
+ function readJson(file) {
75
+ try {
76
+ return { value: JSON.parse(fs.readFileSync(file, 'utf8')) };
77
+ } catch (err) {
78
+ return { error: String(err?.message ?? err) };
79
+ }
80
+ }
81
+
82
+ // Pulls the first hook command for an event out of either host's manifest —
83
+ // the two dialects differ only in the root variable they interpolate.
84
+ function hookCommand(manifest, event) {
85
+ const entries = manifest?.hooks?.[event] ?? [];
86
+
87
+ for (const entry of entries) {
88
+ for (const hook of entry.hooks ?? []) {
89
+ if (typeof hook.command === 'string') return hook.command;
90
+ }
91
+ }
92
+
93
+ return null;
94
+ }
95
+
96
+ function resolveCommandPath(command, rootVariable) {
97
+ const match = command.match(/"([^"]+)"/);
98
+ const raw = match ? match[1] : command.split(/\s+/).slice(1)[0] ?? '';
99
+
100
+ return raw.replace(rootVariable, PLUGIN_ROOT);
101
+ }
102
+
103
+ function runSessionStart(script, hostFlag) {
104
+ const res = spawnSync(process.execPath, [script, hostFlag], {
105
+ timeout: EXEC_TIMEOUT,
106
+ encoding: 'utf8',
107
+ });
108
+
109
+ if (res.error) {
110
+ const why = res.error.code === 'ETIMEDOUT' ? 'no answer within 5s' : String(res.error.message);
111
+
112
+ return { ok: false, why };
113
+ }
114
+
115
+ const stdout = (res.stdout ?? '').trim();
116
+ if (stdout === '') {
117
+ // The documented fail-open path: routing.md missing or empty. Harmless to
118
+ // the session and invisible without this probe.
119
+ return { ok: false, why: 'hook produced no output — it is failing open, so routing.md is missing or empty' };
120
+ }
121
+
122
+ if (hostFlag === '--codex') {
123
+ return stdout.includes('/condux:workflow')
124
+ ? { ok: true, why: 'SessionStart emits the routing payload' }
125
+ : { ok: false, why: 'hook output does not carry the routing rule' };
126
+ }
127
+
128
+ try {
129
+ const payload = JSON.parse(stdout);
130
+ const context = payload?.hookSpecificOutput?.additionalContext ?? '';
131
+
132
+ if (payload?.hookSpecificOutput?.hookEventName !== 'SessionStart') {
133
+ return { ok: false, why: 'hook output is JSON but not a SessionStart envelope' };
134
+ }
135
+
136
+ return context.trim() === ''
137
+ ? { ok: false, why: 'SessionStart envelope carries an empty context' }
138
+ : { ok: true, why: 'SessionStart emits a valid envelope with the routing payload' };
139
+ } catch {
140
+ return { ok: false, why: 'hook output is not the JSON envelope Claude Code expects' };
141
+ }
142
+ }
143
+
144
+ // Hooks are registered by the plugin manifest, so outside a plugin install
145
+ // there is nothing registered to probe — and the root variables the manifest
146
+ // interpolates have no meaning there either.
147
+ const IS_PLUGIN = fs.existsSync(path.join(PLUGIN_ROOT, '.claude-plugin', 'plugin.json'));
148
+
149
+ function probeHookManifest(host, { file, event, rootVariable, foreignVariable, hostFlag }) {
150
+ if (!IS_PLUGIN) {
151
+ return { host, status: 'absent', detail: 'not a plugin install — this tree registers no hooks' };
152
+ }
153
+
154
+ const manifest = findHook(file);
155
+ if (!manifest) {
156
+ return { host, status: 'absent', detail: `not a plugin install — no hooks/${file}` };
157
+ }
158
+
159
+ const { value, error } = readJson(manifest);
160
+ if (error) {
161
+ return { host, status: 'broken', detail: `${file} is unparseable: ${error}`, fix: 'reinstall the plugin' };
162
+ }
163
+
164
+ const command = hookCommand(value, event);
165
+ if (!command) return { host, status: 'broken', detail: `${file} declares no ${event} hook`, fix: 'reinstall the plugin' };
166
+
167
+ if (command.includes(foreignVariable)) {
168
+ return {
169
+ host,
170
+ status: 'broken',
171
+ detail: `${event} uses ${foreignVariable}, which this host never expands`,
172
+ fix: `the manifest must use ${rootVariable} — reinstall the plugin`,
173
+ };
174
+ }
175
+
176
+ const script = resolveCommandPath(command, rootVariable);
177
+ if (!fs.existsSync(script)) {
178
+ return { host, status: 'broken', detail: `${event} points at a missing script: ${script}`, fix: 'reinstall the plugin' };
179
+ }
180
+
181
+ const verdict = runSessionStart(script, hostFlag);
182
+
183
+ return verdict.ok
184
+ ? { host, status: 'done', detail: verdict.why }
185
+ : { host, status: 'broken', detail: verdict.why, fix: 'reinstall the plugin — its routing payload did not ship' };
186
+ }
187
+
188
+ function probeClaude(hosts) {
189
+ if (!hosts.claude) return { host: 'claude', status: 'absent', detail: 'no ~/.claude on this machine' };
190
+
191
+ return probeHookManifest('claude', {
192
+ file: 'hooks.json',
193
+ event: 'SessionStart',
194
+ rootVariable: '${CLAUDE_PLUGIN_ROOT}',
195
+ foreignVariable: '${PLUGIN_ROOT}',
196
+ hostFlag: '--claude',
197
+ });
198
+ }
199
+
200
+ function probeCodex(hosts) {
201
+ if (!hosts.codex) return { host: 'codex', status: 'absent', detail: 'no ~/.codex on this machine' };
202
+
203
+ const row = probeHookManifest('codex', {
204
+ file: 'codex-hooks.json',
205
+ event: 'SessionStart',
206
+ rootVariable: '${PLUGIN_ROOT}',
207
+ foreignVariable: '${CLAUDE_PLUGIN_ROOT}',
208
+ hostFlag: '--codex',
209
+ });
210
+
211
+ if (row.status !== 'done') return row;
212
+
213
+ // plan-review's Stop hook blocks a turn for up to four days waiting on a
214
+ // human, so it is resolved and never executed.
215
+ const manifest = findHook('codex-hooks.json');
216
+ const stop = hookCommand(readJson(manifest).value, 'Stop');
217
+ if (!stop) return { ...row, detail: `${row.detail}; no Stop hook for plan-review` };
218
+
219
+ const server = resolveCommandPath(stop, '${PLUGIN_ROOT}');
220
+
221
+ return fs.existsSync(server)
222
+ ? { ...row, detail: `${row.detail}; plan-review Stop hook resolves` }
223
+ : {
224
+ host: 'codex',
225
+ status: 'broken',
226
+ detail: `SessionStart is fine but the Stop hook points at a missing annotate-server.js: ${server}`,
227
+ fix: 'reinstall the plugin',
228
+ };
229
+ }
230
+
231
+ function findPackage(configDir) {
232
+ return firstExisting([
233
+ path.join(configDir, 'node_modules', ...PACKAGE.split('/')),
234
+ path.join(HOME, '.local', 'share', 'opencode', 'node_modules', ...PACKAGE.split('/')),
235
+ path.join(HOME, '.cache', 'opencode', 'node_modules', ...PACKAGE.split('/')),
236
+ ]);
237
+ }
238
+
239
+ function probeOpencode(hosts) {
240
+ if (!hosts.opencode) return { host: 'opencode', status: 'absent', detail: 'no opencode config dir on this machine' };
241
+
242
+ const config = path.join(hosts.opencode, 'opencode.json');
243
+ const fix = `add ${PACKAGE} to the plugin array in ${config}`;
244
+ if (!fs.existsSync(config)) return { host: 'opencode', status: 'absent', detail: 'no opencode.json — condux is not registered', fix };
245
+
246
+ const { value, error } = readJson(config);
247
+ if (error) return { host: 'opencode', status: 'broken', detail: `opencode.json is unparseable: ${error}`, fix };
248
+
249
+ const registered = (value?.plugin ?? []).some((entry) => String(entry).startsWith(PACKAGE));
250
+ if (!registered) return { host: 'opencode', status: 'absent', detail: `${PACKAGE} is not in the plugin array`, fix };
251
+
252
+ const pkg = findPackage(hosts.opencode);
253
+ if (!pkg) {
254
+ return {
255
+ host: 'opencode',
256
+ status: 'absent',
257
+ detail: 'registered, but no local copy to verify — OpenCode fetches plugins at startup',
258
+ };
259
+ }
260
+
261
+ for (const dir of ['agents', 'skills']) {
262
+ const bundled = path.join(pkg, dir);
263
+ if (!fs.existsSync(bundled) || fs.readdirSync(bundled).length === 0) {
264
+ return { host: 'opencode', status: 'broken', detail: `the installed package ships no ${dir}/`, fix: `reinstall ${PACKAGE}` };
265
+ }
266
+ }
267
+
268
+ const check = spawnSync(process.execPath, ['--check', path.join(pkg, 'index.js')], { timeout: EXEC_TIMEOUT, encoding: 'utf8' });
269
+ if (check.status !== 0) {
270
+ return { host: 'opencode', status: 'broken', detail: 'the installed package entry point does not parse', fix: `reinstall ${PACKAGE}` };
271
+ }
272
+
273
+ return { host: 'opencode', status: 'done', detail: 'registered, and the installed package ships its agents and skills' };
274
+ }
275
+
276
+ // The four specialist agents are plugin-level and reached by their own sync
277
+ // step, not by the skill copy — the mirror that drifted in 6ba6572.
278
+ function probeAgents() {
279
+ const dir = firstExisting([
280
+ path.join(PLUGIN_ROOT, 'agents'),
281
+ path.join(SKILL_BASE, '..', 'subagent-execution', 'agents'),
282
+ ]);
283
+
284
+ if (!dir) return { host: 'all', status: 'absent', detail: 'no agents/ directory — not a plugin install' };
285
+
286
+ const agents = fs.readdirSync(dir).filter((entry) => entry.endsWith('.md'));
287
+ const expected = ['coder', 'explorer', 'planner', 'researcher'];
288
+ const missing = expected.filter((name) => !agents.includes(`${name}.md`));
289
+
290
+ return missing.length === 0
291
+ ? { host: 'all', status: 'done', detail: `all four specialist agents present (${dir === path.join(PLUGIN_ROOT, 'agents') ? 'plugin-level' : 'source tree'})` }
292
+ : { host: 'all', status: 'broken', detail: `missing agent definitions: ${missing.join(', ')}`, fix: 'reinstall the plugin' };
293
+ }
294
+
295
+ function compareVersions(a, b) {
296
+ const pa = String(a).split('.').map(Number);
297
+ const pb = String(b).split('.').map(Number);
298
+
299
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
300
+ const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
301
+ if (diff !== 0) return diff < 0 ? -1 : 1;
302
+ }
303
+
304
+ return 0;
305
+ }
306
+
307
+ // Offline by design: the clone the host already fetched is the only reference,
308
+ // and its age is printed so a stale answer reads as stale.
309
+ function marketplaceVersion(name) {
310
+ const root = path.join(HOME, '.claude', 'plugins', 'marketplaces');
311
+ if (!fs.existsSync(root)) return null;
312
+
313
+ for (const entry of fs.readdirSync(root)) {
314
+ const manifest = path.join(root, entry, '.claude-plugin', 'marketplace.json');
315
+ if (!fs.existsSync(manifest)) continue;
316
+
317
+ const { value } = readJson(manifest);
318
+ const plugin = (value?.plugins ?? []).find((candidate) => candidate.name === name);
319
+ if (!plugin) continue;
320
+
321
+ const clone = path.join(root, entry);
322
+ const log = spawnSync('git', ['-C', clone, 'log', '-1', '--format=%cs'], { timeout: EXEC_TIMEOUT, encoding: 'utf8' });
323
+ const fetched = log.status === 0 && log.stdout.trim()
324
+ ? log.stdout.trim()
325
+ : fs.statSync(manifest).mtime.toISOString().slice(0, 10);
326
+
327
+ const source = String(plugin.source ?? '');
328
+ const offered = source.startsWith('.')
329
+ ? readJson(path.join(clone, source, '.claude-plugin', 'plugin.json')).value?.version ?? null
330
+ : null;
331
+
332
+ return { version: offered, clone: entry, fetched, remote: !source.startsWith('.') };
333
+ }
334
+
335
+ return null;
336
+ }
337
+
338
+ function probeVersion() {
339
+ const manifest = path.join(PLUGIN_ROOT, '.claude-plugin', 'plugin.json');
340
+ if (!fs.existsSync(manifest)) {
341
+ return { host: 'all', status: 'absent', detail: 'not a plugin install — nothing to compare a version against' };
342
+ }
343
+
344
+ const installed = readJson(manifest).value?.version ?? null;
345
+ const market = marketplaceVersion('condux');
346
+
347
+ if (!installed) return { host: 'all', status: 'broken', detail: 'plugin.json carries no version', fix: 'reinstall the plugin' };
348
+ if (!market) return { host: 'all', status: 'absent', detail: `installed ${installed}; no local marketplace clone to compare against` };
349
+ if (!market.version) {
350
+ const why = market.remote ? 'its source is remote and this check never fetches' : 'its manifest carries no version';
351
+
352
+ return { host: 'all', status: 'absent', detail: `installed ${installed}; marketplace clone ${market.clone} offers no comparable version — ${why}` };
353
+ }
354
+
355
+ const order = compareVersions(installed, market.version);
356
+ const stamp = `marketplace clone ${market.clone} as of ${market.fetched}`;
357
+
358
+ if (order < 0) {
359
+ return {
360
+ host: 'all',
361
+ status: 'broken',
362
+ detail: `installed ${installed}, marketplace offers ${market.version} (${stamp})`,
363
+ fix: '/plugin update condux@jabworks-agentic-toolkit',
364
+ };
365
+ }
366
+
367
+ const lead = order > 0 ? ` (ahead of marketplace ${market.version} — local build)` : '';
368
+
369
+ return { host: 'all', status: 'done', detail: `installed ${installed}${lead}; ${stamp}` };
370
+ }
371
+
372
+ const WIDTH_HOST = 10;
373
+ const WIDTH_STATUS = 8;
374
+
375
+ function report(rows, quiet) {
376
+ for (const row of rows) {
377
+ if (quiet && row.status === 'done') continue;
378
+ process.stdout.write(
379
+ row.host.padEnd(WIDTH_HOST) + ' ' + row.status.padEnd(WIDTH_STATUS) + ' ' + row.detail + '\n',
380
+ );
381
+ if (row.fix) {
382
+ process.stdout.write(' '.repeat(WIDTH_HOST + WIDTH_STATUS + 2) + '↳ ' + row.fix + '\n');
383
+ }
384
+ }
385
+ }
386
+
387
+ function summarize(rows) {
388
+ const broken = rows.filter((row) => row.status === 'broken').length;
389
+ const verdict = broken === 0 ? 'condux is healthy on every host present' : `${broken} probe(s) broken`;
390
+
391
+ return `\n${verdict} — probes are static-parse plus execution; they cannot prove the host invoked anything.\n`;
392
+ }
393
+
394
+ function main(argv) {
395
+ const flags = parseFlags(argv);
396
+ if (flags.help) {
397
+ process.stdout.write(USAGE);
398
+
399
+ return 0;
400
+ }
401
+
402
+ if (!findHook('session-start.mjs')) {
403
+ report([{ host: 'all', status: 'broken', detail: `condux's hooks are not reachable from ${SKILL_BASE}` }], false);
404
+
405
+ return 2;
406
+ }
407
+
408
+ const hosts = detectHosts();
409
+ const only = typeof flags.host === 'string' ? flags.host : null;
410
+ const perHost = [
411
+ ['claude', () => probeClaude(hosts)],
412
+ ['codex', () => probeCodex(hosts)],
413
+ ['opencode', () => probeOpencode(hosts)],
414
+ ];
415
+
416
+ const rows = perHost.filter(([host]) => !only || host === only).map(([, probe]) => probe());
417
+ if (!only) rows.push(probeAgents(), probeVersion());
418
+
419
+ report(rows, flags.quiet === true);
420
+ process.stdout.write(summarize(rows));
421
+
422
+ return rows.some((row) => row.status === 'broken') ? 1 : 0;
423
+ }
424
+
425
+ process.exit(main(process.argv.slice(2)));
@@ -0,0 +1,17 @@
1
+ [
2
+ { "query": "is condux actually working on this machine?", "should_trigger": true, "expected_skill": "condux-doctor" },
3
+ { "query": "check my condux install", "should_trigger": true, "expected_skill": "condux-doctor" },
4
+ { "query": "the routing rule stopped showing up at session start", "should_trigger": true, "expected_skill": "condux-doctor" },
5
+ { "query": "did the condux SessionStart hook actually fire?", "should_trigger": true, "expected_skill": "condux-doctor" },
6
+ { "query": "the specialist agents are missing after I updated the plugin", "should_trigger": true, "expected_skill": "condux-doctor" },
7
+ { "query": "run a health check on condux", "should_trigger": true, "expected_skill": "condux-doctor" },
8
+ { "query": "verify condux registered properly on codex", "should_trigger": true, "expected_skill": "condux-doctor" },
9
+ { "query": "add a search filter to the invoice table", "should_trigger": true, "expected_skill": "workflow" },
10
+ { "query": "how do condux tiers work?", "should_trigger": true, "expected_skill": "workflow" },
11
+ { "query": "why isn't my skill triggering", "should_trigger": true, "expected_skill": "toolkit-debugging-playbook" },
12
+ { "query": "dist drifted from skills after my edit", "should_trigger": true, "expected_skill": "toolkit-debugging-playbook" },
13
+ { "query": "what should I work on next?", "should_trigger": true, "expected_skill": "groom" },
14
+ { "query": "what is a git hook?", "should_trigger": false, "expected_skill": null },
15
+ { "query": "my laptop fan is loud when node runs", "should_trigger": false, "expected_skill": null },
16
+ { "query": "check the health of the kubernetes cluster", "should_trigger": false, "expected_skill": null }
17
+ ]
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: discovery
3
- description: "Refine a rough idea into a signed-off design. Asks goal-level questions, surfaces alternatives, then runs a post-approach detail round (contracts, mappings, edge cases) that feeds the tech spec (written via technical-spec at sign-off); presents the design in sections for sign-off before any planning or code begins. Trigger for LARGE tasks, when scope is unclear, or when the user wants to brainstorm or explore a rough idea. Also when resuming or continuing an existing design or design doc — the existing-design check offers resume-or-fresh. Acts as a soft gate before /draft-plan — if discovery hasn't run, ask the user if they want to skip it consciously."
3
+ description: "Refine a rough idea into a signed-off design, presented in sections for sign-off before any planning or code begins. Feeds the tech spec, written via technical-spec at sign-off. Trigger for LARGE tasks, when scope is unclear, or when the user wants to brainstorm or explore a rough idea. Also when resuming an existing design or design doc — the existing-design check offers resume-or-fresh. A soft gate before /draft-plan: if discovery has not run, ask whether to skip it consciously."
4
4
  argument-hint: "<rough idea or feature description>"
5
5
  effort: high
6
6
  ---
@@ -16,7 +16,7 @@
16
16
  { "query": "review the plan in the browser before implementing", "should_trigger": true, "expected_skill": "plan-review" },
17
17
  { "query": "execute the plan with agents", "should_trigger": true, "expected_skill": "subagent-execution" },
18
18
  { "query": "design isnt agreed yet, help me align first", "should_trigger": true, "expected_skill": "discovery" },
19
- { "query": "add a task to my todo list", "should_trigger": false, "expected_skill": null },
19
+ { "query": "add a task to my todo list", "should_trigger": false, "expected_skill": null, "accept": ["record"] },
20
20
  { "query": "plan my sprint for next week", "should_trigger": false, "expected_skill": null },
21
21
  { "query": "make a project timeline gantt chart", "should_trigger": false, "expected_skill": null }
22
22
  ]
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: live-verification
3
- description: "Verify a change by actually running it — drive the real UI or endpoint, check each claim against observed behaviour, and capture evidence before pushing. Detects what's runnable, checks light mode then dark, and reports claim → evidence → verdict with unverifiable claims named rather than hidden. After /finalize, before committing, pushing, or merging a change that has a runnable surface — UI, page, component, endpoint, CLI. Triggers include \"verify it live\", \"did this actually work\", \"check it in the browser\", \"verify on preview\", \"check the preview env then merge\", \"live verify before pushing\", \"does it render right\". Not for typecheck/lint/test (that's finalize), not for auditing plan completeness (preflight), not for reading code to judge it (code-review)."
3
+ description: "Verify a change by actually running it — drive the real UI or endpoint, check each claim against observed behaviour, and capture evidence before pushing. After /finalize, before committing, pushing, or merging a change with a runnable surface — UI, page, component, endpoint, CLI. Triggers include \"verify it live\", \"did this actually work\", \"check it in the browser\", \"verify on preview\", \"live verify before pushing\", \"does it render right\". Not for typecheck/lint/test (finalize), auditing plan completeness (preflight), or reading code to judge it (code-review)."
4
4
  argument-hint: "[what to verify — optional; defaults to the current diff]"
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: plan-review
3
- description: "Render a plan in a local browser for inline annotation, then return an approve / request-revisions / deny decision to the agent. Auto-captures the plan via a Claude Code ExitPlanMode hook or a Codex Stop hook, or runs manually on any markdown file or spec directory. Self-contained — no network egress, no third-party runtime deps. When the agent finishes planning (Claude Code ExitPlanMode, or a Codex planning turn) and you want to review/annotate before it implements, or when you invoke /plan-review on a markdown plan or spec directory. Also owns its own machinery — steer-mode review loops, the live spec preview, and installing or troubleshooting the ExitPlanMode / Codex Stop hooks. Phrases include \"let's review the plan\", \"walk me through the plan before we build\", \"annotate the plan\" — use these when the ExitPlanMode or Codex Stop hook doesn't fire. Not for cataloging a whole specs tree; use spec-browser. Not for the implemented diff; use code-review."
3
+ description: "Render a plan in a local browser for inline annotation, then return an approve / request-revisions / deny decision to the agent. Auto-captured via a Claude Code ExitPlanMode or Codex Stop hook, or run manually on a markdown file or spec directory. When the agent finishes planning and you want to review or annotate before it implements, or on /plan-review for a markdown plan or spec directory. Also owns its machinery — steer-mode loops, the live spec preview, and installing or troubleshooting the ExitPlanMode / Codex Stop hooks. Phrases: \"let's review the plan\", \"walk me through the plan\", \"annotate the plan\". Not for cataloging a whole specs tree (spec-browser); not for the implemented diff (code-review)."
4
4
  ---
5
5
 
6
6
  # /plan-review
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: preflight
3
- description: "Lightweight \"am I actually done?\" checklist before calling /finalize. Prevents premature completion — catches skipped steps, missing behavior, and silent regressions. Runs before /finalize at the end of every /workflow tier (automatic on SMALL; the recommended CP-2 choice on MEDIUM/LARGE). Also trigger standalone on \"verify this\", \"am I done?\", or \"preflight\". Not for executing typecheck, lint, format, and tests; use finalize. Not for actually running the change and observing it work in a browser or CLI; that's live-verification, after finalize."
3
+ description: "Lightweight \"am I actually done?\" checklist before calling /finalize. Prevents premature completion — catches skipped steps, missing behavior, and silent regressions. Runs before /finalize at the end of every /workflow tier (automatic on SMALL; the recommended CP-2 choice on MEDIUM/LARGE). Also standalone on \"verify this\", \"am I done?\", \"preflight\". Not for executing typecheck, lint, format, and tests (finalize); not for running the change and observing it work (live-verification, after finalize)."
4
4
  argument-hint: "[task or feature name]"
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: subagent-deployment
3
- description: "Fan out 2+ independent tasks across named agents (explorer/researcher/planner/coder) in a single message when they share no files and no dependencies. Use for ad-hoc independent work discovered outside a formal plan — unrelated bug fixes, parallel lookups, independent small tasks. Trigger when two or more genuinely independent tasks are on the table — nothing shared between them, no ordering — and any mix of named agents fits. Not for executing an ordered plan task-by-task (that's subagent-execution)."
3
+ description: "Fan out 2+ independent tasks across named agents (explorer/researcher/planner/coder) in a single message when they share no files and no dependencies. For ad-hoc independent work discovered outside a formal plan. Trigger when two or more genuinely independent tasks are on the table — nothing shared between them, no ordering — and any mix of named agents fits. Not for executing an ordered plan task-by-task (that's subagent-execution)."
4
4
  argument-hint: "<list of independent tasks>"
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: subagent-execution
3
- description: "Execute a plan using named specialist agents. Default is to implement yourself — only spawn when there is a concrete justification. Agents must be pre-defined; never create generic subagents with injected system prompts. Trigger when implementing a LARGE plan with tasks that benefit from agent specialization or parallel exploration — including task briefs, review packages, spawn rules, and resuming an interrupted plan from the `.condux/progress/` ledger (not session-handoff). Not for ad-hoc independent tasks outside a plan (that's subagent-deployment)."
3
+ description: "Execute a plan using named specialist agents. Default is to implement yourself — only spawn when there is a concrete justification. Trigger when implementing a LARGE plan with tasks that benefit from agent specialization or parallel exploration — including task briefs, review packages, spawn rules, and resuming an interrupted plan from the `.condux/progress/` ledger (not session-handoff). Not for ad-hoc independent tasks outside a plan (that's subagent-deployment)."
4
4
  argument-hint: "<plan file path>"
5
5
  ---
6
6
 
@@ -16,6 +16,12 @@ Execute a plan using specialist agents. Default is to do the work yourself. Spaw
16
16
 
17
17
  ## Core Principle
18
18
 
19
+ **Agents must be pre-defined.** Spawn only the named agents that ship with condux
20
+ (`explorer`, `researcher`, `planner`, `coder`). Never invent a generic subagent by
21
+ injecting a system prompt into a general-purpose one — an agent whose behaviour is
22
+ written at call time has no reviewable contract, no consistent tool restrictions,
23
+ and no way to improve across sessions.
24
+
19
25
  **You implement by default.** Spawning is an explicit decision with a stated reason — not the default mode of operation.
20
26
 
21
27
  ```
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: test-first-development
3
- description: "Tests-first development — write tests before implementation. Opt-in only. Get one upfront consent (write tests first + auto-run), then run the cycle without re-asking. Still always ask before updating existing test specs. Never silently rewrite test specs to match new behavior. Trigger when the user explicitly asks for tests-first, picks it at a workflow checkpoint, or when implementing logic with clear input/output behavior. Also triggers whenever an existing test spec is about to be edited to make it pass — that decision runs through this skill."
3
+ description: "Tests-first development — write tests before implementation. Opt-in only: one upfront consent (write tests first + auto-run), then run the cycle without re-asking. Trigger when the user explicitly asks for tests-first, picks it at a workflow checkpoint, or when implementing logic with clear input/output behavior. Also triggers whenever an existing test spec is about to be edited to make it pass — that decision runs through this skill."
4
4
  argument-hint: "<what to test>"
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: workflow
3
- description: "Routes any dev task into the right execution tier (Small/Medium/Large) and carries condux's operating rules — proportional effort, lazy skill loading, soft gates, implement-yourself-by-default. Confirms the tier with the user, then executes the matching flow, loading downstream skills only when a step needs them. Trigger with any implementation request — feature, bug fix, refactor, new endpoint, UI change. Every dev task starts here; other skills execute within /workflow, not instead of it. Also the operating manual — questions about how condux works, its tiers, soft gates, checkpoints, and named agents."
3
+ description: "Routes any dev task into the right execution tier (Small/Medium/Large) and carries condux's operating rules — proportional effort, lazy skill loading, soft gates, implement-yourself-by-default. Trigger with any implementation request — feature, bug fix, refactor, new endpoint, UI change. Every dev task starts here; other skills execute within /workflow, not instead of it. Also the operating manual — questions about how condux works, its tiers, soft gates, checkpoints, and named agents."
4
4
  argument-hint: "<task description>"
5
5
  ---
6
6
 
@@ -0,0 +1,26 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "node \"${PLUGIN_ROOT}/hooks/session-start.mjs\" --codex",
9
+ "additionalContextLimit": 4000
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "Stop": [
15
+ {
16
+ "hooks": [
17
+ {
18
+ "type": "command",
19
+ "command": "node \"${PLUGIN_ROOT}/skills/condux/plan-review/references/annotate-server.js\" --codex-stop",
20
+ "timeout": 345600
21
+ }
22
+ ]
23
+ }
24
+ ]
25
+ }
26
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "startup|clear|compact",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs\" --claude"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,28 @@
1
+ <EXTREMELY_IMPORTANT>
2
+ You have condux — this session's agentic workflow system.
3
+
4
+ **Every implementation request starts at `/condux:workflow`.** Feature, bug fix,
5
+ refactor, new endpoint, UI change, "can you add…", "can you fix…" — all of it
6
+ routes there first. The router infers the tier (Small / Medium / Large), confirms
7
+ it with the user, then executes the matching flow, loading downstream skills only
8
+ when a step needs them.
9
+
10
+ **These skills execute _within_ workflow, never instead of it:** `discovery`,
11
+ `draft-plan`, `test-first-development`, `subagent-execution`,
12
+ `subagent-deployment`, `preflight`, `finalize`, `code-review`,
13
+ `root-cause-analysis`, `live-verification`, `plan-review`, `technical-spec`.
14
+
15
+ If you find yourself reaching for one of those as the *first* skill of a task,
16
+ you have skipped the router — go back to `/condux:workflow`. Debugging is the
17
+ most common miss: a crash report, a stack trace, or "why does this fail" is
18
+ still a dev task, so it routes through workflow, which loads
19
+ `root-cause-analysis` at the right moment.
20
+
21
+ **Not a valid bypass:** your own judgment that a task "seems small". Tier
22
+ inference is the router's job, not yours. The only valid bypasses are explicit
23
+ user instructions — "skip workflow", "just do it", "I've already planned this".
24
+
25
+ **Not everything is a dev task.** Questions, explanations, code reading, research,
26
+ and one-line factual answers route nowhere — answer them directly. Routing a
27
+ conversation through a workflow is as wrong as skipping the router on a feature.
28
+ </EXTREMELY_IMPORTANT>
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ // SessionStart hook: puts condux's routing rule in context before the first
3
+ // prompt, so /workflow is reached as the entry point rather than inferred from
4
+ // the skill catalog.
5
+ //
6
+ // Why this exists: the trigger evals (scripts/eval-triggers.mjs, reports in
7
+ // skills/toolkit-research-frontier/references/) put workflow at ~26-27/33 on
8
+ // catalog inference alone. The misses are not silence — they are condux's own
9
+ // siblings winning the query (root-cause-analysis on a crash report, draft-plan
10
+ // on "write the plan", code-review on "fix the findings"). That collision cannot
11
+ // be fixed by strengthening workflow's description without stealing trigger
12
+ // space from those siblings, which toolkit-skill-standards forbids. An
13
+ // always-in-context instruction outranks catalog inference, so the rule goes
14
+ // here instead.
15
+ //
16
+ // The payload is routing.md, edited as prose — never inlined here.
17
+ //
18
+ // Host contracts differ, so the host is passed explicitly rather than sniffed
19
+ // from the environment:
20
+ // --claude Claude Code wants a JSON envelope on stdout
21
+ // --codex Codex injects raw stdout (bounded by additionalContextLimit)
22
+
23
+ import fs from 'node:fs';
24
+ import path from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
28
+
29
+ // Resolved relative to this file, not to a plugin-root env var: the script and
30
+ // its payload ship together, so they move together.
31
+ const PAYLOAD = path.join(HERE, 'routing.md');
32
+
33
+ function main(argv) {
34
+ let text;
35
+ try {
36
+ text = fs.readFileSync(PAYLOAD, 'utf8').trim();
37
+ } catch {
38
+ // A hook that breaks the session is worse than a hook that does nothing.
39
+ // Exit clean and let the skill catalog do its ~80% job.
40
+ return 0;
41
+ }
42
+ if (text === '') return 0;
43
+
44
+ if (argv.includes('--codex')) {
45
+ process.stdout.write(text + '\n');
46
+ return 0;
47
+ }
48
+
49
+ // Default to Claude Code's envelope. JSON.stringify handles the escaping that
50
+ // shell-based versions of this hook get wrong.
51
+ process.stdout.write(JSON.stringify({
52
+ hookSpecificOutput: {
53
+ hookEventName: 'SessionStart',
54
+ additionalContext: text,
55
+ },
56
+ }) + '\n');
57
+ return 0;
58
+ }
59
+
60
+ process.exit(main(process.argv.slice(2)));