wyrm-mcp 7.2.1 → 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 (150) hide show
  1. package/LICENSE +26 -667
  2. package/NOTICE +14 -33
  3. package/dist/activation.js +1 -60
  4. package/dist/agent-daemon.js +4 -281
  5. package/dist/agent-loop.js +7 -332
  6. package/dist/analytics.js +13 -236
  7. package/dist/attribution.js +1 -49
  8. package/dist/audit.js +2 -457
  9. package/dist/auto-capture.js +3 -138
  10. package/dist/auto-orchestrator.js +1 -325
  11. package/dist/autoconfig.js +39 -840
  12. package/dist/buddy-runner.js +1 -109
  13. package/dist/buddy.js +14 -564
  14. package/dist/build-flags.js +1 -17
  15. package/dist/capabilities.js +3 -183
  16. package/dist/capture.js +1 -56
  17. package/dist/causality.js +6 -107
  18. package/dist/cli.js +20 -281
  19. package/dist/cloud/cli.js +5 -541
  20. package/dist/cloud/client.js +1 -221
  21. package/dist/cloud/crypto.js +1 -85
  22. package/dist/cloud/machine-id.js +2 -113
  23. package/dist/cloud/recovery.js +1 -60
  24. package/dist/cloud/sync-engine.js +7 -543
  25. package/dist/cloud-backup.js +5 -579
  26. package/dist/cloud-profile.js +1 -138
  27. package/dist/cloud-sync-entrypoint.js +1 -47
  28. package/dist/cloud-sync.js +2 -309
  29. package/dist/constellation.js +12 -168
  30. package/dist/context-build-budgeted.js +4 -144
  31. package/dist/context-ranking.js +1 -69
  32. package/dist/crypto.js +1 -179
  33. package/dist/daemon-write-endpoint.js +1 -290
  34. package/dist/daemon-writer.js +2 -406
  35. package/dist/database.js +43 -1110
  36. package/dist/deprecations.js +2 -162
  37. package/dist/design.js +13 -141
  38. package/dist/event-replication.js +1 -112
  39. package/dist/events-sse.js +7 -43
  40. package/dist/events.js +6 -238
  41. package/dist/failure-patterns.js +42 -659
  42. package/dist/federation.js +12 -236
  43. package/dist/goals.js +13 -101
  44. package/dist/golden.js +3 -355
  45. package/dist/handlers/agent.js +4 -165
  46. package/dist/handlers/alias-adapters.js +1 -129
  47. package/dist/handlers/aliases.js +1 -171
  48. package/dist/handlers/audit.js +1 -87
  49. package/dist/handlers/boundary.js +1 -221
  50. package/dist/handlers/capture.js +73 -1109
  51. package/dist/handlers/causality.js +7 -114
  52. package/dist/handlers/cloud.js +85 -382
  53. package/dist/handlers/companion.js +28 -459
  54. package/dist/handlers/datalake.js +7 -187
  55. package/dist/handlers/dispatch-context.js +0 -22
  56. package/dist/handlers/entity.js +25 -256
  57. package/dist/handlers/events.js +16 -335
  58. package/dist/handlers/failure.js +13 -340
  59. package/dist/handlers/goals.js +4 -296
  60. package/dist/handlers/intelligence.js +126 -674
  61. package/dist/handlers/invoicing.js +1 -70
  62. package/dist/handlers/mcpclient.js +6 -137
  63. package/dist/handlers/orchestration.js +40 -125
  64. package/dist/handlers/output-schemas.js +1 -24
  65. package/dist/handlers/presence.js +3 -99
  66. package/dist/handlers/project.js +28 -182
  67. package/dist/handlers/prompts.js +6 -157
  68. package/dist/handlers/quest.js +4 -224
  69. package/dist/handlers/recall.js +11 -218
  70. package/dist/handlers/registry.js +1 -167
  71. package/dist/handlers/resources.js +1 -288
  72. package/dist/handlers/review.js +11 -74
  73. package/dist/handlers/run.js +17 -487
  74. package/dist/handlers/search.js +15 -326
  75. package/dist/handlers/session.js +28 -615
  76. package/dist/handlers/share.js +8 -184
  77. package/dist/handlers/shims.js +1 -464
  78. package/dist/handlers/skill.js +67 -449
  79. package/dist/handlers/survivors.js +1 -120
  80. package/dist/handlers/symbols.js +8 -109
  81. package/dist/handlers/syncops.js +4 -302
  82. package/dist/handlers/types.js +1 -27
  83. package/dist/harvest.js +5 -191
  84. package/dist/hours.js +7 -156
  85. package/dist/http-auth.js +3 -321
  86. package/dist/http-fast.js +21 -1137
  87. package/dist/icons.js +1 -47
  88. package/dist/index.js +2 -924
  89. package/dist/indexer.js +4 -145
  90. package/dist/intelligence.js +31 -261
  91. package/dist/internal-dispatch.js +3 -212
  92. package/dist/keyset.js +1 -110
  93. package/dist/knowledge-graph.js +12 -176
  94. package/dist/license.js +2 -441
  95. package/dist/logger.js +2 -199
  96. package/dist/maintenance.js +2 -148
  97. package/dist/mcp-client.js +6 -262
  98. package/dist/memory-artifacts.js +30 -449
  99. package/dist/migrate-prompt.js +2 -124
  100. package/dist/migrations.js +40 -655
  101. package/dist/performance.js +1 -228
  102. package/dist/presence.js +11 -140
  103. package/dist/priority-embed.js +5 -164
  104. package/dist/providers/embedding-provider.js +1 -196
  105. package/dist/readonly-gate.js +1 -29
  106. package/dist/rehydration.js +9 -157
  107. package/dist/reindex.js +1 -88
  108. package/dist/render-target.js +21 -514
  109. package/dist/render.js +4 -280
  110. package/dist/repl-guard.js +1 -173
  111. package/dist/replication-daemon-entrypoint.js +1 -31
  112. package/dist/replication-daemon.js +2 -262
  113. package/dist/resilience.js +1 -591
  114. package/dist/reverse-bridge.js +5 -360
  115. package/dist/security.js +1 -244
  116. package/dist/session-seen.js +3 -51
  117. package/dist/setup.js +1 -260
  118. package/dist/skill-author.js +5 -168
  119. package/dist/spec-kit.js +1 -191
  120. package/dist/sqlite-busy.js +1 -154
  121. package/dist/statusline.js +11 -315
  122. package/dist/sub-agent.js +13 -262
  123. package/dist/summarizer.js +13 -139
  124. package/dist/symbols.js +7 -283
  125. package/dist/sync.js +5 -359
  126. package/dist/tasks-dispatch.js +1 -84
  127. package/dist/tasks.js +1 -282
  128. package/dist/token-budget.js +1 -143
  129. package/dist/tool-analytics.js +7 -129
  130. package/dist/tool-annotations.js +1 -365
  131. package/dist/tool-manifest-v2.json +1 -1
  132. package/dist/tool-manifest.json +1 -1
  133. package/dist/tool-profiles.js +1 -75
  134. package/dist/trace-harvest.js +6 -244
  135. package/dist/types.js +1 -30
  136. package/dist/ui-dashboard.js +41 -50
  137. package/dist/ulid.js +1 -81
  138. package/dist/validate.js +1 -129
  139. package/dist/vault.js +1 -534
  140. package/dist/vectors.js +3 -184
  141. package/dist/version-check.js +4 -136
  142. package/dist/visibility.js +19 -155
  143. package/dist/wyrm-cli.js +98 -2464
  144. package/dist/wyrm-guard.js +14 -424
  145. package/dist/wyrm-loop.js +3 -150
  146. package/dist/wyrm-manifest.json +1 -1
  147. package/dist/wyrm-statusline-daemon.js +1 -11
  148. package/dist/wyrm-statusline.js +4 -56
  149. package/dist/wyrm-ui.js +9 -77
  150. 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};