@gcunharodrigues/wrxn 0.7.1 → 0.8.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/manifest.json CHANGED
@@ -343,6 +343,16 @@
343
343
  "class": "managed",
344
344
  "profile": "project"
345
345
  },
346
+ {
347
+ "path": ".claude/skills/write-an-agent/SKILL.md",
348
+ "class": "managed",
349
+ "profile": "project"
350
+ },
351
+ {
352
+ "path": ".claude/skills/write-an-agent/EXAMPLES.md",
353
+ "class": "managed",
354
+ "profile": "project"
355
+ },
346
356
  {
347
357
  "path": ".claude/skills/audit/SKILL.md",
348
358
  "class": "managed",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gcunharodrigues/wrxn",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "WRXN Kernel — installable AI operating system. Two profiles (project | workspace), pull-based updates, managed/seeded/state file classes.",
5
5
  "bin": {
6
6
  "wrxn": "bin/wrxn.cjs"
@@ -16,11 +16,19 @@
16
16
  "test": "node --test --require ./test/setup.cjs"
17
17
  },
18
18
  "dependencies": {
19
- "recon-wrxn": "6.0.0-wrxn.5"
19
+ "recon-wrxn": "6.0.0-wrxn.6"
20
20
  },
21
21
  "engines": {
22
22
  "node": ">=20"
23
23
  },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/gcunharodrigues/wrxn-kernel.git"
27
+ },
28
+ "publishConfig": {
29
+ "provenance": true,
30
+ "access": "public"
31
+ },
24
32
  "license": "MIT",
25
33
  "private": false
26
34
  }
@@ -35,14 +35,17 @@ This skill **queries that set and reports it**. It is the third maintenance loop
35
35
  node .wrxn/sync.cjs report
36
36
  ```
37
37
 
38
- 2. **Read the JSON** it prints — `{ status, stale[], unwatermarked[] }`:
38
+ 2. **Read the JSON** it prints — `{ status, stale[], unwatermarked[], orphaned[] }`:
39
39
 
40
- - **`status: "synced"`** — the stale set is empty. **Say so briefly ("all synced") and stop.** Do not
41
- manufacture findings. A clean tree is a successful no-op.
40
+ - **`status: "synced"`** — both the stale AND orphaned sets are empty. **Say so briefly ("all synced") and
41
+ stop.** Do not manufacture findings. A clean tree is a successful no-op.
42
42
  - **`status: "drift"`** — present each `stale[]` entry to the operator: the **doc** page, the **symbol**
43
43
  that moved, and **`synced_to` → `current`** (the watermark vs the source's current fingerprint). If
44
- `unwatermarked[]` is non-empty, note those separatelydocs that declare `derived_from` but were
45
- never watermarked (so drift can't yet be computed for them).
44
+ `orphaned[]` is non-empty, flag those DISTINCTLYeach is a **dangling** doc (`doc` + `synced_to`) whose
45
+ `derived_from:` source symbol was **renamed or deleted**, so its provenance is gone and drift can no
46
+ longer be computed. The reconcile loop below cannot fix an orphan (there is no live source to re-stamp
47
+ against); surface it so the operator can re-anchor or retire the page. If `unwatermarked[]` is non-empty,
48
+ note those separately — docs that declare `derived_from` but were never watermarked.
46
49
  - **`status: "unavailable"`** — recon's serve door is not warm (no `recon-wrxn serve` running, or it was
47
50
  unreachable). Report "drift unavailable — start `recon-wrxn serve` and retry." Never treat this as
48
51
  "all synced": unknown is not clean.
@@ -0,0 +1,38 @@
1
+ # write-an-agent — Examples
2
+
3
+ ## A compressed read-only locator
4
+
5
+ A canonical interactive subagent: a single job, a least-privilege grant (read-only — no `Bash`, so no
6
+ push path), `haiku` (the work is mechanical), and a tight output contract with a concrete example.
7
+
8
+ ```
9
+ ---
10
+ name: route-finder
11
+ description: >
12
+ Read-only locator for HTTP routes. Returns a path:line table of where each route
13
+ is defined and its handler. Use PROACTIVELY when someone asks "where is the X
14
+ route", "what handles the Y endpoint", or "list all routes".
15
+ tools: Read, Grep, Glob
16
+ model: haiku
17
+ ---
18
+ You locate HTTP routes and report them. Nothing else.
19
+
20
+ ## Process
21
+ 1. Grep for route definitions (router calls, decorators, path strings).
22
+ 2. Resolve each route's handler symbol.
23
+ 3. Emit the table. Stop.
24
+
25
+ ## Constraints
26
+ - Read-only — never edit, run git, or propose changes.
27
+ - No preamble, no prose padding.
28
+
29
+ ## Output
30
+ Your final message IS the result. One row per route, nothing else:
31
+ `METHOD /path — handler@file:line`
32
+ Example:
33
+ `GET /users — listUsers@api/users.ts:42`
34
+ ```
35
+
36
+ **Why it's SOTA:** read-only grant (no push path), `haiku` (mechanical), the `description` leads
37
+ delegation with "use PROACTIVELY" + exact trigger phrases, and the output contract is a fixed
38
+ one-row-per-route shape — the caller gets a compact table, not prose.
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: write-an-agent
3
+ description: Create a state-of-the-art interactive Claude Code subagent (.claude/agents/<name>.md) through a guided interview — pin its single job, routing trigger, least-privilege tools (you approve the grant), model, and a mandatory compressed output contract, then scaffold and validate the subagent file. Use when someone wants to create, add, or scaffold a subagent, build a custom agent, or says "write an agent", "create an agent", or "new subagent".
4
+ ---
5
+
6
+ # Writing an Agent
7
+
8
+ Scaffold an **interactive subagent** — a `.claude/agents/<name>.md` file spawned via the Agent/Task
9
+ tool — through a short interview that gets the few SOTA levers right. Your job here is not to fill a
10
+ template; it is to **interrogate the job until the subagent is sharp, cheap, and safe**, then write a
11
+ validated file.
12
+
13
+ **Scope:** interactive helpers (locators, reviewers, runners you call in-session). **Not** AFK
14
+ pipeline executors — those live in the kernel's dispatch-executor registry (`wrxn dispatch`). If the
15
+ user wants a workflow *they* follow inline, write a skill instead (`write-a-skill`).
16
+
17
+ ## Why a subagent (the levers that make it good)
18
+
19
+ A subagent is a **separate context** with its own tools, model, and system prompt. The caller sees
20
+ **only its final message**. That one fact drives every lever:
21
+
22
+ | Lever | Why |
23
+ |---|---|
24
+ | **Single responsibility** | One narrow job → sharp prompt → correct routing. An "and" means two subagents. |
25
+ | **`description` = #1 routing lever** | The main thread matches this text to delegate. Vague → mis-routed. Use "use PROACTIVELY" + trigger phrases. |
26
+ | **Least-privilege `tools`** | Omitting `tools` inherits ALL tools. Smaller surface = safer + cheaper. |
27
+ | **Compressed output contract** | The caller eats only the final message. A terse fixed shape is the highest-value, most-skipped lever. |
28
+ | **Stateless** | No memory but the spawn prompt + this file. Put everything it needs on the page. |
29
+ | **Model match** | Mechanical → haiku; reasoning → sonnet; hard → opus. |
30
+
31
+ ## File format (verified — code.claude.com/docs sub-agents)
32
+
33
+ ```
34
+ ---
35
+ name: <kebab-case, equals the filename>
36
+ description: >
37
+ <what it does> + WHEN. Include "use PROACTIVELY" + trigger phrases.
38
+ tools: Read, Grep, Glob # explicit allowlist ALWAYS — omitting inherits ALL tools
39
+ model: haiku # haiku | sonnet | opus | inherit
40
+ skills: tdd # OPTIONAL — preloads that skill's content at startup
41
+ ---
42
+ <system prompt — SOTA body order below>
43
+ ```
44
+
45
+ Real fields: `name`, `description`, `tools`, `model`, `skills` (advanced: `disallowedTools`,
46
+ `permissionMode`). Don't invent others.
47
+
48
+ ## Interview — run in this order
49
+
50
+ 1. **Job** — the ONE responsibility. If it needs an "and", STOP and propose two subagents.
51
+ 2. **Trigger** — when should the main thread delegate? → becomes `description`:
52
+ `<what it does>. Use PROACTIVELY when <the exact situations/phrases that route to it>.`
53
+ 3. **Propose frontmatter → ONE consolidated approval.** Infer from the job, then show `tools` +
54
+ `model` + `skills` together as a single preview; the operator approves or edits before you continue:
55
+ - **tools** — the minimal allowlist. Locator/reviewer → `Read, Grep, Glob`; + run things → `+Bash`;
56
+ mutates files → `+Edit, Write`; research → `WebFetch, WebSearch`; integration →
57
+ `mcp__<server>__<tool>` (named, never `*`). **Never omit** (= inherit-all). **Never grant a push
58
+ path** (no devops). Flag `Bash` as broad (reaches `git` + the network).
59
+ - **model** — mechanical → `haiku`, reasoning → `sonnet`, hard → `opus`, match the caller → `inherit`.
60
+ - **skills** — propose only if the job maps to a skill **installed here** (`.claude/skills/<name>/`
61
+ exists; a dangling preload = a broken subagent). Don't auto-add `caveman` — compression ships via
62
+ the body instruction, which is portable.
63
+ 4. **Output contract (mandatory)** — pin exactly what returns + how terse: a fixed return shape (e.g.
64
+ `path:line — finding`, one per line; or small JSON) **and** a compression instruction (lead with the
65
+ answer, drop prose, backtick exact paths/symbols). Don't finish the subagent without it.
66
+ 5. **Generate → validate → write** (below).
67
+
68
+ ## Body structure (SOTA order — every subagent)
69
+
70
+ 1. **Role** — one line: who it is + the single job.
71
+ 2. **Process** — the numbered steps it follows.
72
+ 3. **Constraints** — hard NOs: what it refuses (scope creep; edits if read-only; ever pushing).
73
+ 4. **Output contract** — the fixed return shape + **one concrete example**. State plainly: *your final
74
+ message IS the return value — return the result, not a conversational reply.* Compress.
75
+ 5. **Stateless reminder** — it gets only its spawn prompt + this file; no main-thread memory, no
76
+ inherited CLAUDE.md persona. Keep the page self-sufficient.
77
+
78
+ ## Validate (loose by design)
79
+
80
+ - [ ] `---` fences open/close; `name` present, kebab-case, equals the filename.
81
+ - [ ] `description` says **what** + **when** (the routing trigger; uses "use PROACTIVELY").
82
+ - [ ] `tools` is an explicit allowlist of **real** tools; present, never omitted; no push path.
83
+ - [ ] `model` ∈ `haiku | sonnet | opus | inherit`.
84
+ - [ ] `skills` (if present) names a skill that exists under `.claude/skills/`.
85
+ - [ ] Body has Role + Process + Constraints + Output contract (with an example).
86
+ - Don't hard-gate `tools` against a frozen list — new MCP tools are valid.
87
+
88
+ ## Write + load
89
+
90
+ - Write `.claude/agents/<name>.md` at the install root. **Brownfield-safe**: if it exists, STOP —
91
+ offer to edit or rename, never overwrite.
92
+ - **Loading:** a file written to disk is **not live this session**. Tell the operator to **restart
93
+ Claude Code, or create/refresh it via the `/agents` interface** (which loads immediately). Then it is
94
+ invokable by the Agent/Task tool, a workflow, or FleetView.
95
+
96
+ See **[EXAMPLES.md](EXAMPLES.md)** for a complete worked subagent.
package/payload/.mcp.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "recon-wrxn": {
4
4
  "command": "npx",
5
- "args": ["-y", "recon-wrxn@6.0.0-wrxn.5", "serve"]
5
+ "args": ["-y", "recon-wrxn@6.0.0-wrxn.6", "serve"]
6
6
  }
7
7
  }
8
8
  }
@@ -50,6 +50,14 @@ const HARVEST_DIR = ['.wrxn', 'harvest'];
50
50
  const STAGED_FILE = 'staged.jsonl'; // the proposed-but-unconfirmed merges (survivor body + absorbed, by-reference).
51
51
  const AUDIT_FILE = 'audit.jsonl'; // append-only outcome log (stage + commit + decay events).
52
52
  const DECAY_STAGED_FILE = 'decay-staged.jsonl'; // harvest-04: proposed-but-unconfirmed decay annotations (by-reference). Distinct fixed name from merge's staged.jsonl; both non-.md so recon never recalls a staged-but-unconfirmed op.
53
+ // Bounded retention for the timestamped <ts>.jsonl check reports (phase-4.5-04). `check` writes a fresh
54
+ // report every run; without a cap the dir grows without bound on a long-lived install. Keep the N most-recent
55
+ // (ISO timestamps sort lexically = chronologically — no clock read). 20 is a generous trailing window of
56
+ // recent checks for trend/diff while strictly bounding growth; env override (clamped >= 1 so the just-written
57
+ // report is never pruned) mirrors synapse's WRXN_HANDOFF_PCT precedent. REPORT_RE matches ONLY a timestamped
58
+ // report — the fixed-name state files (staged/audit/decay-staged.jsonl) + .gitkeep never match, never prune.
59
+ const REPORT_RETAIN_DEFAULT = 20;
60
+ const REPORT_RE = /^\d{4}-\d{2}-\d{2}T.*\.jsonl$/;
53
61
  const BODY_MAX = 32000; // survivor body cap (chars) — a durable merged page, not a dump (dream/sync parity).
54
62
  const WIKI_REL = ['.wrxn', 'wiki']; // all merge targets confine under <root>/.wrxn/wiki/<knowledge-tier>/.
55
63
  const WIKI_PREFIX = '.wrxn/wiki/'; // stripped to form the reinforce.json wiki-rel join key (recall-surface parity).
@@ -296,6 +304,22 @@ function nearDupQualifies(hit) {
296
304
  return Number.isFinite(sem) && hasSemantic && sem >= NEAR_DUP_THRESHOLD;
297
305
  }
298
306
 
307
+ // Order clusters by a STABLE total order so an unchanged tree yields a byte-identical report every run
308
+ // (phase-4.5-04). Primary key = the lexically-first member. The old `(a,b) => a.members[0] < b.members[0]
309
+ // ? -1 : 1` returned 1 for BOTH compare(a,b) AND compare(b,a) on an equal leading member — non-antisymmetric,
310
+ // so V8's sort could resolve tied clusters by input permutation → non-reproducible reports. Clusters are
311
+ // disjoint connected components, so leading members never collide TODAY; the secondary keys make this a
312
+ // proper total order regardless (defense in depth): larger cluster first, then the stronger edge score, then
313
+ // the full member list — and two truly-identical clusters compare EQUAL (return 0).
314
+ function compareClusters(a, b) {
315
+ if (a.members[0] !== b.members[0]) return a.members[0] < b.members[0] ? -1 : 1;
316
+ if (a.members.length !== b.members.length) return b.members.length - a.members.length; // larger cluster first
317
+ if (a.score !== b.score) return b.score - a.score; // stronger dup signal first
318
+ const ja = a.members.join('');
319
+ const jb = b.members.join('');
320
+ return ja < jb ? -1 : ja > jb ? 1 : 0; // full member list; identical clusters → 0
321
+ }
322
+
299
323
  // Collapse pairwise near-dup edges into connected-component CLUSTERS (union-find), so a symmetric A↔B
300
324
  // match is reported once and a transitive A-B-C chain is one cluster of three. Each cluster carries its
301
325
  // sorted members + the STRONGEST edge similarity (the clearest dup signal). Singletons are not clusters.
@@ -333,7 +357,7 @@ function clusterNearDups(edges) {
333
357
  if (members.size < 2) continue;
334
358
  clusters.push({ members: [...members].sort(), score: Math.round((groupScore.get(r) || 0) * 1e4) / 1e4 });
335
359
  }
336
- return clusters.sort((a, b) => (a.members[0] < b.members[0] ? -1 : 1));
360
+ return clusters.sort(compareClusters);
337
361
  }
338
362
 
339
363
  // ── the door (IO shell, injectable transport) — the recall-surface.cjs contract ──
@@ -504,6 +528,41 @@ function reportPath(dir, ts) {
504
528
  return file;
505
529
  }
506
530
 
531
+ // The effective report-retention bound: the WRXN_HARVEST_RETAIN env override (clamped to a whole number >= 1
532
+ // so a bogus/zero value can never prune away the just-written report), else the sane default (phase-4.5-04).
533
+ function reportRetention() {
534
+ const env = Number(process.env.WRXN_HARVEST_RETAIN);
535
+ return Number.isFinite(env) && env >= 1 ? Math.floor(env) : REPORT_RETAIN_DEFAULT;
536
+ }
537
+
538
+ // Prune the timestamped check reports under .wrxn/harvest/ to the retention bound, keeping the `keep` most-
539
+ // recent (phase-4.5-04). ISO timestamps sort lexically = chronologically, so the oldest are the lexical
540
+ // prefix — no clock is read. ONLY <ts>.jsonl reports are eligible (REPORT_RE); the fixed-name state files
541
+ // (staged/audit/decay-staged.jsonl) + .gitkeep never match, so curation/merge/decay trails are never touched.
542
+ // Fail-soft: an unreadable dir or a failed unlink is swallowed — retention is hygiene, never the point of check.
543
+ function pruneReports(dir, keep, protect) {
544
+ let names;
545
+ try {
546
+ names = fs.readdirSync(dir);
547
+ } catch {
548
+ return; // no report dir yet → nothing to prune
549
+ }
550
+ const reports = names.filter((n) => REPORT_RE.test(n)).sort(); // lexical = chronological (oldest first)
551
+ // Delete the oldest down to the `keep` bound, but NEVER the just-written report (`protect`): a same-
552
+ // millisecond collision names it `<base>-N.jsonl`, which collates BEFORE its `<base>.jsonl` sibling, so a
553
+ // blind oldest-prefix prune could delete the fresh report `check` is about to return (phase-4.5-04 review).
554
+ let toDelete = reports.length - keep;
555
+ for (let i = 0; i < reports.length && toDelete > 0; i++) {
556
+ if (reports[i] === protect) continue; // the fresh report is retained and counts toward `keep`
557
+ try {
558
+ fs.unlinkSync(path.join(dir, reports[i]));
559
+ } catch {
560
+ /* fail-soft — a vanished/locked report never breaks the check run */
561
+ }
562
+ toDelete--;
563
+ }
564
+ }
565
+
507
566
  // ── check: the IO orchestrator ───────────────────────────────────────────────────
508
567
  // scanLocal (always) + nearDupFromDoor (degrades to unavailable when cold) → assemble → write the jsonl.
509
568
  // REPORT-ONLY: the ONLY write is the report under .wrxn/harvest/. `transport` is injected in tests.
@@ -520,6 +579,7 @@ async function check(root, { transport, timeoutMs } = {}) {
520
579
  const dir = harvestDir(root);
521
580
  const file = reportPath(dir, ts);
522
581
  fs.writeFileSync(file, records.length ? records.map((r) => JSON.stringify(r)).join('\n') + '\n' : '');
582
+ pruneReports(dir, reportRetention(), path.basename(file)); // phase-4.5-04: bound the report dir, never pruning the fresh report
523
583
  const nearDupCount = near.status === 'unavailable' ? 0 : near.clusters.length;
524
584
  return {
525
585
  report: path.relative(root, file),
@@ -1154,7 +1214,10 @@ module.exports = {
1154
1214
  scanLocal,
1155
1215
  isProse,
1156
1216
  nearDupQualifies,
1217
+ compareClusters,
1157
1218
  clusterNearDups,
1219
+ pruneReports,
1220
+ reportRetention,
1158
1221
  assembleRecords,
1159
1222
  tierOfPath,
1160
1223
  isHarvestPath,
@@ -1187,4 +1250,5 @@ module.exports = {
1187
1250
  NEAR_DUP_THRESHOLD,
1188
1251
  FIND_PATH,
1189
1252
  REINFORCE_WINDOW_DAYS,
1253
+ REPORT_RETAIN_DEFAULT,
1190
1254
  };
@@ -15,10 +15,12 @@
15
15
  //
16
16
  // Subcommand:
17
17
  // report query recon_drift over the serve door and print the drift summary JSON:
18
- // { status, stale[], unwatermarked[] }
19
- // · status "drift" — at least one doc is stale (stale[] names doc/symbol/synced_to/current).
20
- // · status "synced" — the warm door computed an EMPTY stale set ("all synced"; AC3 no-op,
21
- // never manufactures stale rows).
18
+ // { status, stale[], unwatermarked[], orphaned[] }
19
+ // · status "drift" — at least one doc is stale OR orphaned. stale[] names doc/symbol/
20
+ // synced_to/current; orphaned[] (phase-4.5-02) names a doc whose
21
+ // derived_from source symbol is GONE (doc/synced_to, no symbol/current).
22
+ // · status "synced" — the warm door computed an EMPTY stale AND orphaned set ("all synced";
23
+ // AC3 no-op, never manufactures rows).
22
24
  // · status "unavailable" — recon is unreachable OR answered without the structured drift sidecar
23
25
  // (no warm door, a timeout, a non-200, a malformed body, or a 200 whose
24
26
  // body lacks an affirmative `drift.stale` array — unknown is not clean,
@@ -117,11 +119,18 @@ function summarizeDrift(parsed) {
117
119
  if (!isEntry(d) || !Array.isArray(d.stale)) return unavailable();
118
120
  const stale = d.stale.filter(isEntry).map(normEntry);
119
121
  const unwatermarked = (Array.isArray(d.unwatermarked) ? d.unwatermarked : []).filter(isEntry).map(normEntry);
120
- return { status: stale.length ? 'drift' : 'synced', stale, unwatermarked };
122
+ // phase-4.5-02: the third drift class a watermarked page whose derived_from source symbol is GONE from
123
+ // recon's graph (renamed/deleted). Each entry normalizes to { doc, synced_to } (no symbol/current — the
124
+ // source is absent). It is DISTINCT from stale (source moved) and unwatermarked (never reconciled), but
125
+ // like stale it MUST elevate the status off a clean "synced": a dangling page is the exact case sync used
126
+ // to hide (the operator never learned the page was un-reconcilable). An older recon with no drift.orphaned
127
+ // degrades to [] (back-compatible, never throws).
128
+ const orphaned = (Array.isArray(d.orphaned) ? d.orphaned : []).filter(isEntry).map(normEntry);
129
+ return { status: stale.length || orphaned.length ? 'drift' : 'synced', stale, unwatermarked, orphaned };
121
130
  }
122
131
 
123
132
  function unavailable() {
124
- return { status: 'unavailable', stale: [], unwatermarked: [] };
133
+ return { status: 'unavailable', stale: [], unwatermarked: [], orphaned: [] };
125
134
  }
126
135
 
127
136
  // ── the door (IO shell, injectable transport) — the recall-surface.cjs contract ─