selftune 0.2.20 → 0.2.22

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.
@@ -6,9 +6,9 @@
6
6
  * modules can reuse the same calling logic.
7
7
  */
8
8
 
9
- import { readFileSync, writeFileSync } from "node:fs";
9
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
10
10
  import { tmpdir } from "node:os";
11
- import { join } from "node:path";
11
+ import { dirname, join, resolve } from "node:path";
12
12
 
13
13
  import { AGENT_CANDIDATES } from "../constants.js";
14
14
  import { createLogger } from "./logging.js";
@@ -33,6 +33,40 @@ function resolveModelFlag(flag: string): string {
33
33
  return CLAUDE_MODEL_ALIASES[flag] ?? flag;
34
34
  }
35
35
 
36
+ /**
37
+ * Map selftune model aliases to OpenCode provider/model format.
38
+ * OpenCode uses "provider/model" syntax (e.g. "anthropic/claude-sonnet-4-20250514").
39
+ */
40
+ const OPENCODE_MODEL_MAP: Record<string, string> = {
41
+ haiku: "anthropic/claude-haiku-4-5-20251001",
42
+ sonnet: "anthropic/claude-sonnet-4-20250514",
43
+ opus: "anthropic/claude-opus-4-20250514",
44
+ };
45
+
46
+ /** Resolve a model alias to OpenCode's provider/model format. */
47
+ function resolveOpenCodeModel(flag: string): string {
48
+ return OPENCODE_MODEL_MAP[flag] ?? flag;
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Bundled agent file loading (for codex inline prompt injection)
53
+ // ---------------------------------------------------------------------------
54
+
55
+ const BUNDLED_AGENT_DIR = resolve(dirname(import.meta.path), "..", "..", "..", "skill", "agents");
56
+
57
+ /**
58
+ * Read the bundled agent markdown file and return its body (without frontmatter).
59
+ * Used by codex path to inline agent instructions into the prompt since codex
60
+ * has no --agent flag.
61
+ */
62
+ function loadAgentInstructions(agentName: string): string | null {
63
+ const filePath = join(BUNDLED_AGENT_DIR, `${agentName}.md`);
64
+ if (!existsSync(filePath)) return null;
65
+ const content = readFileSync(filePath, "utf-8");
66
+ // Strip YAML frontmatter
67
+ return content.replace(/^---\n[\s\S]*?\n---\n*/, "").trim();
68
+ }
69
+
36
70
  // ---------------------------------------------------------------------------
37
71
  // Agent detection
38
72
  // ---------------------------------------------------------------------------
@@ -155,7 +189,11 @@ export async function callViaAgent(
155
189
  } else if (agent === "codex") {
156
190
  cmd = ["codex", "exec", "--skip-git-repo-check", promptContent];
157
191
  } else if (agent === "opencode") {
158
- cmd = ["opencode", "-p", promptContent, "-f", "text", "-q"];
192
+ cmd = ["opencode", "run"];
193
+ if (modelFlag) {
194
+ cmd.push("--model", resolveOpenCodeModel(modelFlag));
195
+ }
196
+ cmd.push(promptContent);
159
197
  } else {
160
198
  throw new Error(`Unknown agent: ${agent}`);
161
199
  }
@@ -222,9 +260,9 @@ export async function callViaAgent(
222
260
  // Call LLM via named subagent (multi-turn, agentic)
223
261
  // ---------------------------------------------------------------------------
224
262
 
225
- /** Options for calling a named Claude Code subagent. */
263
+ /** Options for calling a named subagent (Claude Code or OpenCode). */
226
264
  export interface SubagentCallOptions {
227
- /** Name of the subagent (synced into ~/.claude/agents/ by selftune init/update). */
265
+ /** Name of the subagent (synced into ~/.claude/agents/ or opencode.json by selftune init/update). */
228
266
  agentName: string;
229
267
  /** The task prompt for the subagent. */
230
268
  prompt: string;
@@ -243,13 +281,13 @@ export interface SubagentCallOptions {
243
281
  }
244
282
 
245
283
  /**
246
- * Call a named Claude Code subagent in print mode. The subagent runs its
247
- * multi-turn workflow (reading files, running commands, etc.) and returns
248
- * the final text output.
284
+ * Call a named subagent in print mode. The subagent runs its multi-turn
285
+ * workflow (reading files, running commands, etc.) and returns the final
286
+ * text output.
249
287
  *
250
- * Unlike callViaAgent(), this does NOT use --bare (agents need discovery)
251
- * and passes --agent + --max-turns for agentic multi-turn behavior.
252
- * Only supports the claude CLI.
288
+ * Supports Claude Code (`claude --agent`), OpenCode (`opencode run --agent`),
289
+ * and Codex (`codex exec` with agent instructions inlined into the prompt).
290
+ * Auto-detects the available agent CLI.
253
291
  */
254
292
  export async function callViaSubagent(options: SubagentCallOptions): Promise<string> {
255
293
  const {
@@ -263,31 +301,58 @@ export async function callViaSubagent(options: SubagentCallOptions): Promise<str
263
301
  allowedTools,
264
302
  } = options;
265
303
 
266
- const cmd: string[] = [
267
- "claude",
268
- "-p",
269
- prompt,
270
- "--agent",
271
- agentName,
272
- "--max-turns",
273
- String(maxTurns),
274
- ];
275
-
276
- if (appendSystemPrompt) {
277
- cmd.push("--append-system-prompt", appendSystemPrompt);
278
- }
279
- if (modelFlag) {
280
- const resolved = resolveModelFlag(modelFlag);
281
- cmd.push("--model", resolved);
304
+ const agent = detectAgent();
305
+ if (!agent || (agent !== "claude" && agent !== "opencode" && agent !== "codex")) {
306
+ throw new Error(
307
+ `Subagent calls require 'claude', 'opencode', or 'codex' CLI in PATH (detected: ${agent ?? "none"})`,
308
+ );
282
309
  }
283
- if (effort) {
284
- cmd.push("--effort", effort);
285
- }
286
- if (allowedTools && allowedTools.length > 0) {
287
- cmd.push("--allowedTools", ...allowedTools);
310
+
311
+ let cmd: string[];
312
+
313
+ if (agent === "opencode") {
314
+ // OpenCode supports --agent and --model but not allowedTools, appendSystemPrompt, or maxTurns
315
+ if (allowedTools?.length || appendSystemPrompt) {
316
+ logger.warn(
317
+ `Subagent '${agentName}' on opencode: allowedTools and appendSystemPrompt are not supported and will be ignored`,
318
+ );
319
+ }
320
+ cmd = ["opencode", "run", "--agent", agentName];
321
+ if (modelFlag) {
322
+ cmd.push("--model", resolveOpenCodeModel(modelFlag));
323
+ }
324
+ cmd.push(prompt);
325
+ } else if (agent === "codex") {
326
+ // Codex has no --agent flag; inline the agent instructions into the prompt.
327
+ // allowedTools, appendSystemPrompt, maxTurns, and effort are not supported.
328
+ if (allowedTools?.length || appendSystemPrompt) {
329
+ logger.warn(
330
+ `Subagent '${agentName}' on codex: allowedTools and appendSystemPrompt are not supported and will be ignored`,
331
+ );
332
+ }
333
+ const agentInstructions = loadAgentInstructions(agentName);
334
+ const fullPrompt = agentInstructions ? `${agentInstructions}\n\n---\n\n${prompt}` : prompt;
335
+ cmd = ["codex", "exec", "--skip-git-repo-check", fullPrompt];
336
+ } else {
337
+ // Claude Code
338
+ cmd = ["claude", "-p", prompt, "--agent", agentName, "--max-turns", String(maxTurns)];
339
+
340
+ if (appendSystemPrompt) {
341
+ cmd.push("--append-system-prompt", appendSystemPrompt);
342
+ }
343
+ if (modelFlag) {
344
+ const resolved = resolveModelFlag(modelFlag);
345
+ cmd.push("--model", resolved);
346
+ }
347
+ if (effort) {
348
+ cmd.push("--effort", effort);
349
+ }
350
+ if (allowedTools && allowedTools.length > 0) {
351
+ cmd.push("--allowedTools", ...allowedTools);
352
+ }
353
+ // Skip permissions since this runs non-interactively in a pipeline
354
+ cmd.push("--dangerously-skip-permissions");
288
355
  }
289
- // Skip permissions since this runs non-interactively in a pipeline
290
- cmd.push("--dangerously-skip-permissions");
291
356
 
292
357
  const maxRetries = retryOpts?.maxRetries ?? DEFAULT_MAX_RETRIES;
293
358
  const initialBackoffMs = retryOpts?.initialBackoffMs ?? DEFAULT_INITIAL_BACKOFF_MS;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "selftune",
3
- "version": "0.2.20",
3
+ "version": "0.2.22",
4
4
  "description": "Self-improving skills CLI for AI agents",
5
5
  "keywords": [
6
6
  "agent",
package/skill/SKILL.md CHANGED
@@ -125,6 +125,14 @@ selftune uninstall [--dry-run] [--keep-logs] [--npm-uninstall]
125
125
  # Hook dispatch (for debugging/manual invocation)
126
126
  selftune hook <name> # prompt-log | session-stop | skill-eval | auto-activate | skill-change-guard | evolution-guard
127
127
 
128
+ # Platform hooks (non-Claude-Code agents)
129
+ selftune codex hook
130
+ selftune codex install [--dry-run] [--uninstall]
131
+ selftune opencode hook
132
+ selftune opencode install [--dry-run] [--uninstall]
133
+ selftune cline hook
134
+ selftune cline install [--dry-run] [--uninstall]
135
+
128
136
  # Alpha enrollment (device-code flow — browser opens automatically)
129
137
  selftune init --alpha --alpha-email <email>
130
138
  selftune alpha upload [--dry-run]
@@ -169,6 +177,7 @@ selftune status # shows c
169
177
  | repair, rebuild usage, fix skill usage, trustworthy usage, repair-skill-usage | RepairSkillUsage | Workflows/RepairSkillUsage.md |
170
178
  | export canonical, canonical export, canonical telemetry, push payload | ExportCanonical | Workflows/ExportCanonical.md |
171
179
  | hook, run hook, invoke hook, manual hook, debug hook | Hook | Workflows/Hook.md |
180
+ | codex hooks, codex install, codex setup, opencode hooks, opencode install, opencode setup, cline hooks, cline install, cline setup, multi-platform, platform hooks, non-claude hooks, multiple agents, multi-agent | PlatformHooks | Workflows/PlatformHooks.md |
172
181
  | export, dump, jsonl, export sqlite, debug export | Export | _(direct command — no workflow file)_ |
173
182
  | status, health summary, skill health, how are skills, skills doing, run selftune | Status | _(direct command — no workflow file)_ |
174
183
  | last, last session, recent session, what happened, what changed | Last | _(direct command — no workflow file)_ |
@@ -357,6 +366,7 @@ accomplish a task _using_ a skill, route to that skill instead.
357
366
  | `Workflows/CreatorContributions.md` | Manage bundled `selftune.contribute.json` configs | When preparing a skill package for creator contributions |
358
367
  | `Workflows/ExportCanonical.md` | Export canonical telemetry for downstream use | When exporting data for external consumption |
359
368
  | `Workflows/Hook.md` | Manual hook invocation for debugging | When debugging or testing hooks manually |
369
+ | `Workflows/PlatformHooks.md` | Non-Claude-Code platform hook install/config | When setting up Codex, OpenCode, or Cline hooks |
360
370
  | `references/logs.md` | Log file formats (telemetry, usage, queries, audit) | When parsing or debugging log files |
361
371
  | `references/grading-methodology.md` | 3-tier grading model, evidence standards | When grading sessions or interpreting grades |
362
372
  | `references/invocation-taxonomy.md` | 4 invocation types, coverage analysis | When analyzing trigger coverage |
@@ -89,15 +89,31 @@ skills in the same registry, so replay-backed validation is preferred whenever
89
89
  that local fixture can be constructed because it captures host-style routing
90
90
  behavior instead of model judgment.
91
91
 
92
- The current replay path is fixture-backed: it evaluates the target routing table
93
- against the installed target/competing skill surfaces in a controlled replay
94
- fixture and records per-entry evidence. That is still a stronger signal than a
95
- free-form judge prompt, but you should describe it as replay-backed validation,
96
- not as live operator telemetry.
92
+ For Claude Code, the replay path now stages a temporary project-local
93
+ `.claude/skills` registry, swaps in the candidate routing table, and runs a
94
+ one-turn Claude print-mode session with project/local settings only. Validation
95
+ records whether Claude actually invoked the target skill, invoked a competing
96
+ skill, invoked an unrelated skill, or made no routing decision at all.
97
+ Unrelated skill use is treated as a replay failure even on negative evals,
98
+ because it still indicates the runtime routed somewhere unexpected. If that
99
+ runtime path is unavailable or fails to reach a runtime decision, selftune
100
+ falls back to the existing fixture-backed surface simulation and notes the
101
+ fallback in the replay evidence instead of pretending it was a runtime result.
102
+
103
+ For non-Claude platforms today, replay remains fixture-backed: it evaluates the
104
+ target routing table against the installed target/competing skill surfaces in a
105
+ controlled replay fixture and records per-entry evidence. That is still a
106
+ stronger signal than a free-form judge prompt, but you should describe it as
107
+ replay-backed validation, not as live operator telemetry.
97
108
 
98
109
  Replay parsing is intentionally conservative: unreadable skill files degrade to
99
110
  empty surfaces instead of throwing, and malformed routing rows with empty
100
- trigger cells are ignored rather than treated as valid triggers.
111
+ trigger cells are ignored rather than treated as valid triggers. Claude replay
112
+ also normalizes observed `Read` paths against the staged workspace, so relative
113
+ skill reads still count as read-only evidence for the target or competing
114
+ skill. Reads outside the staged skill set are treated as replay failures rather
115
+ than benign negatives, because they indicate the runtime left the controlled
116
+ evaluation surface.
101
117
 
102
118
  ## Parsing Instructions
103
119
 
@@ -7,6 +7,7 @@ Bootstrap selftune for first-time use or after changing environments.
7
7
  - The user asks to set up selftune, configure selftune, or initialize selftune
8
8
  - The agent detects `~/.selftune/config.json` does not exist
9
9
  - The user has switched agent platforms (Claude Code, Codex, OpenCode)
10
+ - The user wants to add hooks for additional platforms (multi-agent setup)
10
11
 
11
12
  ## Default Command
12
13
 
@@ -136,15 +137,49 @@ Code subagent calls stay up to date.
136
137
  | `PostToolUse` (Bash) | `hooks/commit-track.ts` | Track git commits for session traceability | Fast-path: skips non-git Bash commands |
137
138
  | `Stop` | `hooks/session-stop.ts` | Capture session telemetry | Runs async (non-blocking), 60s timeout |
138
139
 
139
- **Codex agents:**
140
+ ### 4b. Multi-Platform Hooks
140
141
 
141
- - Use `selftune ingest wrap-codex` for real-time telemetry capture (see `Workflows/Ingest.md`)
142
- - Or batch-ingest existing sessions with `selftune ingest codex`
142
+ After Claude Code hooks are installed, check whether the user has **other** agent
143
+ CLIs available. Run these checks:
143
144
 
144
- **OpenCode agents:**
145
+ ```bash
146
+ which codex 2>/dev/null && echo "codex available"
147
+ which opencode 2>/dev/null && echo "opencode available"
148
+ ls ~/Documents/Cline/Hooks/ 2>/dev/null && echo "cline available"
149
+ ```
150
+
151
+ If **any** additional platforms are detected, use `AskUserQuestion` listing only
152
+ the platforms that were actually found:
153
+
154
+ > I detected these agent platforms in addition to your primary one:
155
+ > - [list only detected platforms, e.g. "Codex", "OpenCode"]
156
+ >
157
+ > Would you like to install selftune hooks for any of them? This enables
158
+ > real-time skill tracking across all your agents.
159
+
160
+ Options:
161
+ - `Yes — install hooks for all detected platforms`
162
+ - `Let me pick — show me the list` (then present only the detected platforms)
163
+ - `No — skip for now` (they can always run `selftune <platform> install` later)
164
+
165
+ For each platform the user selects, run the install command:
166
+
167
+ ```bash
168
+ selftune codex install # writes hooks.json entries
169
+ selftune opencode install # writes shell shim + config entries
170
+ selftune cline install # creates hook scripts
171
+ ```
172
+
173
+ Use `--dry-run` first if the user wants to preview. See `Workflows/PlatformHooks.md`
174
+ for platform-specific details.
175
+
176
+ **Batch ingest** fallback for platforms without real-time hooks or to backfill history:
145
177
 
146
- - Use `selftune ingest opencode` to import sessions from the SQLite database
147
- - See `Workflows/Ingest.md` for details
178
+ ```bash
179
+ selftune ingest codex # import Codex rollout sessions
180
+ selftune ingest opencode # import OpenCode sessions from SQLite
181
+ selftune ingest openclaw # import OpenClaw sessions
182
+ ```
148
183
 
149
184
  ### 5. Initialize Memory Directory
150
185
 
@@ -387,6 +422,13 @@ retrying with `selftune init --alpha --alpha-email <email> --force`.
387
422
  > and optional display name in chat, then run `selftune init --alpha --alpha-email ...`.
388
423
  > The browser opens automatically for approval. No manual key management needed.
389
424
 
425
+ **User uses multiple agents (Claude Code + Codex, etc.)**
426
+
427
+ > Run `selftune init` for the primary agent, then offer to install hooks for
428
+ > additional detected platforms. Run `selftune codex install`, `selftune opencode install`,
429
+ > or `selftune cline install` as needed. All platforms write to the same shared
430
+ > log schema — no extra config required.
431
+
390
432
  **Hooks not capturing data**
391
433
 
392
434
  > Run `selftune doctor` to check hook installation. Parse the JSON output
@@ -0,0 +1,93 @@
1
+ # Platform Hooks Workflow
2
+
3
+ ## Purpose
4
+
5
+ Install and configure selftune hooks for non-Claude-Code platforms (Codex, OpenCode, Cline).
6
+
7
+ ## When to Use
8
+
9
+ - User wants selftune on Codex, OpenCode, or Cline
10
+ - User asks about multi-platform support
11
+ - User wants real-time skill tracking on a non-Claude-Code agent
12
+
13
+ ## Commands
14
+
15
+ ### Install hooks for a platform
16
+
17
+ ```bash
18
+ selftune <platform> install [--dry-run] [--uninstall]
19
+ ```
20
+
21
+ Supported platforms: `codex`, `opencode`, `cline`
22
+
23
+ | Flag | Description |
24
+ | ------------- | ---------------------------------------------- |
25
+ | `--dry-run` | Preview what would be installed without writing |
26
+ | `--uninstall` | Remove selftune hooks from the platform |
27
+ | `--help, -h` | Show usage help |
28
+
29
+ ### Hook handler (called by the agent, not the user)
30
+
31
+ ```bash
32
+ selftune <platform> hook
33
+ ```
34
+
35
+ This is called automatically by the agent's hook system. Users don't run this directly.
36
+
37
+ ## Platform Details
38
+
39
+ ### Codex
40
+
41
+ - Config: `~/.codex/hooks.json`
42
+ - Events: SessionStart, PreToolUse, PostToolUse, Stop
43
+ - Install creates hooks.json entries that prefer `$SELFTUNE_CLI_PATH codex hook`, otherwise `npx -y selftune@latest codex hook`
44
+
45
+ ### OpenCode
46
+
47
+ - Config: `./opencode.json` or `~/.config/opencode/opencode.json`
48
+ - Plugin dir: `~/.config/opencode/plugins/` (global) or `./.opencode/plugins/` (project)
49
+ - Events: tool.execute.before, tool.execute.after, session.idle (via event handler)
50
+ - Install writes a TypeScript plugin file (`selftune-opencode-plugin.ts`) into the plugins directory (auto-discovered by OpenCode at startup)
51
+ - Agents are registered in the `agent` config key (identified by `[selftune]` description prefix)
52
+
53
+ ### Cline
54
+
55
+ - Config: `~/Documents/Cline/Hooks/`
56
+ - Events: PostToolUse, TaskComplete, TaskCancel
57
+ - Install creates executable shell scripts in the hooks directory
58
+
59
+ ## Examples
60
+
61
+ ### Codex
62
+
63
+ ```bash
64
+ selftune codex install # Install hooks into ~/.codex/hooks.json
65
+ selftune codex install --dry-run # Preview changes without writing
66
+ selftune codex install --uninstall # Remove selftune hooks
67
+ ```
68
+
69
+ ### OpenCode
70
+
71
+ ```bash
72
+ selftune opencode install # Install plugin (selftune-opencode-plugin.ts) + config entries
73
+ selftune opencode install --dry-run # Preview changes without writing
74
+ selftune opencode install --uninstall # Remove selftune plugin and config entries
75
+ ```
76
+
77
+ ### Cline
78
+
79
+ ```bash
80
+ selftune cline install # Create hook scripts in ~/Documents/Cline/Hooks/
81
+ selftune cline install --dry-run # Preview what would be created
82
+ selftune cline install --uninstall # Remove selftune hook scripts
83
+ ```
84
+
85
+ ### Hook handler (agent-only, not user-facing)
86
+
87
+ The hook subcommand is called automatically by the agent. Users do not run it directly:
88
+
89
+ ```bash
90
+ printf '%s\n' "$PAYLOAD" | selftune codex hook
91
+ printf '%s\n' "$PAYLOAD" | selftune opencode hook
92
+ printf '%s\n' "$PAYLOAD" | selftune cline hook
93
+ ```