@gcunharodrigues/wrxn 0.6.0 → 0.7.1

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.
@@ -13,6 +13,9 @@
13
13
  // recall <text...> alias of query (page-level recall; same substring engine)
14
14
  // write-page <tier> <slug> create <tier>/<slug>.md (refuses to overwrite); prints the path.
15
15
  // --force overwrites in place, but ONLY for the `_slots` focus slot.
16
+ // delete-page <tier> <slug> delete <tier>/<slug>.md (harvest-03 merge: the absorbed-page removal).
17
+ // Confined to the wiki tiers BY CONSTRUCTION (tier ∈ TIERS + kebab slug),
18
+ // so the path can never escape .wrxn/wiki/. Errors if the page is absent.
16
19
  //
17
20
  // Flags: --tier <concepts|decisions|gotchas|sessions|_rules|_slots|all> (default all) · --limit <N> (default 20)
18
21
  // --force (write-page only; overwrite the `_slots` slot in place) · --root <dir> (test override)
@@ -168,6 +171,28 @@ function runWritePage() {
168
171
  process.stdout.write(JSON.stringify({ written: path.relative(root, dest), tier }, null, 2) + '\n');
169
172
  }
170
173
 
174
+ // ── delete-page (harvest-03) ────────────────────────────────────────────────────
175
+ // The delete-by-reference path for harvest's merge: remove an absorbed near-dup page after the survivor
176
+ // is written. Confinement is structural — the page is addressed by <tier>/<slug>, where tier is checked
177
+ // against the TIERS allowlist and slug must be kebab-case (no `/`, no `..`, no `.`), so the constructed
178
+ // path can never traverse out of .wrxn/wiki/. Errors on a missing page (the symmetric inverse of
179
+ // write-page refusing an existing one), so a no-op delete is observable to the caller.
180
+ function runDeletePage() {
181
+ const [tier, slug] = positionals();
182
+ if (!tier || !slug) {
183
+ process.stdout.write('Usage: node .wrxn/wiki.cjs delete-page <tier> <slug>\n');
184
+ process.exit(2);
185
+ }
186
+ if (!TIERS.includes(tier)) fail(`unknown tier "${tier}" — one of ${TIERS.join(', ')}`);
187
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) fail(`slug must be kebab-case ([a-z0-9-]): "${slug}"`);
188
+
189
+ const root = wikiRoot();
190
+ const dest = path.join(root, tier, `${slug}.md`);
191
+ if (!fs.existsSync(dest)) fail(`page does not exist, nothing to delete: ${path.relative(root, dest)}`);
192
+ fs.unlinkSync(dest);
193
+ process.stdout.write(JSON.stringify({ deleted: path.relative(root, dest), tier }, null, 2) + '\n');
194
+ }
195
+
171
196
  function main() {
172
197
  const cmd = process.argv[2];
173
198
  switch (cmd) {
@@ -176,8 +201,10 @@ function main() {
176
201
  return runQuery();
177
202
  case 'write-page':
178
203
  return runWritePage();
204
+ case 'delete-page':
205
+ return runDeletePage();
179
206
  default:
180
- process.stdout.write('Usage: node .wrxn/wiki.cjs <query|recall|write-page> ...\n');
207
+ process.stdout.write('Usage: node .wrxn/wiki.cjs <query|recall|write-page|delete-page> ...\n');
181
208
  process.exit(cmd ? 2 : 0);
182
209
  }
183
210
  }
@@ -1,172 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- // WRXN session-end hook — the episodic writer + session janitor (wrxn-kernel-10, foundation-honesty-02).
5
- // SessionEnd. Writes a dated session page into the install's own wiki sessions tier from the
6
- // captured turn trail, then reaps the session's scratch state. Hygiene (foundation-honesty-02):
7
- // - skip-empty: a session that captured no turns writes NO page;
8
- // - reap: the consumed trail AND the first-touch marker (.wrxn/history/<sid>.touched, written by
9
- // code-intel-push) are removed so .wrxn/history/ can't grow without bound;
10
- // - bound: the sessions tier is capped (WRXN_SESSIONS_MAX, default 50) — oldest pages rotate out.
11
- // CONTINUITY DOCTRINE: this writer touches ONLY the sessions tier + the session's own history
12
- // scratch — it NEVER writes OR deletes the continuity baton (.wrxn/continuity/latest.md). That slot
13
- // has a single writer (the handoff skill); keeping the paths disjoint is the structural fix for the
14
- // clobber observed live 2026-06-12.
15
- //
16
- // Self-contained: ships into installs, MUST NOT import the kernel lib (node stdlib only).
17
- // Fail-open + side-effect-only: emits nothing useful, never blocks; any fault exits 0 silently.
18
- //
19
- // Contract: SessionEnd event JSON on stdin → exit 0. Side effect: a sessions/<date>-<sid>.md page
20
- // for a non-empty session, plus reaping of that session's trail + touched marker.
21
-
22
- const fs = require('fs');
23
- const path = require('path');
24
-
25
- // Bound the sessions tier to the most-recent N pages (override: WRXN_SESSIONS_MAX).
26
- const DEFAULT_SESSIONS_MAX = 50;
27
-
28
- function done() {
29
- process.exit(0);
30
- }
31
-
32
- function findInstallRoot(startDir) {
33
- let dir = startDir || process.env.CLAUDE_PROJECT_DIR || process.cwd();
34
- for (let i = 0; i < 12; i++) {
35
- if (fs.existsSync(path.join(dir, 'wrxn.install.json'))) return dir;
36
- const up = path.dirname(dir);
37
- if (up === dir) break;
38
- dir = up;
39
- }
40
- return null;
41
- }
42
-
43
- function nowISO() {
44
- return process.env.WRXN_NOW || new Date().toISOString();
45
- }
46
-
47
- // Sanitize a session id into a kebab slug fragment (the page + trail FILE name).
48
- function safeId(sid) {
49
- return String(sid || 'session')
50
- .toLowerCase()
51
- .replace(/[^a-z0-9]+/g, '-')
52
- .replace(/^-+|-+$/g, '')
53
- .slice(0, 48) || 'session';
54
- }
55
-
56
- // Collapse any whitespace run (incl. newlines/tabs) to a single space — keeps a value safe to
57
- // embed on a single frontmatter line (the wiki adapter + synapse engine PARSE these pages, so
58
- // a stray newline would corrupt the frontmatter and break wiki query).
59
- function oneLine(s) {
60
- return String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, 120);
61
- }
62
-
63
- // Read + consume the captured turn trail for this session (one `<iso>\t<line>` per turn).
64
- function readTrail(root, sid) {
65
- const trail = path.join(root, '.wrxn', 'history', `${safeId(sid)}.trail`);
66
- let raw;
67
- try {
68
- raw = fs.readFileSync(trail, 'utf8');
69
- } catch {
70
- return { turns: [], trail };
71
- }
72
- const turns = raw.split('\n').map((l) => l.trim()).filter(Boolean);
73
- return { turns, trail };
74
- }
75
-
76
- // Best-effort removal — `force` ignores a missing file; any other fault is swallowed (fail-open).
77
- function rmQuiet(p) {
78
- try {
79
- fs.rmSync(p, { force: true });
80
- } catch {
81
- /* best-effort cleanup, never block session close */
82
- }
83
- }
84
-
85
- // Bound the sessions tier: keep at most `max` most-recent dated pages, reaping the oldest. Dated
86
- // `YYYY-MM-DD-…` slugs sort chronologically, so the oldest are the lexicographically-first ones.
87
- // Cap = WRXN_SESSIONS_MAX (env) or DEFAULT_SESSIONS_MAX. Self-contained: never throws.
88
- function capSessions(dir) {
89
- const max = Number(process.env.WRXN_SESSIONS_MAX) || DEFAULT_SESSIONS_MAX;
90
- if (!Number.isFinite(max) || max <= 0) return;
91
- let pages;
92
- try {
93
- pages = fs.readdirSync(dir).filter((f) => /^\d{4}-\d{2}-\d{2}-.+\.md$/.test(f)).sort();
94
- } catch {
95
- return;
96
- }
97
- for (let i = 0; i < pages.length - max; i++) {
98
- rmQuiet(path.join(dir, pages[i]));
99
- }
100
- }
101
-
102
- function main() {
103
- let event = {};
104
- try {
105
- const stdin = fs.readFileSync(0, 'utf8');
106
- if (stdin.trim()) event = JSON.parse(stdin);
107
- } catch {
108
- /* no/garbled stdin → write a minimal page anyway */
109
- }
110
-
111
- const root = findInstallRoot();
112
- if (!root) done();
113
-
114
- const { turns, trail } = readTrail(root, event.session_id);
115
-
116
- // Skip-empty: a session that captured no turns leaves NO page — the first half of bounding the
117
- // sessions tier. Only write the page (and only then consume its trail) when there is activity.
118
- if (turns.length) {
119
- const sid = oneLine(event.session_id || 'session');
120
- const reason = oneLine(event.reason || 'unknown');
121
- const date = nowISO().slice(0, 10); // YYYY-MM-DD
122
- const slug = `${date}-${safeId(event.session_id)}`;
123
-
124
- const trailLines = turns.map((t, i) => {
125
- const tab = t.indexOf('\t');
126
- const line = tab > -1 ? t.slice(tab + 1) : t;
127
- return `${i + 1}. ${line}`;
128
- });
129
-
130
- const page = [
131
- '---',
132
- `name: ${slug}`,
133
- `description: Session ${sid} — ${turns.length} turn(s), ended ${reason}`,
134
- 'tier: sessions',
135
- 'source: session-end-hook',
136
- '---',
137
- '',
138
- `# Session ${date} (${sid})`,
139
- '',
140
- `- Ended: ${reason}`,
141
- `- Turns: ${turns.length}`,
142
- '',
143
- '## Turn trail',
144
- ...trailLines,
145
- '',
146
- ].join('\n');
147
-
148
- const dir = path.join(root, '.wrxn', 'wiki', 'sessions');
149
- try {
150
- fs.mkdirSync(dir, { recursive: true });
151
- fs.writeFileSync(path.join(dir, `${slug}.md`), page);
152
- rmQuiet(trail); // consume the trail only after its page has landed
153
- capSessions(dir); // rotation: bound the sessions tier (reap the oldest beyond the cap)
154
- } catch {
155
- /* page write failed → fail-open; leave the trail intact for no-loss */
156
- }
157
- } else {
158
- rmQuiet(trail); // empty session: nothing to write; drop any stray empty trail file
159
- }
160
-
161
- // The first-touch gate marker (written by code-intel-push) is pure per-session scratch — always
162
- // reap it so .wrxn/history/ can't grow without bound. NEVER touches the continuity baton.
163
- rmQuiet(path.join(root, '.wrxn', 'history', `${safeId(event.session_id)}.touched`));
164
-
165
- done();
166
- }
167
-
168
- try {
169
- main();
170
- } catch {
171
- done();
172
- }
@@ -1,76 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- // WRXN session-history hook — the turn-trail recorder (wrxn-kernel-10).
5
- // UserPromptSubmit. Appends one `<iso>\t<first-line>` record per turn to the session's trail
6
- // at .wrxn/history/<sid>.trail. SessionEnd reads this trail to build the durable session page.
7
- //
8
- // Self-contained: ships into installs, MUST NOT import the kernel lib (node stdlib only).
9
- // Pass-through recorder: it NEVER injects context and NEVER blocks — always emits {} (exit 0).
10
- // Fail-open: any fault still emits {}.
11
- //
12
- // Contract: UserPromptSubmit event JSON on stdin → {} on stdout (exit 0). Side effect: trail append.
13
-
14
- const fs = require('fs');
15
- const path = require('path');
16
-
17
- function emitEmpty() {
18
- process.stdout.write('{}');
19
- process.exit(0);
20
- }
21
-
22
- function findInstallRoot(startDir) {
23
- let dir = startDir || process.env.CLAUDE_PROJECT_DIR || process.cwd();
24
- for (let i = 0; i < 12; i++) {
25
- if (fs.existsSync(path.join(dir, 'wrxn.install.json'))) return dir;
26
- const up = path.dirname(dir);
27
- if (up === dir) break;
28
- dir = up;
29
- }
30
- return null;
31
- }
32
-
33
- function nowISO() {
34
- return process.env.WRXN_NOW || new Date().toISOString();
35
- }
36
-
37
- function safeId(sid) {
38
- return String(sid || 'session')
39
- .toLowerCase()
40
- .replace(/[^a-z0-9]+/g, '-')
41
- .replace(/^-+|-+$/g, '')
42
- .slice(0, 48) || 'session';
43
- }
44
-
45
- function main() {
46
- let event = {};
47
- try {
48
- const stdin = fs.readFileSync(0, 'utf8');
49
- if (stdin.trim()) event = JSON.parse(stdin);
50
- } catch {
51
- emitEmpty();
52
- }
53
-
54
- const root = findInstallRoot();
55
- if (!root) emitEmpty();
56
-
57
- const sid = safeId(event.session_id);
58
- const prompt = String(event.prompt || '').split('\n')[0].slice(0, 200).replace(/\t/g, ' ').trim();
59
- if (prompt) {
60
- try {
61
- const dir = path.join(root, '.wrxn', 'history');
62
- fs.mkdirSync(dir, { recursive: true });
63
- fs.appendFileSync(path.join(dir, `${sid}.trail`), `${nowISO()}\t${prompt}\n`);
64
- } catch {
65
- /* trail append is best-effort — never block the prompt */
66
- }
67
- }
68
-
69
- emitEmpty();
70
- }
71
-
72
- try {
73
- main();
74
- } catch {
75
- emitEmpty();
76
- }