@inetafrica/open-claudia 2.6.58 → 2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.7.0
4
+ - **Tools become memory objects (tool-first architecture, Phase 1).** A tool is no longer a flat script — it's a directory: `tools/<name>/tool` (the executable), `TOOL.md` (Interface · Known issues · State · Journal — the curated, human-readable condition of the tool), and `state.json` (machine telemetry, runtime-written, gitignored). The tools dir is now a **git repository**: every add, metadata change, note, and removal auto-commits (with a per-invocation identity, never touching git config), so `git log` *is* the upgrade history and any change is one revert from undone. `tool show` is tiered disclosure: header metadata, live telemetry, TOOL.md, and the recent commit log — the source only with `--source`, so reading about a tool no longer costs its whole body in context.
5
+ - **Risk tiers + run gate.** Every tool declares `risk: read-only | write | destructive` in its header (absent = treated as write, fail-safe). `tool run` now enforces the ask-first rule mechanically: read-only runs freely; write requires `--yes`; destructive requires `--yes-destructive` — and a plain `--yes` does **not** unlock a destructive tool. Refusals exit 3 with a clear message. The flags pass through to the tool unstripped, so tools with their own confirmation gates (e.g. prod-k8s) keep working. `tool add`/`tool set` accept `--risk`; adding without it defaults to write and says so.
6
+ - **Run telemetry stamped at the point of use.** `tool run` itself records every execution into the tool's `state.json`: run count, last used, last exit, last success/failure, per-verb health (first non-flag arg), and a rolling history of the last 20 invocations with timing. A failing run prints a nudge to journal the defect (`tool note <name> --issue "..."`). This is the v1.2 health doctrine in code: no smoke tests, no health crons — health is verified by use, and breakage is captured automatically where it happens.
7
+ - **`tool scaffold`, `tool note`, `tool migrate`.** `scaffold <name>` creates a ready-to-extend verb-dispatch stub (node or bash) with full header, TOOL.md skeleton, and initial telemetry — and refuses if the tool exists ("extend it instead"). `note <name> --issue|--state|--journal` maintains TOOL.md from the CLI: issues and journal lines insert newest-first; `--state` rewrites the State section (it describes *now*, not history). `migrate` converts legacy flat-file tools into directories — one commit each, sidecar telemetry carried over, TOOL.md generated — and runs automatically on first CLI use, so existing tools arrive in the new layout with their memory intact.
8
+ - **Secrets can no longer be saved into a tool.** The add-time secret lint is now **blocking** (was advisory): `tool add`, `tool set`, and `tool note` refuse content matching credential patterns, because the git-backed dir would remember the secret forever. Other lints (requires mismatch, syntax check, dangling pack) stay advisory. Extends `test-tools.js` end to end: layout, risk gate matrix, per-verb telemetry, secret refusal, migration, scaffold, git history.
9
+
3
10
  ## v2.6.58
4
11
  - **Tiered recall gate — stop paying 15 seconds for "thanks".** Measured over 700+ live turns, the old discoverer pre-gate returned true whenever *any* seed matched, so it gated just 0.4% of turns — a bare "ok" with an incidental keyword hit still paid the full seed → graph → walker pass (p50 ~15s). A new `recallTier()` classifies each turn into three tiers: **skip** (pleasantries/acks up to 4 words *regardless of seeds*, emoji-only, or ≤2 words with no seeds — no recall at all), **seeds** (short command-like turns ≤4 words — user-origin keyword seeds inject directly with no graph expansion, no walker, no excerpts; the classic-engine baseline at ~ms cost), and **full** (substantive turns — the whole pipeline). The tier is logged per turn and the metrics summary now tracks the seeds-only share alongside the gated share.
5
12
  - **Walker candidate cap.** The latency deep-dive showed the walk itself dominates, not process spawn: 15–40 candidates × 600-char excerpts = 10–25KB prompts to the haiku judge on every substantive turn. Full-tier candidates are now priority-ordered — user-origin seeds by match score, then context seeds, then graph-activated nodes by activation — and capped at `RECALL_WALKER_MAX_CANDIDATES` (default 14), so the judge reads the strongest few instead of everything the graph touched.
package/README.md CHANGED
@@ -267,7 +267,7 @@ Open Claudia layers three memory systems on top of the backend's native sessions
267
267
 
268
268
  **Recall engines** — how packs and entities get matched and surfaced is pluggable per chat via `/engine` (or the `RECALL_ENGINE` env default). **classic** (the default) is keyword FTS plus a relevance judge with headline injection — stable and unchanged. **discoverer** (opt-in) adds a typed-edge graph over the same corpus (`parent`/`governed-by`/`related` edges with weights in `recall-graph.db`) and runs: a pre-gate that skips recall on trivial turns → FTS seeding → spreading activation across the graph (1–2 hops — auto-pulls cross-cutting concerns the query never named) → a walker that reads each candidate and returns the genuinely-relevant set with one-line why-bullets (fail-open to keyword seeds, so it never recalls worse than classic). Edges form structurally from pack `parent` frontmatter and `[[links]]`, and strengthen via Hebbian co-use when the agent opens packs together (📖); weights decay over time. Inspect with `open-claudia recall-stats` and `open-claudia recall graph [--sync]`, or flip on `/recall` to watch — per turn — which packs/entities surfaced and why, right in the chat. Switch back any time with `/engine classic`.
269
269
 
270
- **Dream consolidation** — while the per-turn reviewer takes quick notes, *dream* is the slow overnight pass (default 4am, on a high-tier model — opus by default, set `DREAM_TIER` or `DREAM_MODEL`): it merges packs that drifted into the same topic, builds parent/sub pack trees with umbrella summaries, tightens descriptions and tags so the router matches with less noise, dedupes entities, cross-links notes, and tends the recall graph (structural sync, weight decay, orphan prune). Anything merged away is backed up under `~/.open-claudia/backup/dream-<stamp>/` first, and every dream that changes something reports in chat. Configure with `DREAM_CRON` / `DREAM_MODEL`, disable with `DREAM=off`.
270
+ **Dream consolidation** — while the per-turn reviewer takes quick notes, *dream* is the slow overnight pass (default 4am, on a high-tier model — opus by default, set `DREAM_TIER` or `DREAM_MODEL`): it merges packs that drifted into the same topic, builds parent/sub pack trees with umbrella summaries, tightens descriptions and tags so the router matches with less noise, dedupes entities, cross-links notes, and tends the recall graph (structural sync, weight decay, orphan prune). The pass is *evidence-grounded*: per-turn recall telemetry (what surfaced, what got kept, what the agent actually opened) feeds the prompt, so archive/merge calls rest on usage numbers rather than vibes. Most nights run as cheap *deltas* — full pack bodies only for what changed since the last dream plus graph neighbours and pre-computed merge candidates; a full-corpus sweep runs every `DREAM_FULL_SWEEP_DAYS` (30). Deterministic phases run even if the model call fails: journal backfill dedupe, weakening of graph nodes that keep arriving but never get kept, co-rescue edge reinforcement, episodic index tending, and a memory-health report (rescue rate, noise, latency, spend) in the morning chat summary. Dream may also tune one bounded recall knob per night (`recall-tuning.json`) — every change is checked against the next window's health and auto-rolled-back if rescue drops or noise rises; env pins always win. Anything merged away is backed up under `~/.open-claudia/backup/dream-<stamp>/` first, and every dream that changes something reports in chat. Configure with `DREAM_CRON` / `DREAM_MODEL`, disable with `DREAM=off`.
271
271
 
272
272
  **Personality** — your `soul.md` holds identity and hard rules; `~/.open-claudia/persona.md` holds the voice on top — tone, quirks, emoji habits. It feeds into the system prompt and the dream pass may evolve it gently (bounded, backed up, announced). Edit it directly any time.
273
273
 
@@ -279,6 +279,7 @@ open-claudia entity list|show <slug>|match "<text>"|note <name> "<text>"|remove
279
279
  open-claudia dream [--dry-run] # run the consolidation pass now
280
280
  open-claudia recall-stats # discoverer-engine metrics summary
281
281
  open-claudia recall graph [--sync] # recall-graph node/edge stats; --sync rebuilds structural edges
282
+ open-claudia kpi [--json|--html|--knobs] # harness KPIs by version+model; --html writes a chart report, --knobs shows dream-tuned knob state
282
283
  open-claudia transcript-search "<query>" # alias: ts; --all for every project
283
284
  open-claudia transcript-window "<pattern>" # alias: tw; hits with surrounding turns
284
285
  ```
@@ -436,8 +437,11 @@ All stored in `~/.open-claudia/`:
436
437
  | `MEMORY_RECALL_MAX_CHARS` | No | Hard cap for auto-injected pack/entity memory per turn (default `9000`, `off` disables auto recall injection) |
437
438
  | `RECALL_ENGINE` | No | Default recall engine when a chat hasn't set one via `/engine` (`classic` or `discoverer`, default `classic`) |
438
439
  | `RECALL_GRAPH_DB` / `RECALL_METRICS` | No | Override the discoverer graph DB path; `off` on metrics disables per-turn recall logging |
439
- | `RECALL_WALKER_MAX_CANDIDATES` | No | Cap on candidates sent to the discoverer's walker judge per turn (default `14`) |
440
- | `RECALL_EPISODES` / `RECALL_EPISODE_LIMIT` | No | `off` disables episodic transcript candidates in recall; limit caps transcript hits per turn (default `3`) |
440
+ | `RECALL_WALKER_MAX_CANDIDATES` | No | Cap on candidates sent to the discoverer's walker judge per turn (default `14`; dream-tunable 8–20, env pins it) |
441
+ | `RECALL_EPISODES` / `RECALL_EPISODE_LIMIT` | No | `off` disables episodic transcript candidates in recall; limit caps transcript hits per turn (default `3`; dream-tunable 1–5) |
442
+ | `RECALL_SEEDS_TIER_MAX_WORDS` | No | Turns at or under this word count take the fast seeds-only recall path (default `4`; dream-tunable 2–6) |
443
+ | `RECALL_DECAY_HALF_LIFE_DAYS` | No | Half-life for recall-graph edge decay during dream tending (default `60`; dream-tunable 10–90) |
444
+ | `RECALL_CASCADE` / `RECALL_CASCADE_SHADOW_RATE` | No | `off` disables the deterministic keep/drop cascade before the walker judge; shadow rate is the fraction of cascade verdicts also judged to audit agreement (default `0.1`) |
441
445
  | `PROJECT_TRANSCRIPTS` | No | Enable redacted project transcripts (default `true`) |
442
446
  | `TRANSCRIPT_MAX_ENTRY_CHARS` | No | Max chars per transcript entry (default `12000`) |
443
447
  | `TRANSCRIPTS_DIR` / `PACKS_DIR` / `ENTITIES_DIR` | No | Override storage directories |
@@ -448,6 +452,7 @@ All stored in `~/.open-claudia/`:
448
452
  | `DREAM_CRON` | No | Schedule for the dream pass (default `0 4 * * *`) |
449
453
  | `DREAM_MODEL` | No | Explicit model override for the dream pass (otherwise picked from `DREAM_TIER`) |
450
454
  | `DREAM_TIER` | No | Model tier for the dream pass: `low` (haiku) / `medium` (sonnet) / `high` (opus, default) |
455
+ | `DREAM_FULL_SWEEP_DAYS` | No | Days between full-corpus dream sweeps; other nights are cheap deltas over recently-touched packs (default `30`) |
451
456
  | `PERSONA_FILE` | No | Override the persona file location |
452
457
  | `WEB_UI` / `WEB_PORT` / `WEB_PASSWORD` | No | Web UI toggle, port, and password |
453
458
  | `WHISPER_CLI` / `WHISPER_MODEL` | No | whisper.cpp binary and model for voice notes |
package/bin/cli.js CHANGED
@@ -318,6 +318,10 @@ switch (command) {
318
318
  require("./recall").run(["stats", ...args.slice(1)]);
319
319
  break;
320
320
  }
321
+ case "kpi": {
322
+ require("./kpi").run(args.slice(1));
323
+ break;
324
+ }
321
325
  case "recall": {
322
326
  require("./recall").run(args.slice(1));
323
327
  break;
@@ -370,7 +374,7 @@ Memory tools:
370
374
  open-claudia transcript-window <pattern> Search project transcript, show hits with context
371
375
  (alias: tw; --help for options)
372
376
  open-claudia pack list|show|match|archive|restore|archived Context packs: living topic docs (skills + memory)
373
- open-claudia tool list|search|show|add|set|edit|run|remove Reusable tools: executable scripts the agent saves & re-runs (keyring preauth)
377
+ open-claudia tool list|search|show|scaffold|add|set|note|edit|run|migrate|remove Reusable tools: git-tracked dirs (exec + TOOL.md + telemetry), risk-gated runs
374
378
  open-claudia entity list|show|match|note Entity notes: people/places/projects memory
375
379
  open-claudia lessons list|add|remove|show Always-loaded learned rules (cross-cutting, promoted after a miss)
376
380
  open-claudia ideas list|add|remove|show Self-improvement backlog (captured by the nightly dream)
package/bin/kpi.js ADDED
@@ -0,0 +1,55 @@
1
+ // open-claudia kpi [--json] [--html [--out <path>]] [--knobs]
2
+ //
3
+ // Compare harness performance across versions and models: rescue rate, noise,
4
+ // latency by tier, cost/turn, cascade + walker health — aggregated from the
5
+ // version-stamped recall telemetry, plus the nightly dream KPI snapshots.
6
+
7
+ const path = require("path");
8
+
9
+ const HELP = `
10
+ Harness KPIs by version.
11
+
12
+ open-claudia kpi Version-vs-version table
13
+ open-claudia kpi --json Raw aggregates
14
+ open-claudia kpi --html Write a self-contained HTML report (charts), print its path
15
+ open-claudia kpi --html --out p Write the report to a specific path
16
+ open-claudia kpi --knobs Current recall-knob values + dream tuning history
17
+
18
+ Data sources: recall-metrics.jsonl (per-turn, version+model stamped) and
19
+ recall-kpi.jsonl (one snapshot per dream night).
20
+ `;
21
+
22
+ function run(args) {
23
+ if (args.includes("--help") || args[0] === "help") { console.log(HELP); return; }
24
+ const kpi = require(path.join(__dirname, "..", "core", "recall", "kpi"));
25
+
26
+ if (args.includes("--knobs")) {
27
+ const tuning = require(path.join(__dirname, "..", "core", "recall", "tuning"));
28
+ console.log("Recall knobs (env pin > dream tuning > default)");
29
+ console.log("───────────────────────────────────────────────");
30
+ for (const k of tuning.knobTable()) {
31
+ console.log(`${k.name.padEnd(22)} ${String(k.current).padEnd(6)}${k.pinned ? " [pinned by env]" : k.current !== k.default ? " [dream-tuned]" : ""}`);
32
+ }
33
+ const hist = tuning.history();
34
+ if (hist.length) {
35
+ console.log("\nTuning history");
36
+ for (const h of hist.slice(-10)) console.log(` ${h.ts.slice(0, 10)} ${h.knob}: ${h.from} → ${h.to} — ${h.reason}`);
37
+ }
38
+ return;
39
+ }
40
+
41
+ if (args.includes("--html")) {
42
+ const i = args.indexOf("--out");
43
+ const out = i !== -1 ? args[i + 1] : undefined;
44
+ console.log(kpi.writeHtmlReport(out));
45
+ return;
46
+ }
47
+
48
+ const rows = kpi.aggregate();
49
+ if (args.includes("--json")) { console.log(JSON.stringify({ versions: rows, snapshots: kpi.trend() }, null, 2)); return; }
50
+ console.log(kpi.renderTable(rows));
51
+ const snaps = kpi.trend();
52
+ if (snaps.length) console.log(`\n${snaps.length} nightly snapshot(s) in recall-kpi.jsonl — \`open-claudia kpi --html\` for charts.`);
53
+ }
54
+
55
+ module.exports = { run, HELP };
package/bin/tool.js CHANGED
@@ -1,23 +1,38 @@
1
- // CLI: manage and run reusable tools — executable scripts the agent saves so a
2
- // procedure it worked out once becomes a re-runnable command, not a re-typed
3
- // heredoc. The executable sibling of context packs (`open-claudia pack`).
1
+ // CLI: manage and run reusable tools — the interface operational work happens
2
+ // through. Tool-first loop: search exists: use it missing a verb: extend →
3
+ // nothing: scaffold, then work through the tool.
4
4
  //
5
5
  // open-claudia tool list — index (name — description [skill])
6
6
  // open-claudia tool search <query> — lexical match (find before adding a dup)
7
- // open-claudia tool show <name> full docs, path, keyring status
8
- // open-claudia tool add <path> [--name n] register a script as a tool
9
- // [--pack <dir>] [--desc "..."] [--requires "k1,k2"] [--usage "..."]
7
+ // open-claudia tool show <name> [--source] TOOL.md, telemetry, history (source with --source)
8
+ // open-claudia tool scaffold <name> create a new tool skeleton (dir + stub + TOOL.md)
9
+ // [--pack <dir>] [--desc "..."] [--risk read-only|write|destructive]
10
+ // [--requires "k1,k2"] [--usage "..."] [--lang node|bash]
11
+ // open-claudia tool add <path> [--name n] — register an existing script as a tool
12
+ // [--pack <dir>] [--desc "..."] [--risk tier] [--requires "k1,k2"] [--usage "..."]
10
13
  // open-claudia tool set <name> [--desc ...] — fix a tool's metadata in place
11
- // [--pack <dir>] [--requires "k1,k2"] [--usage "..."]
14
+ // [--pack <dir>] [--risk tier] [--requires "k1,k2"] [--usage "..."]
15
+ // open-claudia tool note <name> — append to TOOL.md (auto-committed)
16
+ // [--issue "..."] [--state "..."] [--journal "..."]
12
17
  // open-claudia tool edit <name> — print the file path to open & edit
13
- // open-claudia tool run <name> [args...] — run it with keyring pre-loaded
18
+ // open-claudia tool run <name> [args...] — run it (keyring pre-loaded, risk-gated)
19
+ // open-claudia tool migrate — convert legacy flat-file tools to directories
14
20
  // open-claudia tool remove <name> — delete one
15
21
  // open-claudia tool path — print the tools directory
16
22
  //
17
- // Fix before fork: when a tool is wrong, prefer `tool set` (metadata) or `tool
18
- // edit` (open the file) over adding a near-duplicate. `add` lints the source on
19
- // save flags hardcoded secrets, requires/keyring mismatches, syntax errors,
20
- // and dangling pack links — all advisory, never blocking.
23
+ // A tool is a directory: executable + TOOL.md (Interface · Known issues ·
24
+ // State · Journal) + state.json (runtime telemetry). The dir is git-tracked;
25
+ // every change commits, so `git -C $(open-claudia tool path) log` is the
26
+ // upgrade history.
27
+ //
28
+ // Risk tiers gate `tool run`: write-tier needs --yes, destructive-tier needs
29
+ // --yes-destructive — get the user's approval first; the flag records that
30
+ // deliberate step. Read-only tools run freely.
31
+ //
32
+ // Fix before fork: when a tool is wrong, prefer `tool set` (metadata), `tool
33
+ // note` (docs), or `tool edit` (body) over adding a near-duplicate. Hardcoded
34
+ // secrets BLOCK a save; other lints (requires/keyring mismatch, syntax,
35
+ // dangling pack link) stay advisory.
21
36
  //
22
37
  // Credentials: a tool runs with the operational keyring merged into its env, so
23
38
  // reference creds as $NAME inside the script. `open-claudia keyring list` shows
@@ -25,6 +40,7 @@
25
40
 
26
41
  const { spawnSync } = require("child_process");
27
42
  const tools = require("../core/tools");
43
+ const repo = require("../core/tool-repo");
28
44
 
29
45
  // Minimal flag parser: pulls --key value (and --key=value) out of argv, returns
30
46
  // { positional, flags }.
@@ -88,6 +104,17 @@ function run(args) {
88
104
  const cmd = (args[0] || "list").toLowerCase();
89
105
  const rest = args.slice(1);
90
106
 
107
+ // Auto-migrate legacy flat-file tools to the directory layout on first use
108
+ // (idempotent, one git commit per tool, telemetry carried over).
109
+ if (cmd !== "path") {
110
+ try {
111
+ if (repo.hasLegacyTools()) {
112
+ const { migrated } = repo.migrateLegacyTools();
113
+ if (migrated.length) console.error(`(migrated ${migrated.length} tool(s) to directory layout: ${migrated.join(", ")})`);
114
+ }
115
+ } catch (e) { /* best-effort */ }
116
+ }
117
+
91
118
  switch (cmd) {
92
119
  case "list":
93
120
  case "ls": {
@@ -98,7 +125,8 @@ function run(args) {
98
125
  console.log(`${all.length} reusable tool(s) — run via 'open-claudia tool run <name>':\n`);
99
126
  for (const t of all) {
100
127
  const skill = t.pack ? ` [skill: ${t.pack}]` : "";
101
- console.log(`• ${t.name} ${t.description || "(no description)"}${skill}`);
128
+ const risk = t.risk === "read-only" ? "" : ` [${t.risk || "write?"}]`;
129
+ console.log(`• ${t.name} — ${t.description || "(no description)"}${skill}${risk}`);
102
130
  }
103
131
  console.log(`\nDocs: open-claudia tool show <name> · Dir: ${tools.TOOLS_DIR}`);
104
132
  break;
@@ -125,13 +153,15 @@ function run(args) {
125
153
 
126
154
  case "show":
127
155
  case "cat": {
128
- const name = rest[0];
129
- if (!name) { console.error("Usage: tool show <name>"); process.exitCode = 1; return; }
156
+ const { positional, flags } = parseFlags(rest);
157
+ const name = positional[0];
158
+ if (!name) { console.error("Usage: tool show <name> [--source]"); process.exitCode = 1; return; }
130
159
  const t = tools.findTool(name);
131
160
  if (!t) { console.error(`No tool named "${name}". Run: open-claudia tool list`); process.exitCode = 1; return; }
132
161
  console.log(`Tool: ${t.name}`);
133
162
  if (t.description) console.log(`Description: ${t.description}`);
134
163
  if (t.pack) console.log(`Skill pack: ${t.pack} (open-claudia pack show ${t.pack})`);
164
+ console.log(`Risk: ${t.risk || "(undeclared — treated as write)"}`);
135
165
  console.log(`Usage: open-claudia tool run ${t.name}${t.usage ? " (" + t.usage + ")" : ""}`);
136
166
  if (t.requires.length) {
137
167
  const missing = tools.missingRequires(t);
@@ -139,6 +169,22 @@ function run(args) {
139
169
  console.log(`Requires keyring keys: ${t.requires.join(", ")} (${status})`);
140
170
  }
141
171
  console.log(`File: ${t.file}${t.executable ? "" : " (not executable!)"}`);
172
+
173
+ // Telemetry: overall + per-verb health from state.json.
174
+ if (t.runCount || t.lastUsed) {
175
+ const exit = t.lastExit === undefined ? "" : ` · last exit ${t.lastExit}`;
176
+ console.log(`Runs: ${t.runCount}${t.lastUsed ? ` · last used ${t.lastUsed.slice(0, 16).replace("T", " ")}` : ""}${exit}`);
177
+ }
178
+ if (t.verbs && Object.keys(t.verbs).length) {
179
+ const rows = Object.entries(t.verbs).map(([v, s]) =>
180
+ ` • ${v}: ${s.runs} run(s), last exit ${s.lastExit}${s.lastSuccess ? `, last success ${s.lastSuccess.slice(0, 10)}` : ""}`);
181
+ console.log("Verbs:\n" + rows.join("\n"));
182
+ }
183
+ try {
184
+ const log = repo.toolLog(t.name, 5);
185
+ if (log.length) console.log("Recent changes:\n" + log.map((l) => " " + l).join("\n"));
186
+ } catch (e) { /* git optional */ }
187
+
142
188
  // Directed tool-graph: what tends to run right after / before this one.
143
189
  try {
144
190
  const graph = require("../core/tool-graph");
@@ -156,7 +202,13 @@ function run(args) {
156
202
  }
157
203
  }
158
204
  } catch (e) { /* graph optional (old node) */ }
159
- console.log(`\n----- source -----\n${t.content}`);
205
+
206
+ // Tiered disclosure: TOOL.md is the manual; source only when modifying.
207
+ const doc = tools.readToolDoc(t.name);
208
+ if (doc) console.log(`\n----- TOOL.md -----\n${doc.trim()}`);
209
+ if (flags.source) console.log(`\n----- source -----\n${t.content}`);
210
+ else if (!doc) console.log(`\n----- source (no TOOL.md yet) -----\n${t.content}`);
211
+ else console.log(`\n(source: open-claudia tool show ${t.name} --source)`);
160
212
  break;
161
213
  }
162
214
 
@@ -173,10 +225,14 @@ function run(args) {
173
225
  name: typeof flags.name === "string" ? flags.name : undefined,
174
226
  pack: typeof flags.pack === "string" ? flags.pack : undefined,
175
227
  description: typeof flags.desc === "string" ? flags.desc : (typeof flags.description === "string" ? flags.description : undefined),
228
+ risk: typeof flags.risk === "string" ? flags.risk : undefined,
176
229
  requires: typeof flags.requires === "string" ? flags.requires.split(/[,\s]+/).filter(Boolean) : undefined,
177
230
  usage: typeof flags.usage === "string" ? flags.usage : undefined,
178
231
  });
179
- console.log(`Saved tool "${t.name}"${t.pack ? ` (skill: ${t.pack})` : ""}.`);
232
+ console.log(`Saved tool "${t.name}"${t.pack ? ` (skill: ${t.pack})` : ""} — risk: ${t.risk}.`);
233
+ if (t.defaultedRisk) {
234
+ console.log(` No --risk given → defaulted to "write" (runs will demand --yes). If it only reads, fix: open-claudia tool set ${t.name} --risk read-only`);
235
+ }
180
236
  console.log(`Run it: open-claudia tool run ${t.name}`);
181
237
  if (t.requires.length) {
182
238
  const missing = tools.missingRequires(t);
@@ -217,17 +273,21 @@ function run(args) {
217
273
  if (typeof flags.desc === "string") patch.description = flags.desc;
218
274
  else if (typeof flags.description === "string") patch.description = flags.description;
219
275
  if (typeof flags.pack === "string") patch.pack = flags.pack;
276
+ if (typeof flags.risk === "string") patch.risk = flags.risk;
220
277
  if (typeof flags.requires === "string") patch.requires = flags.requires.split(/[,\s]+/).filter(Boolean);
221
278
  if (typeof flags.usage === "string") patch.usage = flags.usage;
222
279
  if (Object.keys(patch).length === 0) {
223
- console.error('Nothing to set. Pass at least one of --desc / --pack / --requires / --usage. Body edits: open-claudia tool edit ' + name);
280
+ console.error('Nothing to set. Pass at least one of --desc / --pack / --risk / --requires / --usage. Body edits: open-claudia tool edit ' + name);
224
281
  process.exitCode = 1; return;
225
282
  }
226
- const updated = tools.updateToolHeader(name, patch);
283
+ let updated;
284
+ try { updated = tools.updateToolHeader(name, patch); }
285
+ catch (e) { console.error(`Could not update "${name}": ${e.message}`); process.exitCode = 1; return; }
227
286
  if (!updated) { console.error(`Could not update "${name}".`); process.exitCode = 1; return; }
228
287
  console.log(`Updated tool "${updated.name}".`);
229
288
  console.log(` Description: ${updated.description || "(none)"}`);
230
289
  if (updated.pack) console.log(` Skill pack: ${updated.pack}`);
290
+ console.log(` Risk: ${updated.risk || "(undeclared — treated as write)"}`);
231
291
  if (updated.requires.length) console.log(` Requires: ${updated.requires.join(", ")}`);
232
292
  if (updated.usage) console.log(` Usage: ${updated.usage}`);
233
293
  reportSafety(updated);
@@ -252,14 +312,108 @@ function run(args) {
252
312
  if (!name) { console.error("Usage: tool run <name> [args...]"); process.exitCode = 1; return; }
253
313
  const t = tools.findTool(name);
254
314
  if (!t) { console.error(`No tool named "${name}". Run: open-claudia tool list`); process.exitCode = 1; return; }
315
+ const toolArgs = rest.slice(1);
316
+
317
+ // Risk-tier gate: write needs --yes, destructive needs --yes-destructive.
318
+ // The flag is passed through to the tool (which may have its own gate).
319
+ const gate = tools.runGate(t, toolArgs);
320
+ if (!gate.ok) { console.error(gate.message); process.exitCode = 3; return; }
321
+
255
322
  const missing = tools.missingRequires(t);
256
323
  if (missing.length) {
257
324
  console.error(`Cannot run ${t.name}: missing keyring keys ${missing.join(", ")}. Set them with 'open-claudia keyring set <name> <value>'.`);
258
325
  process.exitCode = 1; return;
259
326
  }
260
- const r = spawnSync(t.file, rest.slice(1), { stdio: "inherit", env: tools.runEnv() });
261
- if (r.error) { console.error(`Failed to run ${t.name}: ${r.error.message}`); process.exitCode = 1; return; }
262
- process.exitCode = typeof r.status === "number" ? r.status : 1;
327
+ const started = Date.now();
328
+ const r = spawnSync(t.file, toolArgs, { stdio: "inherit", env: tools.runEnv() });
329
+ const ms = Date.now() - started;
330
+ if (r.error) {
331
+ tools.recordRunResult(t.name, { args: toolArgs, exit: -1, ms });
332
+ console.error(`Failed to run ${t.name}: ${r.error.message}`);
333
+ process.exitCode = 1; return;
334
+ }
335
+ const exit = typeof r.status === "number" ? r.status : 1;
336
+ tools.recordRunResult(t.name, { args: toolArgs, exit, ms });
337
+ if (exit !== 0) {
338
+ console.error(`(exit ${exit} recorded in ${t.name} telemetry — if this is a real defect, journal it: ` +
339
+ `open-claudia tool note ${t.name} --issue "<what broke>")`);
340
+ }
341
+ process.exitCode = exit;
342
+ break;
343
+ }
344
+
345
+ case "scaffold":
346
+ case "new": {
347
+ const { positional, flags } = parseFlags(rest);
348
+ const name = positional[0];
349
+ if (!name) {
350
+ console.error('Usage: tool scaffold <name> [--pack dir] [--desc "..."] [--risk read-only|write|destructive] [--requires "k1,k2"] [--usage "..."] [--lang node|bash]');
351
+ process.exitCode = 1; return;
352
+ }
353
+ try {
354
+ const t = repo.scaffoldTool(name, {
355
+ pack: typeof flags.pack === "string" ? flags.pack : undefined,
356
+ description: typeof flags.desc === "string" ? flags.desc : (typeof flags.description === "string" ? flags.description : undefined),
357
+ risk: typeof flags.risk === "string" ? flags.risk : undefined,
358
+ requires: typeof flags.requires === "string" ? flags.requires.split(/[,\s]+/).filter(Boolean) : [],
359
+ usage: typeof flags.usage === "string" ? flags.usage : undefined,
360
+ lang: typeof flags.lang === "string" ? flags.lang : "node",
361
+ });
362
+ console.log(`Scaffolded tool "${t.name}" (risk: ${t.risk}).`);
363
+ console.log(` Executable: ${t.file}`);
364
+ console.log(` Docs: ${t.docFile}`);
365
+ console.log(`Fill in the verbs, then work through it: open-claudia tool run ${t.name} <verb>`);
366
+ } catch (e) {
367
+ console.error(`Could not scaffold: ${e.message}`); process.exitCode = 1;
368
+ }
369
+ break;
370
+ }
371
+
372
+ case "note": {
373
+ const { positional, flags } = parseFlags(rest);
374
+ const name = positional[0];
375
+ const issue = typeof flags.issue === "string" ? flags.issue : null;
376
+ const state = typeof flags.state === "string" ? flags.state : null;
377
+ const journal = typeof flags.journal === "string" ? flags.journal : null;
378
+ if (!name || (!issue && !state && !journal)) {
379
+ console.error('Usage: tool note <name> [--issue "..."] [--state "..."] [--journal "..."]');
380
+ process.exitCode = 1; return;
381
+ }
382
+ const t = tools.findTool(name);
383
+ if (!t) { console.error(`No tool named "${name}". Run: open-claudia tool list`); process.exitCode = 1; return; }
384
+ if (!t.docFile) { console.error(`"${t.name}" is a legacy flat tool — run any tool command to migrate it first.`); process.exitCode = 1; return; }
385
+ try {
386
+ tools.assertNoSecrets([issue, state, journal].filter(Boolean).join("\n"), "note");
387
+ const fs = require("fs");
388
+ let doc = tools.readToolDoc(t.name) || repo.toolMdSkeleton(t);
389
+ const date = new Date().toISOString().slice(0, 10);
390
+ const appendUnder = (heading, line) => {
391
+ const re = new RegExp(`(##\\s*${heading}[^\\n]*\\n)`, "i");
392
+ if (re.test(doc)) doc = doc.replace(re, `$1${line}\n`);
393
+ else doc += `\n## ${heading}\n${line}\n`;
394
+ };
395
+ if (issue) appendUnder("Known issues", `- [${date}] ${issue}`);
396
+ if (state) {
397
+ // State is a rewrite, not an append — it describes NOW.
398
+ const re = /(##\s*State[^\n]*\n)([\s\S]*?)(?=\n##\s|$)/i;
399
+ if (re.test(doc)) doc = doc.replace(re, `$1${state}\n\n`);
400
+ else doc += `\n## State\n${state}\n`;
401
+ }
402
+ if (journal) appendUnder("Journal", `- [${date}] ${journal}`);
403
+ fs.writeFileSync(t.docFile, doc, { mode: 0o600 });
404
+ repo.commitTool(t.name, `note ${t.name}: ${(issue || state || journal).slice(0, 60)}`);
405
+ console.log(`Noted in ${t.name}/TOOL.md${issue ? " (known issue)" : ""}${state ? " (state)" : ""}${journal ? " (journal)" : ""}.`);
406
+ } catch (e) {
407
+ console.error(`Could not note: ${e.message}`); process.exitCode = 1;
408
+ }
409
+ break;
410
+ }
411
+
412
+ case "migrate": {
413
+ const { migrated, skipped } = repo.migrateLegacyTools();
414
+ if (!migrated.length && !skipped.length) console.log("Nothing to migrate — all tools already use the directory layout.");
415
+ if (migrated.length) console.log(`Migrated: ${migrated.join(", ")}`);
416
+ if (skipped.length) console.log(`Skipped (no tool header — left untouched): ${skipped.join(", ")}`);
263
417
  break;
264
418
  }
265
419
 
@@ -277,7 +431,7 @@ function run(args) {
277
431
  break;
278
432
 
279
433
  default:
280
- console.log('Usage: open-claudia tool [list | search <query> | show <name> | add <path> [flags] | set <name> [flags] | edit <name> | run <name> [args] | remove <name> | path]');
434
+ console.log('Usage: open-claudia tool [list | search <query> | show <name> [--source] | scaffold <name> [flags] | add <path> [flags] | set <name> [flags] | note <name> [flags] | edit <name> | run <name> [args] | migrate | remove <name> | path]');
281
435
  }
282
436
  }
283
437