wyrm-mcp 7.2.0 → 7.2.2

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.
Files changed (156) hide show
  1. package/LICENSE +26 -667
  2. package/NOTICE +14 -33
  3. package/dist/activation.d.ts.map +1 -1
  4. package/dist/activation.js +1 -44
  5. package/dist/activation.js.map +1 -1
  6. package/dist/agent-daemon.js +4 -281
  7. package/dist/agent-loop.js +7 -332
  8. package/dist/analytics.js +13 -236
  9. package/dist/attribution.js +1 -49
  10. package/dist/audit.js +2 -457
  11. package/dist/auto-capture.js +3 -138
  12. package/dist/auto-orchestrator.js +1 -325
  13. package/dist/autoconfig.js +39 -840
  14. package/dist/buddy-runner.js +1 -109
  15. package/dist/buddy.js +14 -564
  16. package/dist/build-flags.js +1 -17
  17. package/dist/capabilities.js +3 -183
  18. package/dist/capture.js +1 -56
  19. package/dist/causality.js +6 -107
  20. package/dist/cli.js +20 -281
  21. package/dist/cloud/cli.js +5 -541
  22. package/dist/cloud/client.js +1 -221
  23. package/dist/cloud/crypto.js +1 -85
  24. package/dist/cloud/machine-id.js +2 -113
  25. package/dist/cloud/recovery.js +1 -60
  26. package/dist/cloud/sync-engine.js +7 -543
  27. package/dist/cloud-backup.js +5 -579
  28. package/dist/cloud-profile.js +1 -138
  29. package/dist/cloud-sync-entrypoint.js +1 -47
  30. package/dist/cloud-sync.js +2 -309
  31. package/dist/constellation.js +12 -168
  32. package/dist/context-build-budgeted.js +4 -144
  33. package/dist/context-ranking.js +1 -69
  34. package/dist/crypto.js +1 -179
  35. package/dist/daemon-write-endpoint.js +1 -290
  36. package/dist/daemon-writer.js +2 -406
  37. package/dist/database.js +43 -1110
  38. package/dist/deprecations.js +2 -162
  39. package/dist/design.js +13 -141
  40. package/dist/event-replication.js +1 -112
  41. package/dist/events-sse.js +7 -43
  42. package/dist/events.js +6 -238
  43. package/dist/failure-patterns.js +42 -659
  44. package/dist/federation.js +12 -236
  45. package/dist/goals.js +13 -101
  46. package/dist/golden.js +3 -355
  47. package/dist/handlers/agent.js +4 -165
  48. package/dist/handlers/alias-adapters.js +1 -129
  49. package/dist/handlers/aliases.js +1 -171
  50. package/dist/handlers/audit.js +1 -87
  51. package/dist/handlers/boundary.js +1 -221
  52. package/dist/handlers/capture.js +73 -1109
  53. package/dist/handlers/causality.js +7 -114
  54. package/dist/handlers/cloud.js +85 -382
  55. package/dist/handlers/companion.js +28 -459
  56. package/dist/handlers/datalake.js +7 -187
  57. package/dist/handlers/dispatch-context.js +0 -22
  58. package/dist/handlers/entity.js +25 -256
  59. package/dist/handlers/events.js +16 -335
  60. package/dist/handlers/failure.js +13 -340
  61. package/dist/handlers/goals.js +4 -296
  62. package/dist/handlers/intelligence.js +126 -674
  63. package/dist/handlers/invoicing.js +1 -70
  64. package/dist/handlers/mcpclient.js +6 -137
  65. package/dist/handlers/orchestration.js +40 -125
  66. package/dist/handlers/output-schemas.js +1 -24
  67. package/dist/handlers/presence.js +3 -99
  68. package/dist/handlers/project.js +28 -182
  69. package/dist/handlers/prompts.js +6 -157
  70. package/dist/handlers/quest.js +4 -224
  71. package/dist/handlers/recall.js +11 -218
  72. package/dist/handlers/registry.js +1 -167
  73. package/dist/handlers/resources.js +1 -288
  74. package/dist/handlers/review.js +11 -74
  75. package/dist/handlers/run.js +17 -487
  76. package/dist/handlers/search.js +15 -326
  77. package/dist/handlers/session.js +28 -615
  78. package/dist/handlers/share.js +8 -184
  79. package/dist/handlers/shims.js +1 -464
  80. package/dist/handlers/skill.js +67 -449
  81. package/dist/handlers/survivors.js +1 -120
  82. package/dist/handlers/symbols.js +8 -109
  83. package/dist/handlers/syncops.js +4 -302
  84. package/dist/handlers/types.js +1 -27
  85. package/dist/harvest.js +5 -191
  86. package/dist/hours.js +7 -156
  87. package/dist/http-auth.js +3 -321
  88. package/dist/http-fast.js +21 -1137
  89. package/dist/icons.js +1 -47
  90. package/dist/index.js +2 -924
  91. package/dist/indexer.js +4 -145
  92. package/dist/intelligence.js +31 -261
  93. package/dist/internal-dispatch.js +3 -212
  94. package/dist/keyset.js +1 -110
  95. package/dist/knowledge-graph.js +12 -176
  96. package/dist/license.d.ts +11 -0
  97. package/dist/license.d.ts.map +1 -1
  98. package/dist/license.js +2 -414
  99. package/dist/license.js.map +1 -1
  100. package/dist/logger.js +2 -199
  101. package/dist/maintenance.js +2 -148
  102. package/dist/mcp-client.js +6 -262
  103. package/dist/memory-artifacts.js +30 -449
  104. package/dist/migrate-prompt.js +2 -124
  105. package/dist/migrations.js +40 -655
  106. package/dist/performance.js +1 -228
  107. package/dist/presence.js +11 -140
  108. package/dist/priority-embed.js +5 -164
  109. package/dist/providers/embedding-provider.js +1 -196
  110. package/dist/readonly-gate.js +1 -29
  111. package/dist/rehydration.js +9 -157
  112. package/dist/reindex.js +1 -88
  113. package/dist/render-target.js +21 -514
  114. package/dist/render.js +4 -280
  115. package/dist/repl-guard.js +1 -173
  116. package/dist/replication-daemon-entrypoint.js +1 -31
  117. package/dist/replication-daemon.js +2 -262
  118. package/dist/resilience.js +1 -591
  119. package/dist/reverse-bridge.js +5 -360
  120. package/dist/security.js +1 -244
  121. package/dist/session-seen.js +3 -51
  122. package/dist/setup.js +1 -260
  123. package/dist/skill-author.js +5 -168
  124. package/dist/spec-kit.js +1 -191
  125. package/dist/sqlite-busy.js +1 -154
  126. package/dist/statusline.js +11 -315
  127. package/dist/sub-agent.js +13 -262
  128. package/dist/summarizer.js +13 -139
  129. package/dist/symbols.js +7 -283
  130. package/dist/sync.js +5 -359
  131. package/dist/tasks-dispatch.js +1 -84
  132. package/dist/tasks.js +1 -282
  133. package/dist/token-budget.js +1 -143
  134. package/dist/tool-analytics.js +7 -129
  135. package/dist/tool-annotations.js +1 -365
  136. package/dist/tool-manifest-v2.json +1 -1
  137. package/dist/tool-manifest.json +1 -1
  138. package/dist/tool-profiles.js +1 -75
  139. package/dist/trace-harvest.js +6 -244
  140. package/dist/types.js +1 -30
  141. package/dist/ui-dashboard.js +41 -50
  142. package/dist/ulid.js +1 -81
  143. package/dist/validate.js +1 -129
  144. package/dist/vault.js +1 -534
  145. package/dist/vectors.js +3 -184
  146. package/dist/version-check.js +4 -136
  147. package/dist/visibility.js +19 -155
  148. package/dist/wyrm-cli.js +98 -2451
  149. package/dist/wyrm-cli.js.map +1 -1
  150. package/dist/wyrm-guard.js +14 -424
  151. package/dist/wyrm-loop.js +3 -150
  152. package/dist/wyrm-manifest.json +1 -1
  153. package/dist/wyrm-statusline-daemon.js +1 -11
  154. package/dist/wyrm-statusline.js +4 -56
  155. package/dist/wyrm-ui.js +9 -77
  156. package/package.json +4 -2
@@ -1,72 +1,10 @@
1
- /**
2
- * Wyrm Render Target (v7 F4 — T038) — the deterministic DB→memory-file compiler.
3
- *
4
- * `wyrm render` is the ZERO-MCP-TOKEN path for casual sessions: instead of an
5
- * agent spending tool calls to load context, Wyrm compiles the project's
6
- * authoritative state (ground truths, open quests, validated patterns,
7
- * unresolved failures) straight into the harness-native memory slot —
8
- * `MEMORY.md` (hard 200-line budget) + per-topic files + a lean SessionStart
9
- * brief — plus per-client adapters (Claude Code / Cursor / Copilot / AGENTS.md).
10
- *
11
- * Design law (this module is the NET-NEW, TEMPLATE-ISOLATED writer per the spec):
12
- * - DETERMINISTIC / BYTE-STABLE: there is NO Date.now()/Math.random() in the
13
- * OUTPUT. Every volatile value (the Wyrm version, the compiled-at stamp, the
14
- * artifact count) is PASSED IN via {@link RenderStamp}. Same model + same
15
- * stamp ⇒ byte-identical bytes, forever (golden-replayable).
16
- * - PROVENANCE-STAMPED: every emitted file carries a header that says it was
17
- * compiled by Wyrm and that edits are HARVESTED, not lost (the reverse bridge,
18
- * T039, picks them up). Wyrm-managed regions are MARKER-BOUNDED so a writer
19
- * NEVER clobbers operator prose outside the markers (the autoconfig.ts
20
- * idempotent-block discipline, reused here).
21
- * - SAFE (Article VII): {@link writeRenderTarget} refuses to escape its target
22
- * directory (no `..`, no absolute re-roots) and refuses to overwrite a file
23
- * that is NOT Wyrm-managed unless explicitly forced — a human's hand-written
24
- * MEMORY.md is never silently destroyed.
25
- * - OFFLINE (Article III): pure DB read + string building. No network, no LLM,
26
- * no clock of its own.
27
- *
28
- * The DB read is injected ({@link RenderDeps}) so the COMPILER is unit-testable
29
- * against an in-memory fixture, exactly like harvest.ts.
30
- *
31
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
32
- * @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
33
- */
34
- import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'fs';
35
- import { dirname, isAbsolute, join, relative, resolve, sep } from 'path';
36
- // ──────────────────────────────────────────────────────────────────────────
37
- // Budgets & markers
38
- // ──────────────────────────────────────────────────────────────────────────
39
- /** MEMORY.md hard line budget (spec FR-6). The compiler trims to fit. */
40
- export const MEMORY_MD_LINE_BUDGET = 200;
41
- /** Marker delimiting the Wyrm-managed region inside any rendered file. */
42
- export const RENDER_MARKER_START = '<!-- wyrm:render:start -->';
43
- export const RENDER_MARKER_END = '<!-- wyrm:render:end -->';
44
- /**
45
- * Default DB-backed deps. Pure SQL, bound params, deterministic ORDER BY on a
46
- * TOTAL order (every query appends an id/key tiebreak so two rows never tie on
47
- * the wire — the bare-sort-key trap). All listings are hard-capped.
48
- */
49
- export function makeRenderDeps(db) {
50
- return {
51
- truths(projectId) {
52
- const rows = db.prepare(`
1
+ import{existsSync as E,lstatSync as T,mkdirSync as k,readFileSync as D,realpathSync as N,writeFileSync as O}from"fs";import{dirname as _,isAbsolute as $,join as f,relative as w,resolve as R,sep as S}from"path";const W=200,m="<!-- wyrm:render:start -->",h="<!-- wyrm:render:end -->";function P(e){return{truths(r){return e.prepare(`
53
2
  SELECT category, key, value, rationale, confidence, ttl_days,
54
3
  CAST((julianday('now') - julianday(last_verified_at)) AS REAL) AS age_days
55
4
  FROM ground_truths
56
5
  WHERE project_id = ? AND is_current = 1
57
6
  ORDER BY category, key
58
- `).all(projectId);
59
- return rows.map((r) => ({
60
- category: r.category,
61
- key: r.key,
62
- value: r.value,
63
- rationale: r.rationale,
64
- confidence: r.confidence,
65
- stale: r.ttl_days != null && r.age_days > r.ttl_days,
66
- }));
67
- },
68
- quests(projectId) {
69
- return db.prepare(`
7
+ `).all(r).map(t=>({category:t.category,key:t.key,value:t.value,rationale:t.rationale,confidence:t.confidence,stale:t.ttl_days!=null&&t.age_days>t.ttl_days}))},quests(r){return e.prepare(`
70
8
  SELECT id, title, description, priority
71
9
  FROM quests
72
10
  WHERE project_id = ? AND status IN ('pending','in_progress')
@@ -75,461 +13,30 @@ export function makeRenderDeps(db) {
75
13
  WHEN 'medium' THEN 2 ELSE 3 END,
76
14
  id ASC
77
15
  LIMIT 100
78
- `).all(projectId);
79
- },
80
- artifacts(projectId) {
81
- try {
82
- return db.prepare(`
16
+ `).all(r)},artifacts(r){try{return e.prepare(`
83
17
  SELECT id, problem, validated_fix
84
18
  FROM memory_artifacts
85
19
  WHERE project_id = ? AND (needs_review = 0 OR needs_review IS NULL)
86
20
  ORDER BY id DESC LIMIT 50
87
- `).all(projectId);
88
- }
89
- catch {
90
- return [];
91
- }
92
- },
93
- failures(projectId) {
94
- try {
95
- return db.prepare(`
21
+ `).all(r)}catch{return[]}},failures(r){try{return e.prepare(`
96
22
  SELECT id, scope, target, description, severity, occurrences
97
23
  FROM failure_patterns
98
24
  WHERE (project_id = ? OR project_id IS NULL) AND resolved = 0
99
25
  ORDER BY last_seen DESC, id DESC LIMIT 50
100
- `).all(projectId);
101
- }
102
- catch {
103
- return [];
104
- }
105
- },
106
- };
107
- }
108
- /** Compile the full structured model for a project (pure given deps). */
109
- export function compileRenderModel(deps, project) {
110
- return {
111
- project_id: project.id,
112
- project_name: project.name,
113
- project_path: project.path,
114
- truths: deps.truths(project.id),
115
- quests: deps.quests(project.id),
116
- artifacts: deps.artifacts(project.id),
117
- failures: deps.failures(project.id),
118
- };
119
- }
120
- // ──────────────────────────────────────────────────────────────────────────
121
- // Provenance
122
- // ──────────────────────────────────────────────────────────────────────────
123
- function artifactCount(m) {
124
- return m.truths.length + m.quests.length + m.artifacts.length + m.failures.length;
125
- }
126
- /**
127
- * The provenance lines. Deterministic given (model, stamp). States plainly that
128
- * edits are HARVESTED, not lost (the reverse bridge picks up changes between the
129
- * markers and routes them to the review queue, T039).
130
- */
131
- export function provenanceHeader(m, stamp) {
132
- const n = artifactCount(m);
133
- return [
134
- `<!-- Compiled by Wyrm v${stamp.wyrm_version} from ${n} memory artifact${n === 1 ? '' : 's'}.`,
135
- ` Project: ${m.project_name}. Compiled at ${stamp.compiled_at}.`,
136
- ` Edits inside the wyrm:render markers are HARVESTED to the review queue, not lost —`,
137
- ` but they are OVERWRITTEN on the next \`wyrm render\`; capture durable notes with \`wyrm capture\`. -->`,
138
- ];
139
- }
140
- // ──────────────────────────────────────────────────────────────────────────
141
- // MEMORY.md — the 200-line-budgeted master file.
142
- // ──────────────────────────────────────────────────────────────────────────
143
- /** Sanitise a one-line cell: collapse newlines so a value never spends >1 line. */
144
- function oneLine(s) {
145
- return s.replace(/\s*\n\s*/g, ' ').trim();
146
- }
147
- /** Truncate to a max length with an ellipsis (deterministic). */
148
- function clip(s, max) {
149
- return s.length > max ? `${s.slice(0, max - 1)}…` : s;
150
- }
151
- /**
152
- * Render MEMORY.md body (the lines BETWEEN the markers, provenance included).
153
- * Hard 200-line budget: sections are emitted in priority order and the LAST
154
- * section to overflow is truncated with an explicit "N more" note, so the file
155
- * never exceeds the budget no matter how large the corpus.
156
- *
157
- * Returns the full marker-bounded block (markers + provenance + body), ready to
158
- * splice into a file by {@link spliceWyrmRegion}.
159
- */
160
- export function renderMemoryMd(m, stamp) {
161
- const header = provenanceHeader(m, stamp);
162
- // The marker + provenance lines count against NOTHING extra here; the budget
163
- // governs the human-readable body. We reserve a few lines for the title and
164
- // the footer so the total stays at/under the budget.
165
- const body = [];
166
- body.push(`# ${m.project_name} — Project Memory`);
167
- body.push('');
168
- // Footer is always 2 lines; title+blank is 2 lines. Keep the content within
169
- // the remaining budget.
170
- const reserved = 4;
171
- const contentBudget = MEMORY_MD_LINE_BUDGET - reserved;
172
- const sections = [];
173
- if (m.truths.length > 0) {
174
- sections.push({
175
- heading: `## Ground truths (${m.truths.length})`,
176
- refTool: 'wyrm_truth_get',
177
- lines: m.truths.map((t) => {
178
- const marker = t.stale ? ' [STALE]' : '';
179
- const conf = t.confidence < 1 ? ` (conf ${t.confidence})` : '';
180
- return `- **${t.category}.${t.key}**${marker}: ${clip(oneLine(t.value), 160)}${conf}`;
181
- }),
182
- });
183
- }
184
- if (m.failures.length > 0) {
185
- sections.push({
186
- heading: `## Failures — DO NOT repeat (${m.failures.length})`,
187
- refTool: 'wyrm_failure_check',
188
- lines: m.failures.map((f) => `- [${f.severity}] ${f.scope}:${f.target} ×${f.occurrences} — ${clip(oneLine(f.description), 140)}`),
189
- });
190
- }
191
- if (m.quests.length > 0) {
192
- sections.push({
193
- heading: `## Open quests (${m.quests.length})`,
194
- refTool: 'wyrm_quest',
195
- lines: m.quests.map((q) => `- #${q.id} [${q.priority}] ${clip(oneLine(q.title), 140)}`),
196
- });
197
- }
198
- if (m.artifacts.length > 0) {
199
- sections.push({
200
- heading: `## Validated patterns (${m.artifacts.length})`,
201
- refTool: 'wyrm_recall',
202
- lines: m.artifacts.map((a) => `- ${clip(oneLine(a.problem), 120)}${a.validated_fix ? ` → ${clip(oneLine(a.validated_fix), 120)}` : ''}`),
203
- });
204
- }
205
- // Greedily emit sections within the budget. Each section costs: heading + a
206
- // blank line + N item lines + a trailing blank. When a section would overflow,
207
- // emit as many items as fit and a single "N more — query <tool>" line.
208
- let used = 0;
209
- for (const s of sections) {
210
- // Minimum cost to open a section: heading + blank + at least the "more" note
211
- // + trailing blank = 4 lines. If we can't even do that, stop.
212
- if (used + 4 > contentBudget)
213
- break;
214
- body.push(s.heading);
215
- body.push('');
216
- used += 2;
217
- let emitted = 0;
218
- for (const line of s.lines) {
219
- // Reserve 1 line for a potential "more" note + 1 trailing blank.
220
- if (used + 1 + (emitted < s.lines.length ? 1 : 0) + 1 > contentBudget)
221
- break;
222
- body.push(line);
223
- used += 1;
224
- emitted += 1;
225
- }
226
- if (emitted < s.lines.length) {
227
- body.push(`- _… ${s.lines.length - emitted} more — query \`${s.refTool}\`._`);
228
- used += 1;
229
- }
230
- body.push('');
231
- used += 1;
232
- }
233
- body.push('---');
234
- body.push(`*Full memory: \`wyrm recall\` / \`wyrm session prime\`. This file is a deterministic digest.*`);
235
- return [...header, ...body].join('\n');
236
- }
237
- /**
238
- * Render the per-topic files (the full, un-budgeted corpus). Each is its own
239
- * marker-bounded, provenance-stamped file. Deterministic given (model, stamp).
240
- */
241
- export function renderTopicFiles(m, stamp) {
242
- const header = provenanceHeader(m, stamp);
243
- const out = [];
244
- const mk = (filename, title, lines) => {
245
- if (lines.length === 0)
246
- return;
247
- out.push({
248
- filename,
249
- content: [...header, `# ${m.project_name} — ${title}`, '', ...lines].join('\n'),
250
- });
251
- };
252
- mk('truths.md', 'Ground truths', m.truths.map((t) => {
253
- const marker = t.stale ? ' [STALE]' : '';
254
- return `- **${t.category}.${t.key}**${marker}: ${oneLine(t.value)}` +
255
- (t.rationale ? `\n - rationale: ${oneLine(t.rationale)}` : '') +
256
- (t.confidence < 1 ? `\n - confidence: ${t.confidence}` : '');
257
- }));
258
- mk('quests.md', 'Open quests', m.quests.map((q) => `- #${q.id} [${q.priority}] ${oneLine(q.title)}` +
259
- (q.description ? `\n - ${oneLine(q.description)}` : '')));
260
- mk('patterns.md', 'Validated patterns', m.artifacts.map((a) => `- ${oneLine(a.problem)}` + (a.validated_fix ? `\n - fix: ${oneLine(a.validated_fix)}` : '')));
261
- mk('failures.md', 'Failures — DO NOT repeat', m.failures.map((f) => `- [${f.severity}] ${f.scope}:${f.target} ×${f.occurrences}\n - ${oneLine(f.description)}`));
262
- return out;
263
- }
264
- // ──────────────────────────────────────────────────────────────────────────
265
- // SessionStart brief — the lean, byte-stable injection payload.
266
- // ──────────────────────────────────────────────────────────────────────────
267
- /**
268
- * The SessionStart brief: a compact, deterministic summary a harness can inject
269
- * at session start with near-zero cost. Caps each section so the brief stays
270
- * lean (it is NOT the full corpus — that's the topic files). Byte-stable.
271
- */
272
- export function renderSessionBrief(m, stamp) {
273
- const lines = [];
274
- lines.push(`Wyrm memory for ${m.project_name} (compiled by Wyrm v${stamp.wyrm_version}):`);
275
- if (m.failures.length > 0) {
276
- lines.push('');
277
- lines.push(`Known failures (do not repeat):`);
278
- for (const f of m.failures.slice(0, 5)) {
279
- lines.push(`- ${f.scope}:${f.target} — ${clip(oneLine(f.description), 120)}`);
280
- }
281
- if (m.failures.length > 5)
282
- lines.push(`- (+${m.failures.length - 5} more — wyrm_failure_check)`);
283
- }
284
- if (m.truths.length > 0) {
285
- lines.push('');
286
- lines.push(`Ground truths:`);
287
- for (const t of m.truths.slice(0, 8)) {
288
- lines.push(`- ${t.category}.${t.key}: ${clip(oneLine(t.value), 120)}`);
289
- }
290
- if (m.truths.length > 8)
291
- lines.push(`- (+${m.truths.length - 8} more — wyrm_truth_get)`);
292
- }
293
- if (m.quests.length > 0) {
294
- lines.push('');
295
- lines.push(`Open quests:`);
296
- for (const q of m.quests.slice(0, 5)) {
297
- lines.push(`- #${q.id} [${q.priority}] ${clip(oneLine(q.title), 100)}`);
298
- }
299
- if (m.quests.length > 5)
300
- lines.push(`- (+${m.quests.length - 5} more — wyrm_quest)`);
301
- }
302
- lines.push('');
303
- lines.push(`Load full context with wyrm_session_prime. (Wyrm is this session's memory; consult it first.)`);
304
- return lines.join('\n');
305
- }
306
- /**
307
- * Per-client memory-file layout. Each adapter targets that client's native
308
- * memory slot with its native filename — no Anthropic-only behaviour leaks into
309
- * the others. The BLOCK content is identical (the MEMORY.md digest); only the
310
- * destination differs, so the digest is one source of truth.
311
- */
312
- export function renderForClient(client, m, stamp) {
313
- const block = renderMemoryMd(m, stamp);
314
- switch (client) {
315
- case 'claude':
316
- // Claude Code reads a project-root CLAUDE.md (or MEMORY.md). We target
317
- // CLAUDE.md as the canonical Claude Code project memory slot.
318
- return { client, relPath: 'CLAUDE.md', block };
319
- case 'cursor':
320
- return { client, relPath: join('.cursor', 'rules', 'wyrm-memory.md'), block };
321
- case 'copilot':
322
- return { client, relPath: join('.github', 'copilot-instructions.md'), block };
323
- case 'agents':
324
- return { client, relPath: 'AGENTS.md', block };
325
- default: {
326
- const never = client;
327
- throw new Error(`unknown render client: ${String(never)}`);
328
- }
329
- }
330
- }
331
- // ──────────────────────────────────────────────────────────────────────────
332
- // Marker splicing + safe writing.
333
- // ──────────────────────────────────────────────────────────────────────────
334
- /**
335
- * Splice a marker-bounded Wyrm block into existing file content WITHOUT touching
336
- * operator prose outside the markers. If a marker region already exists it is
337
- * replaced; otherwise the block is appended. Returns the new file content.
338
- *
339
- * The returned content wraps `block` in the start/end markers exactly once.
340
- */
341
- export function spliceWyrmRegion(existing, block) {
342
- const region = `${RENDER_MARKER_START}\n${block}\n${RENDER_MARKER_END}`;
343
- if (!existing)
344
- return `${region}\n`;
345
- const startIdx = existing.indexOf(RENDER_MARKER_START);
346
- const endIdx = existing.indexOf(RENDER_MARKER_END);
347
- if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
348
- const before = existing.slice(0, startIdx);
349
- const after = existing.slice(endIdx + RENDER_MARKER_END.length);
350
- return `${before}${region}${after}`;
351
- }
352
- const sep = existing.endsWith('\n') ? '\n' : '\n\n';
353
- return `${existing}${sep}${region}\n`;
354
- }
355
- /** Does this file content already carry a Wyrm-managed region? */
356
- export function hasWyrmRegion(content) {
357
- const s = content.indexOf(RENDER_MARKER_START);
358
- const e = content.indexOf(RENDER_MARKER_END);
359
- return s !== -1 && e !== -1 && e > s;
360
- }
361
- export class RenderPathError extends Error {
362
- }
363
- const NODE_STAT_FS = { existsSync, lstatSync, realpathSync };
364
- /**
365
- * Resolve `relPath` strictly inside `rootDir`. Rejects absolute paths and any
366
- * traversal that escapes the root (Article VII: a writer/watcher must never
367
- * escape its target dir). Returns the absolute, validated path.
368
- *
369
- * The lexical `resolve`/`relative` gate blocks `..` traversal, but it does NOT
370
- * stop a SYMLINK that lives inside the root and points outside it — `writeFile`/
371
- * `readFile` follow symlinks, so a target like `MEMORY.md` symlinked to
372
- * `~/.ssh/config` would be written/read THROUGH the link, escaping the dir. So
373
- * we additionally:
374
- * 1. refuse when the existing target is itself a symlink (lstat isSymbolicLink),
375
- * 2. re-assert the REAL path of the target (or its nearest existing ancestor,
376
- * for a not-yet-created target) is inside the REAL path of the root —
377
- * defeating an in-root symlinked ancestor directory.
378
- * The stat surface is injected so the symlink lens is unit-testable without a
379
- * live filesystem.
380
- */
381
- export function resolveInsideRoot(rootDir, relPath, statFs = NODE_STAT_FS) {
382
- if (isAbsolute(relPath)) {
383
- throw new RenderPathError(`render target must be relative, got absolute: ${relPath}`);
384
- }
385
- const root = resolve(rootDir);
386
- const target = resolve(root, relPath);
387
- const rel = relative(root, target);
388
- if (rel === '' || rel.startsWith('..') || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
389
- throw new RenderPathError(`render target escapes root: ${relPath}`);
390
- }
391
- // Symlink defence (Article VII). The lexical check above passed, so now guard
392
- // against in-root symlinks that resolve outside the root.
393
- let realRoot;
394
- try {
395
- realRoot = statFs.realpathSync(root);
396
- }
397
- catch {
398
- // Root itself does not resolve (does not exist / broken) — fall back to the
399
- // lexically validated path; nothing on disk to escape through yet.
400
- return target;
401
- }
402
- // 1. The target, if it already exists, must not BE a symlink.
403
- if (statFs.existsSync(target)) {
404
- let isLink = false;
405
- try {
406
- isLink = statFs.lstatSync(target).isSymbolicLink();
407
- }
408
- catch {
409
- isLink = false;
410
- }
411
- if (isLink) {
412
- throw new RenderPathError(`render target is a symlink (refusing to follow): ${relPath}`);
413
- }
414
- }
415
- // 2. The REAL path of the target's nearest existing ancestor must stay inside
416
- // the real root — catches a symlinked intermediate directory.
417
- let probe = target;
418
- // Walk up to the nearest ancestor that exists on disk.
419
- while (!statFs.existsSync(probe)) {
420
- const parent = dirname(probe);
421
- if (parent === probe)
422
- break; // reached filesystem root
423
- probe = parent;
424
- }
425
- let realProbe;
426
- try {
427
- realProbe = statFs.realpathSync(probe);
428
- }
429
- catch {
430
- return target; // ancestor vanished mid-check — degrade to the lexical path
431
- }
432
- const realRel = relative(realRoot, realProbe);
433
- if (realRel === '' && probe === root) {
434
- return target; // the existing ancestor IS the root — fine
435
- }
436
- if (realRel.startsWith('..') || realRel.startsWith(`..${sep}`) || isAbsolute(realRel)) {
437
- throw new RenderPathError(`render target escapes root via symlink: ${relPath}`);
438
- }
439
- return target;
440
- }
441
- const NODE_FS = { existsSync, readFileSync, writeFileSync, mkdirSync };
442
- /**
443
- * Write a marker-bounded block into `rootDir/relPath` safely:
444
- * - the path is validated to stay inside rootDir (no escape),
445
- * - an existing Wyrm region is replaced; operator prose outside is untouched,
446
- * - a file that exists but is NOT Wyrm-managed is SKIPPED unless `force` (then
447
- * the block is APPENDED, never overwriting the human content),
448
- * - the write is idempotent: identical content ⇒ 'skipped' (no churn, lets the
449
- * daemon re-render cheaply).
450
- */
451
- export function writeRenderTarget(rootDir, relPath, block, opts = {}) {
452
- const fs = opts.fs ?? NODE_FS;
453
- const abs = resolveInsideRoot(rootDir, relPath);
454
- const exists = fs.existsSync(abs);
455
- const existing = exists ? fs.readFileSync(abs, 'utf-8') : null;
456
- if (exists && existing != null && !hasWyrmRegion(existing) && !opts.force) {
457
- return { path: abs, action: 'skipped', reason: 'file exists and is not Wyrm-managed (pass force to append)' };
458
- }
459
- const next = spliceWyrmRegion(existing, block);
460
- if (existing != null && next === existing) {
461
- return { path: abs, action: 'skipped', reason: 'unchanged' };
462
- }
463
- fs.mkdirSync(dirname(abs), { recursive: true });
464
- fs.writeFileSync(abs, next, 'utf-8');
465
- return { path: abs, action: exists ? 'updated' : 'created' };
466
- }
467
- /** Build the full deterministic render plan for a project (no I/O). */
468
- export function buildRenderPlan(deps, project, stamp) {
469
- const model = compileRenderModel(deps, project);
470
- return {
471
- model,
472
- memoryMd: renderMemoryMd(model, stamp),
473
- topics: renderTopicFiles(model, stamp),
474
- sessionBrief: renderSessionBrief(model, stamp),
475
- };
476
- }
477
- /**
478
- * Render a project's memory to disk: MEMORY.md + topic files (+ optional client
479
- * adapter files). All writes are safe (no escape, no clobber of human prose).
480
- */
481
- export function renderToDisk(deps, project, stamp, opts = {}) {
482
- const root = opts.rootDir ?? project.path;
483
- const topicsDir = opts.topicsDir ?? join('.wyrm', 'memory');
484
- const plan = buildRenderPlan(deps, project, stamp);
485
- const writes = [];
486
- writes.push(writeRenderTarget(root, 'MEMORY.md', plan.memoryMd, opts));
487
- for (const t of plan.topics) {
488
- writes.push(writeRenderTarget(root, join(topicsDir, t.filename), t.content, { ...opts, force: true }));
489
- }
490
- for (const client of opts.clients ?? []) {
491
- const target = renderForClient(client, plan.model, stamp);
492
- writes.push(writeRenderTarget(root, target.relPath, target.block, opts));
493
- }
494
- return { plan, writes };
495
- }
496
- // ──────────────────────────────────────────────────────────────────────────
497
- // Daemon re-render on Live Memory events (opt-in).
498
- // ──────────────────────────────────────────────────────────────────────────
499
- /**
500
- * Whether daemon re-render is enabled. Off by default (Article VII) — the
501
- * operator opts in with WYRM_RENDER_WATCH=1. Kept as a function so tests and the
502
- * daemon read a fresh value, never a cached one.
503
- */
504
- export function renderWatchEnabled(env = process.env) {
505
- const v = (env.WYRM_RENDER_WATCH ?? '').toLowerCase();
506
- return v === '1' || v === 'true' || v === 'yes';
507
- }
508
- /**
509
- * Tracks a project's Live Memory cursor and decides whether a re-render is due.
510
- * Pure given the cursor inputs — the daemon supplies "the latest cursor I saw"
511
- * and this returns whether it ADVANCED past the last render. No clock, no I/O.
512
- *
513
- * The daemon pattern: keep a RenderCursorTracker per project; on each tick poll
514
- * eventsSince(db, projectId, tracker.last) for the head cursor; call
515
- * tracker.advance(headCursor) — if it returns true, re-render and the tracker
516
- * records the new head as rendered.
517
- */
518
- export class RenderCursorTracker {
519
- rendered;
520
- constructor(initialCursor = 0) { this.rendered = initialCursor; }
521
- /** The cursor value last rendered. */
522
- get last() { return this.rendered; }
523
- /**
524
- * Given the current head cursor, returns true (and records it) iff it is
525
- * strictly greater than the last rendered cursor — i.e. new events arrived
526
- * since the last render. Idempotent: a stale/equal cursor never re-renders.
527
- */
528
- advance(headCursor) {
529
- if (!Number.isFinite(headCursor) || headCursor <= this.rendered)
530
- return false;
531
- this.rendered = headCursor;
532
- return true;
533
- }
534
- }
535
- //# sourceMappingURL=render-target.js.map
26
+ `).all(r)}catch{return[]}}}}function b(e,r){return{project_id:r.id,project_name:r.name,project_path:r.path,truths:e.truths(r.id),quests:e.quests(r.id),artifacts:e.artifacts(r.id),failures:e.failures(r.id)}}function L(e){return e.truths.length+e.quests.length+e.artifacts.length+e.failures.length}function v(e,r){const n=L(e);return[`<!-- Compiled by Wyrm v${r.wyrm_version} from ${n} memory artifact${n===1?"":"s"}.`,` Project: ${e.project_name}. Compiled at ${r.compiled_at}.`," Edits inside the wyrm:render markers are HARVESTED to the review queue, not lost \u2014"," but they are OVERWRITTEN on the next `wyrm render`; capture durable notes with `wyrm capture`. -->"]}function u(e){return e.replace(/\s*\n\s*/g," ").trim()}function p(e,r){return e.length>r?`${e.slice(0,r-1)}\u2026`:e}function x(e,r){const n=v(e,r),t=[];t.push(`# ${e.project_name} \u2014 Project Memory`),t.push("");const s=W-4,c=[];e.truths.length>0&&c.push({heading:`## Ground truths (${e.truths.length})`,refTool:"wyrm_truth_get",lines:e.truths.map(i=>{const l=i.stale?" [STALE]":"",d=i.confidence<1?` (conf ${i.confidence})`:"";return`- **${i.category}.${i.key}**${l}: ${p(u(i.value),160)}${d}`})}),e.failures.length>0&&c.push({heading:`## Failures \u2014 DO NOT repeat (${e.failures.length})`,refTool:"wyrm_failure_check",lines:e.failures.map(i=>`- [${i.severity}] ${i.scope}:${i.target} \xD7${i.occurrences} \u2014 ${p(u(i.description),140)}`)}),e.quests.length>0&&c.push({heading:`## Open quests (${e.quests.length})`,refTool:"wyrm_quest",lines:e.quests.map(i=>`- #${i.id} [${i.priority}] ${p(u(i.title),140)}`)}),e.artifacts.length>0&&c.push({heading:`## Validated patterns (${e.artifacts.length})`,refTool:"wyrm_recall",lines:e.artifacts.map(i=>`- ${p(u(i.problem),120)}${i.validated_fix?` \u2192 ${p(u(i.validated_fix),120)}`:""}`)});let a=0;for(const i of c){if(a+4>s)break;t.push(i.heading),t.push(""),a+=2;let l=0;for(const d of i.lines){if(a+1+(l<i.lines.length?1:0)+1>s)break;t.push(d),a+=1,l+=1}l<i.lines.length&&(t.push(`- _\u2026 ${i.lines.length-l} more \u2014 query \`${i.refTool}\`._`),a+=1),t.push(""),a+=1}return t.push("---"),t.push("*Full memory: `wyrm recall` / `wyrm session prime`. This file is a deterministic digest.*"),[...n,...t].join(`
27
+ `)}function A(e,r){const n=v(e,r),t=[],o=(s,c,a)=>{a.length!==0&&t.push({filename:s,content:[...n,`# ${e.project_name} \u2014 ${c}`,"",...a].join(`
28
+ `)})};return o("truths.md","Ground truths",e.truths.map(s=>{const c=s.stale?" [STALE]":"";return`- **${s.category}.${s.key}**${c}: ${u(s.value)}`+(s.rationale?`
29
+ - rationale: ${u(s.rationale)}`:"")+(s.confidence<1?`
30
+ - confidence: ${s.confidence}`:"")})),o("quests.md","Open quests",e.quests.map(s=>`- #${s.id} [${s.priority}] ${u(s.title)}`+(s.description?`
31
+ - ${u(s.description)}`:""))),o("patterns.md","Validated patterns",e.artifacts.map(s=>`- ${u(s.problem)}`+(s.validated_fix?`
32
+ - fix: ${u(s.validated_fix)}`:""))),o("failures.md","Failures \u2014 DO NOT repeat",e.failures.map(s=>`- [${s.severity}] ${s.scope}:${s.target} \xD7${s.occurrences}
33
+ - ${u(s.description)}`)),t}function M(e,r){const n=[];if(n.push(`Wyrm memory for ${e.project_name} (compiled by Wyrm v${r.wyrm_version}):`),e.failures.length>0){n.push(""),n.push("Known failures (do not repeat):");for(const t of e.failures.slice(0,5))n.push(`- ${t.scope}:${t.target} \u2014 ${p(u(t.description),120)}`);e.failures.length>5&&n.push(`- (+${e.failures.length-5} more \u2014 wyrm_failure_check)`)}if(e.truths.length>0){n.push(""),n.push("Ground truths:");for(const t of e.truths.slice(0,8))n.push(`- ${t.category}.${t.key}: ${p(u(t.value),120)}`);e.truths.length>8&&n.push(`- (+${e.truths.length-8} more \u2014 wyrm_truth_get)`)}if(e.quests.length>0){n.push(""),n.push("Open quests:");for(const t of e.quests.slice(0,5))n.push(`- #${t.id} [${t.priority}] ${p(u(t.title),100)}`);e.quests.length>5&&n.push(`- (+${e.quests.length-5} more \u2014 wyrm_quest)`)}return n.push(""),n.push("Load full context with wyrm_session_prime. (Wyrm is this session's memory; consult it first.)"),n.join(`
34
+ `)}function q(e,r,n){const t=x(r,n);switch(e){case"claude":return{client:e,relPath:"CLAUDE.md",block:t};case"cursor":return{client:e,relPath:f(".cursor","rules","wyrm-memory.md"),block:t};case"copilot":return{client:e,relPath:f(".github","copilot-instructions.md"),block:t};case"agents":return{client:e,relPath:"AGENTS.md",block:t};default:{const o=e;throw new Error(`unknown render client: ${String(o)}`)}}}function C(e,r){const n=`${m}
35
+ ${r}
36
+ ${h}`;if(!e)return`${n}
37
+ `;const t=e.indexOf(m),o=e.indexOf(h);if(t!==-1&&o!==-1&&o>t){const c=e.slice(0,t),a=e.slice(o+h.length);return`${c}${n}${a}`}const s=e.endsWith(`
38
+ `)?`
39
+ `:`
40
+
41
+ `;return`${e}${s}${n}
42
+ `}function I(e){const r=e.indexOf(m),n=e.indexOf(h);return r!==-1&&n!==-1&&n>r}class y extends Error{}const j={existsSync:E,lstatSync:T,realpathSync:N};function H(e,r,n=j){if($(r))throw new y(`render target must be relative, got absolute: ${r}`);const t=R(e),o=R(t,r),s=w(t,o);if(s===""||s.startsWith("..")||s.startsWith(`..${S}`)||$(s))throw new y(`render target escapes root: ${r}`);let c;try{c=n.realpathSync(t)}catch{return o}if(n.existsSync(o)){let d=!1;try{d=n.lstatSync(o).isSymbolicLink()}catch{d=!1}if(d)throw new y(`render target is a symlink (refusing to follow): ${r}`)}let a=o;for(;!n.existsSync(a);){const d=_(a);if(d===a)break;a=d}let i;try{i=n.realpathSync(a)}catch{return o}const l=w(c,i);if(l===""&&a===t)return o;if(l.startsWith("..")||l.startsWith(`..${S}`)||$(l))throw new y(`render target escapes root via symlink: ${r}`);return o}const F={existsSync:E,readFileSync:D,writeFileSync:O,mkdirSync:k};function g(e,r,n,t={}){const o=t.fs??F,s=H(e,r),c=o.existsSync(s),a=c?o.readFileSync(s,"utf-8"):null;if(c&&a!=null&&!I(a)&&!t.force)return{path:s,action:"skipped",reason:"file exists and is not Wyrm-managed (pass force to append)"};const i=C(a,n);return a!=null&&i===a?{path:s,action:"skipped",reason:"unchanged"}:(o.mkdirSync(_(s),{recursive:!0}),o.writeFileSync(s,i,"utf-8"),{path:s,action:c?"updated":"created"})}function B(e,r,n){const t=b(e,r);return{model:t,memoryMd:x(t,n),topics:A(t,n),sessionBrief:M(t,n)}}function U(e,r,n,t={}){const o=t.rootDir??r.path,s=t.topicsDir??f(".wyrm","memory"),c=B(e,r,n),a=[];a.push(g(o,"MEMORY.md",c.memoryMd,t));for(const i of c.topics)a.push(g(o,f(s,i.filename),i.content,{...t,force:!0}));for(const i of t.clients??[]){const l=q(i,c.model,n);a.push(g(o,l.relPath,l.block,t))}return{plan:c,writes:a}}function V(e=process.env){const r=(e.WYRM_RENDER_WATCH??"").toLowerCase();return r==="1"||r==="true"||r==="yes"}class K{rendered;constructor(r=0){this.rendered=r}get last(){return this.rendered}advance(r){return!Number.isFinite(r)||r<=this.rendered?!1:(this.rendered=r,!0)}}export{W as MEMORY_MD_LINE_BUDGET,h as RENDER_MARKER_END,m as RENDER_MARKER_START,K as RenderCursorTracker,y as RenderPathError,B as buildRenderPlan,b as compileRenderModel,I as hasWyrmRegion,P as makeRenderDeps,v as provenanceHeader,q as renderForClient,x as renderMemoryMd,M as renderSessionBrief,U as renderToDisk,A as renderTopicFiles,V as renderWatchEnabled,H as resolveInsideRoot,C as spliceWyrmRegion,g as writeRenderTarget};