@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 +4 -3
- package/package.json +1 -1
- package/skills/code-review/SKILL.md +1 -1
- package/skills/condux-doctor/SKILL.md +64 -0
- package/skills/condux-doctor/doctor.mjs +425 -0
- package/skills/condux-doctor/evals/trigger_eval.json +17 -0
- package/skills/discovery/SKILL.md +1 -1
- package/skills/draft-plan/evals/trigger_eval.json +1 -1
- package/skills/live-verification/SKILL.md +1 -1
- package/skills/plan-review/SKILL.md +1 -1
- package/skills/preflight/SKILL.md +1 -1
- package/skills/subagent-deployment/SKILL.md +1 -1
- package/skills/subagent-execution/SKILL.md +7 -1
- package/skills/test-first-development/SKILL.md +1 -1
- package/skills/workflow/SKILL.md +1 -1
- package/skills/workflow/hooks/codex-hooks.json +26 -0
- package/skills/workflow/hooks/hooks.json +15 -0
- package/skills/workflow/hooks/routing.md +28 -0
- package/skills/workflow/hooks/session-start.mjs +60 -0
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
|
|
6
|
+
- **Skills** — bundles the 14 condux workflow skills (`workflow`, `discovery`,
|
|
7
7
|
`draft-plan`, `test-first-development`, `subagent-execution`,
|
|
8
|
-
`subagent-deployment`, `finalize`, `
|
|
9
|
-
`root-cause-analysis`, `plan-review`, `technical-spec
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
package/skills/workflow/SKILL.md
CHANGED
|
@@ -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.
|
|
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,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)));
|