@gcunharodrigues/wrxn 0.6.0 → 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/lib/install.cjs +2 -0
- package/manifest.json +15 -15
- package/migrations/004-retire-session-capture.cjs +106 -0
- package/package.json +1 -1
- package/payload/.claude/hooks/recall-surface.cjs +90 -4
- package/payload/.claude/hooks/session-start.cjs +4 -23
- package/payload/.claude/hooks/synapse-engine.cjs +41 -4
- package/payload/.claude/settings.json +0 -8
- package/payload/.claude/skills/harvest/SKILL.md +210 -0
- package/payload/.wrxn/dream.cjs +43 -1
- package/payload/.wrxn/harvest.cjs +1190 -0
- package/payload/.wrxn/wiki.cjs +28 -1
- package/payload/.claude/hooks/session-end.cjs +0 -172
- package/payload/.claude/hooks/session-history.cjs +0 -76
- /package/payload/.wrxn/{wiki/sessions → harvest}/.gitkeep +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: harvest
|
|
3
|
+
description: Curate the knowledge tiers — run a health-check, then through operator-confirmed proposals MERGE near-duplicate pages into one evidence-grounded survivor and FORWARD-LINK or FLAG superseded/orphaned pages. The destructive sibling to dream (additive) and sync (drift). Use when someone says "harvest", "curate the wiki", "merge duplicate pages", "clean up stale knowledge", or at handoff when there is curation debt.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# harvest — knowledge curation
|
|
8
|
+
|
|
9
|
+
harvest is the third maintenance loop, sibling to **dream** (consolidation, additive-only) and **sync**
|
|
10
|
+
(drift). It does what dream deliberately doesn't: **merge, forward-link, and flag** the four knowledge
|
|
11
|
+
tiers (`concepts` / `decisions` / `gotchas` / `_rules`), which silt up over time — near-duplicates pile
|
|
12
|
+
beside each other, superseded pages linger next to their replacements, and pages whose source code was
|
|
13
|
+
deleted go orphaned. Left alone, Recall surfaces a thicket where one clean page belongs.
|
|
14
|
+
|
|
15
|
+
You **propose**; the deterministic adapter **gates and writes**. Every destructive act is shown to the
|
|
16
|
+
operator and **nothing is merged, annotated, or deleted without confirmation** — and a rejected proposal
|
|
17
|
+
is re-checked at the write boundary, so it can never mutate a page. The only sanctioned hard-delete is a
|
|
18
|
+
**merge**: an absorbed page is deleted *only* once its content provably lives in the survivor.
|
|
19
|
+
|
|
20
|
+
## Indirection contract (MUST)
|
|
21
|
+
|
|
22
|
+
> Drive the adapter. NEVER edit, annotate, or delete a wiki page directly, and NEVER re-implement the gate.
|
|
23
|
+
|
|
24
|
+
- Health-check, merge (`stage` / `commit`), and decay (`decay propose` / `decay confirm`) all go through
|
|
25
|
+
**`.wrxn/harvest.cjs`** — the report, the safety gate (secret-scan, integrity hash, path-confinement),
|
|
26
|
+
the audit trail, and the writer.
|
|
27
|
+
- To DRAFT a survivor you may **read** the absorbed pages — they are plain `.md` at the paths the report
|
|
28
|
+
gives — and confirm recall via **`.wrxn/wiki.cjs query`**. Reading never mutates; the indirection
|
|
29
|
+
contract forbids only direct writes/deletes and re-implementing the gate.
|
|
30
|
+
- The skill is the **semantic** filter (you don't draft a junk merge); the adapter is the **mechanical**
|
|
31
|
+
backstop (it rejects what slips through). A proposal the gate rejects is never written.
|
|
32
|
+
|
|
33
|
+
## The loop
|
|
34
|
+
|
|
35
|
+
1. **Health-check** — `node .wrxn/harvest.cjs check` writes a durable report; nothing is touched.
|
|
36
|
+
2. **Read the report** — one record per finding: `near_dup` clusters, `decay_candidate`s
|
|
37
|
+
(orphaned / superseded), and `malformed` pages.
|
|
38
|
+
3. **Per finding, draft a proposal** — a merge survivor for a near-dup cluster; a `stale:` /
|
|
39
|
+
`superseded_by:` annotation for a decay candidate.
|
|
40
|
+
4. **Stage** — `stage` (merge) or `decay propose` (annotate): record by-reference, the live page untouched.
|
|
41
|
+
5. **Present + confirm** — show the operator the survivor (and the pages it deletes) or the annotation, and
|
|
42
|
+
wait. Nothing is written on staging alone.
|
|
43
|
+
6. **Commit** — `commit` (merge) or `decay confirm` (annotate) the operator-approved subset only.
|
|
44
|
+
|
|
45
|
+
If the report is clean, **say so and stop** — a tree with no debt is a successful no-op. Do not manufacture
|
|
46
|
+
findings.
|
|
47
|
+
|
|
48
|
+
## 1 — Health-check (`check`)
|
|
49
|
+
|
|
50
|
+
Run from inside the install (the adapter walks up to `wrxn.install.json` — no `--root` needed):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
node .wrxn/harvest.cjs check
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
It scans the 4 tiers and writes a fresh, never-mutated `.wrxn/harvest/<ts>.jsonl`, then prints
|
|
57
|
+
`{ report, summary: { nearDupStatus, findings: { near_dup, decay_candidate, malformed } } }`. Read the
|
|
58
|
+
report file it names. Each record carries enough to seed a proposal:
|
|
59
|
+
|
|
60
|
+
- **`near_dup`** — a cluster of pages over the measured semantic-similarity threshold (`members[]` with
|
|
61
|
+
`slug` / `path` / `tier`, the strongest-edge `score`). The merge candidates.
|
|
62
|
+
- **`decay_candidate`** — `subtype: "orphaned"` (its `derived_from:` source file is gone, carries
|
|
63
|
+
`missing_source`). The decay candidates. Pages already annotated `stale:`/`superseded_by:` are treated
|
|
64
|
+
as resolved and excluded. A supersession is raised via `decay propose` with a replacement target
|
|
65
|
+
(operator/skill judgment), not auto-detected by `check`.
|
|
66
|
+
- **`malformed`** — bad frontmatter (the wiki-lint signal). Report-only here (see §4).
|
|
67
|
+
|
|
68
|
+
If `nearDupStatus` is `"unavailable"`, the recon serve door was cold — near-dup detection was **skipped**
|
|
69
|
+
(the local decay + malformed scans still ran). Tell the operator "near-dup unavailable — start
|
|
70
|
+
`recon-wrxn serve` and re-run `check`"; never read silence as "no duplicates".
|
|
71
|
+
|
|
72
|
+
## 2 — Merge (a `near_dup` cluster → one survivor)
|
|
73
|
+
|
|
74
|
+
A merge folds N near-duplicate pages into **one net-new survivor** and deletes the absorbed originals.
|
|
75
|
+
The survivor is a **fresh** page (a new kebab slug, a path that does not yet exist) — list **every**
|
|
76
|
+
near-dup member in `absorbed`, including any whose name you'd like to reuse (the adapter writes the
|
|
77
|
+
survivor *before* it deletes, so the survivor path can't already exist). The survivor's provenance lands
|
|
78
|
+
as `merged_from: [<absorbed slugs>]` on the surviving page.
|
|
79
|
+
|
|
80
|
+
### The merge reflection rubric (MUST — run it before you stage)
|
|
81
|
+
|
|
82
|
+
You are **merging, not authoring**. Read every absorbed page, then self-check the drafted survivor against
|
|
83
|
+
each one. Stage only if all four hold:
|
|
84
|
+
|
|
85
|
+
1. **No dropped facts.** Every distinct fact in *each* absorbed page appears in the survivor. A merge that
|
|
86
|
+
loses knowledge is worse than no merge.
|
|
87
|
+
2. **No invented facts.** Every survivor line **traces to a source page** — do not add knowledge, dates,
|
|
88
|
+
versions, paths, or claims that were not in an absorbed page. If it isn't in a source, it doesn't belong
|
|
89
|
+
in the survivor.
|
|
90
|
+
3. **Union of evidence preserved.** Every evidence quote, citation, and provenance marker from each
|
|
91
|
+
absorbed page is carried into the survivor — the merge is loss-free on evidence, not just on prose.
|
|
92
|
+
4. **No secrets.** Redact any credential that surfaced (the gate also rejects `contains_secret`, but you
|
|
93
|
+
are the first filter).
|
|
94
|
+
|
|
95
|
+
If any check fails, fix the survivor and re-run the rubric. If the cluster members are **not** actually
|
|
96
|
+
duplicates (your judgment overrides the threshold), **abstain** — stage nothing for that cluster.
|
|
97
|
+
|
|
98
|
+
### Stage the merge (PROPOSE)
|
|
99
|
+
|
|
100
|
+
Write the proposal to a throwaway temp file and stage it by-reference (the live pages are untouched):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
node .wrxn/harvest.cjs stage /tmp/harvest-merge.json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```jsonc
|
|
107
|
+
{
|
|
108
|
+
"survivor": ".wrxn/wiki/concepts/auth-flow.md", // NEW path under a knowledge tier, .md, kebab slug
|
|
109
|
+
"description": "one-line page description", // becomes the survivor's frontmatter description
|
|
110
|
+
"body": "# Auth flow\n\n…the synthesised union…", // the rubric-checked survivor markdown
|
|
111
|
+
"absorbed": [".wrxn/wiki/concepts/login-flow.md", ".wrxn/wiki/concepts/auth-overview.md"]
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`stage` path-confines the survivor + every absorbed target to a knowledge tier, rejects a body over the
|
|
116
|
+
cap, secret-scans, then records the proposal + an integrity hash under `.wrxn/harvest/staged.jsonl`. It
|
|
117
|
+
prints `{ staged, survivor, absorbed, stagedFile }` and **writes/deletes nothing**.
|
|
118
|
+
|
|
119
|
+
### Present, then confirm
|
|
120
|
+
|
|
121
|
+
Show the operator the **diff**: the absorbed pages (read them at the report's paths) and the one survivor
|
|
122
|
+
that replaces them, naming exactly which pages will be **deleted**. Wait for confirmation. If the operator
|
|
123
|
+
approves none, you are done — commit nothing.
|
|
124
|
+
|
|
125
|
+
### Commit the merge (CONFIRM, by reference)
|
|
126
|
+
|
|
127
|
+
Build a JSON array of the approved **survivor paths** (not a rebuilt proposal) and commit:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
node .wrxn/harvest.cjs commit /tmp/harvest-approved.json # [".wrxn/wiki/concepts/auth-flow.md"] (or {"approved":[…]})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`commit` looks up each approved survivor's staged merge, **re-runs the gate** (secret-scan → integrity →
|
|
134
|
+
path-confine survivor + every absorbed → survivor is new), then **writes the survivor first** (knowledge
|
|
135
|
+
preserved) and **only then deletes each absorbed** (survivor-before-delete). It prints `{ merged, skipped }`.
|
|
136
|
+
An **empty** approval is the decline — nothing changes.
|
|
137
|
+
|
|
138
|
+
## 3 — Decay / supersession (a `decay_candidate` → annotate, never delete)
|
|
139
|
+
|
|
140
|
+
Decay is **non-destructive**: it stamps a single forward-link key into a page's frontmatter (the body is
|
|
141
|
+
byte-for-byte preserved) so Recall and the operator know its status. Provenance survives — decay never
|
|
142
|
+
deletes (deletion is merge's job alone). Two kinds:
|
|
143
|
+
|
|
144
|
+
- **`stale: <missing-source-path>`** — an orphaned page whose `derived_from:` source file is gone.
|
|
145
|
+
Mechanical: `decay propose` **auto-derives** these from the same scan `check` ran — no draft needed.
|
|
146
|
+
- **`superseded_by: <path>`** — a page replaced by another. A **judgment** (auto-scan can't invent the
|
|
147
|
+
replacement) — you draft it into a proposal file.
|
|
148
|
+
|
|
149
|
+
### Propose the decay (STAGE)
|
|
150
|
+
|
|
151
|
+
Auto-scan only (stages a `stale:` annotation for every orphaned page):
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
node .wrxn/harvest.cjs decay propose
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Or pass a draft file to add `superseded_by:` judgments (it overrides the auto stale for the same page):
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
node .wrxn/harvest.cjs decay propose /tmp/harvest-decay.json
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```jsonc
|
|
164
|
+
// one object, an array, or { "proposals": [ … ] }; key ∈ {stale, superseded_by}; value is a path
|
|
165
|
+
{ "page": ".wrxn/wiki/gotchas/old-auth-bug.md", "key": "superseded_by",
|
|
166
|
+
"value": ".wrxn/wiki/gotchas/new-auth-bug.md", "reason": "replaced by the post-refactor write-up" }
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`decay propose` gates each candidate — page confined + present, key allowlisted, value sanitised +
|
|
170
|
+
secret-scanned, the page **not reinforced** (a page surfaced in Recall within the last 30 days is live
|
|
171
|
+
knowledge and is skipped), and **not already annotated** (idempotent) — and records survivors to
|
|
172
|
+
`.wrxn/harvest/decay-staged.jsonl`. It prints `{ staged, skipped, stagedFile }`. A `skipped` entry names
|
|
173
|
+
its reason (`reinforced`, `already_annotated`, `unsafe_page`, …).
|
|
174
|
+
|
|
175
|
+
### Present, then confirm
|
|
176
|
+
|
|
177
|
+
Show the operator each staged annotation — the page, the key, the value, and the reason. Wait.
|
|
178
|
+
|
|
179
|
+
### Confirm the decay (COMMIT, by reference)
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
node .wrxn/harvest.cjs decay confirm /tmp/harvest-decay-approved.json # ["…/old-auth-bug.md"] (or {"approved":[…]})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`decay confirm` re-gates each approved page and stamps the one frontmatter key in place (body preserved,
|
|
186
|
+
never deleted). It prints `{ annotated, skipped }`. An empty approval is the decline.
|
|
187
|
+
|
|
188
|
+
## 4 — Malformed pages (report-only)
|
|
189
|
+
|
|
190
|
+
`check` also reports pages with bad frontmatter. harvest does **not** auto-fix them — surface the list to
|
|
191
|
+
the operator to repair by hand (the wiki-lint Stop hook flags the same signal). Fixing frontmatter is
|
|
192
|
+
authoring, outside harvest's gated destructive scope.
|
|
193
|
+
|
|
194
|
+
## Boundaries
|
|
195
|
+
|
|
196
|
+
- **Curation only, on the 4 knowledge tiers.** Never the retired `sessions` tier, never `_slots`
|
|
197
|
+
(the focus slot is dream's), never code.
|
|
198
|
+
- **Confirm-gated + re-checked.** Every merge and decay is staged, presented, and confirmed by reference;
|
|
199
|
+
the gate re-validates at the write boundary, so a rejected/tampered proposal can never mutate a page.
|
|
200
|
+
- **Merge is the only hard-delete.** Decay forward-links / flags; it never deletes. No free-form delete
|
|
201
|
+
path exists — only a staged cluster's absorbed members are ever removed.
|
|
202
|
+
- **Restraint.** A clean health-check is a success: say so, stage nothing, commit nothing.
|
|
203
|
+
- **Never autonomous.** harvest is deliberate and operator-confirmed — never a background sweep.
|
|
204
|
+
|
|
205
|
+
## Source
|
|
206
|
+
|
|
207
|
+
WRXN Kernel issue harvest-06, integrating harvest-02 (`check`), harvest-03 (merge `stage` / `commit`),
|
|
208
|
+
and harvest-04 (`decay propose` / `confirm`). Adapter: `.wrxn/harvest.cjs`. Skill + adapter shape and the
|
|
209
|
+
propose→confirm-by-reference spine adapted from `dream` (`SKILL.md`, `.wrxn/dream.cjs`) and the `sync`
|
|
210
|
+
report→propose→confirm walkthrough. ADR 0005; PRD `harvest-prd`.
|
package/payload/.wrxn/dream.cjs
CHANGED
|
@@ -146,6 +146,43 @@ function normalizeTitle(t) {
|
|
|
146
146
|
return String(t == null ? '' : t).toLowerCase().replace(/\s+/g, ' ').trim();
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
// ── importance stamp — the decay-weight PRODUCER (harvest-10) ──────────────────
|
|
150
|
+
// dream already scores each page (`confidence`, gate floor 0.75) but never PERSISTED it, so recon
|
|
151
|
+
// D1/D3's `recency × importance` recall weight collapsed to `recency × tier-prior`. On commit we persist
|
|
152
|
+
// that EXISTING score as a single `importance:` frontmatter scalar — we do NOT recompute or invent a new
|
|
153
|
+
// model. Clamped to [0,1] and rendered through Number() so it is one bare scalar that cannot inject a
|
|
154
|
+
// newline/colon (AC4, same discipline as sync.cjs's `synced_to:` watermark).
|
|
155
|
+
function clamp01(score) {
|
|
156
|
+
const n = Number(score);
|
|
157
|
+
if (Number.isNaN(n)) return 0; // a non-numeric/garbage score floors to 0 — never a raw string in the page
|
|
158
|
+
return Math.max(0, Math.min(1, n));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// PURE in-place stamp (mirrors sync.cjs restampDoc): update `importance:` when present (no duplicate key),
|
|
162
|
+
// else append one frontmatter line; the body after the closing fence is preserved byte-for-byte (no churn).
|
|
163
|
+
// A page with no frontmatter fence is returned unchanged (defensive — wiki pages always carry one).
|
|
164
|
+
function stampImportance(content, score) {
|
|
165
|
+
const text = String(content);
|
|
166
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
167
|
+
if (!m) return text;
|
|
168
|
+
const lines = m[1].split(/\r?\n/);
|
|
169
|
+
const value = clamp01(score);
|
|
170
|
+
let found = false;
|
|
171
|
+
for (let i = 0; i < lines.length; i++) {
|
|
172
|
+
if (/^importance:\s*/.test(lines[i])) { lines[i] = `importance: ${value}`; found = true; break; }
|
|
173
|
+
}
|
|
174
|
+
if (!found) lines.push(`importance: ${value}`);
|
|
175
|
+
// splice ONLY the frontmatter fence back; the body after the closing --- is byte-for-byte preserved.
|
|
176
|
+
return text.slice(0, m.index) + ['---', lines.join('\n'), '---'].join('\n') + text.slice(m.index + m[0].length);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Stamp the just-written wiki page in place (read → stampImportance → write). The page is reached by its
|
|
180
|
+
// tier+slug under .wrxn/wiki/ — the same page wiki.cjs just wrote — so the stamp never re-routes the write.
|
|
181
|
+
function stampPageImportance(root, tier, slug, score) {
|
|
182
|
+
const file = path.join(root, '.wrxn', 'wiki', tier, `${slug}.md`);
|
|
183
|
+
fs.writeFileSync(file, stampImportance(fs.readFileSync(file, 'utf8'), score));
|
|
184
|
+
}
|
|
185
|
+
|
|
149
186
|
// ── anti-superstition negative filters ────────────────────────────────────────
|
|
150
187
|
// A mechanical backstop to the dream skill's prompt (the skill is the primary semantic filter; this is
|
|
151
188
|
// "reinforced where mechanical"). Each pattern catches a class of transient/false "memory" that, if
|
|
@@ -407,6 +444,7 @@ function runCommit() {
|
|
|
407
444
|
}
|
|
408
445
|
try {
|
|
409
446
|
const r = wikiWritePage(root, p.tier, p.slug, p.title, p.body);
|
|
447
|
+
stampPageImportance(root, p.tier, p.slug, p.confidence); // persist dream's score as importance: (harvest-10)
|
|
410
448
|
written.push({ slug: p.slug, tier: p.tier, file: r.written });
|
|
411
449
|
} catch (err) {
|
|
412
450
|
// wiki.cjs write-page does process.exit(2) on an existing page — catch the non-zero exit so a
|
|
@@ -465,4 +503,8 @@ function main() {
|
|
|
465
503
|
}
|
|
466
504
|
}
|
|
467
505
|
|
|
468
|
-
main
|
|
506
|
+
if (require.main === module) {
|
|
507
|
+
main();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
module.exports = { stampImportance };
|