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,425 +1,15 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * wyrm-guard — deterministic PreToolUse hook bin (v7 F2, T016; spec FR-2/G2).
4
- *
5
- * Harness-side negative learning: Claude Code pipes the PreToolUse payload for
6
- * Bash/Edit/Write(/MultiEdit/NotebookEdit) to this bin; the bin runs the SAME
7
- * deterministic failure_check the MCP tool runs (FailurePatterns.checkVerdict —
8
- * Article III: bm25/match-tier arithmetic, zero LLM calls, zero network)
9
- * against the LOCAL SQLite DB opened READ-ONLY, and answers on the Claude Code
10
- * hook contract:
11
- *
12
- * BLOCK — exit 2, the failure pattern on stderr (Claude Code feeds stderr
13
- * back to the model and does not execute the tool call).
14
- * The deterministic block rule identity blocks, similarity warns:
15
- * · an EXACT signature match (checkVerdict tier 'exact'), or
16
- * · a TARGET-IDENTITY match: a stored failure whose scope+target
17
- * normalize identical to the probed command / file path under
18
- * normalizeIdentity — the FULL, UNCAPPED form of the signature
19
- * normalization. Identity spans the whole command/path: the
20
- * 200-char signature cap is range-scan recall only, so two
21
- * long actions differing only past it never alias into one
22
- * block, and the lookup filters identity BEFORE its result
23
- * cap, so same-prefix cousins can never crowd an
24
- * identical-target row out. v7 F2 review fix: this tier runs
25
- * as a DEDICATED signature-prefix lookup
26
- * (FailurePatterns.targetIdentityMatches) — production rows
27
- * always carry a non-empty description, so full-signature
28
- * equality against this bin's empty-description probe could
29
- * never fire, and a block never depends on FTS recall.
30
- * WARN — exit 0, hookSpecificOutput.additionalContext JSON on stdout
31
- * (non-blocking context injection — the wyrm-push.py shape) for any
32
- * other match.
33
- * SILENT — exit 0, no output, when clean.
34
- *
35
- * Run/scope semantics (T014/T015): the caller's run identity comes from
36
- * WYRM_RUN_ID (the F2 fleet convention — the orchestrator exports it once for
37
- * the whole fleet) and is passed EXPLICITLY to checkVerdict, so same-run
38
- * quarantined failures bite instantly while runless callers only ever see the
39
- * project/global tiers. Project scoping: the payload `cwd` (or the edited
40
- * file's directory) is walked up against projects.path — unenrolled paths
41
- * still check the global (project-NULL) failures.
42
- *
43
- * Probe construction (recall under FTS5 implicit-AND, see query() in
44
- * failure-patterns.ts):
45
- * · Bash → scope 'command', probe target = the command string.
46
- * · Files → scopes 'file' + 'edit', probe target = the BASENAME (a full path
47
- * would AND-in every directory token and miss relpath-keyed rows); the
48
- * identity rule then compares the stored target against the absolute,
49
- * project-relative, and as-given path forms — equality on any blocks.
50
- * The edit content signal is extracted/validated but NOT fed to the FTS
51
- * probe: under implicit-AND it strictly narrows recall, and it can never
52
- * produce an exact signature against recorder-authored descriptions.
53
- *
54
- * Budget (the wyrm-push.py ~200ms discipline): readonly open + fileMustExist,
55
- * busy_timeout=100ms (a locked DB fails open instead of hanging the harness),
56
- * a small fixed number of prepared statements per scope (the two checkVerdict
57
- * stages + one identity range-probe per identity candidate), no migrations,
58
- * no index work. Measured by bench/guard-budget.mjs (Article VIII).
59
- *
60
- * Analytics (v7 F2 review fix): a BLOCK files one failure_blocks row — the
61
- * prevented-repeat ROI counter behind `wyrm_stats view=failures` — via a
62
- * separate short-lived RW connection (busy_timeout=50ms). Strictly
63
- * best-effort and fail-open: the CHECK connection stays readonly, and any
64
- * ledger failure is swallowed without affecting the verdict. Blocks are rare
65
- * (they abort the tool call), so the extra write never touches the hot path.
66
- *
67
- * A broken guard must NEVER block the operator's harness: ANY internal error →
68
- * silent exit 0 (fail-open). Set WYRM_GUARD_DEBUG=1 to surface the swallowed
69
- * reason on stderr (still exit 0 — debug output never blocks).
70
- *
71
- * Operator escape hatch: WYRM_GUARD_MODE=warn (downgrade blocks to warns) |
72
- * off (disable entirely). Article I: fully offline, local DB only. Article
73
- * VII: read-only handle, every value a bound parameter (inside
74
- * FailurePatterns), stored text control-char-stripped + length-capped before
75
- * it is rendered into hook output.
76
- *
77
- * Install: scripts/hooks/install.sh wires the PreToolUse matchers
78
- * (`Bash` and `Edit|Write|MultiEdit|NotebookEdit`) into ~/.claude/settings.json.
79
- *
80
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
81
- * @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
82
- */
83
- import Database from 'better-sqlite3';
84
- import { readFileSync, realpathSync } from 'node:fs';
85
- import { homedir } from 'node:os';
86
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
87
- import { fileURLToPath } from 'node:url';
88
- import { FailurePatterns, normalizeIdentity, } from './failure-patterns.js';
89
- import { LEGACY_ENVELOPE, runWithActor, sanitizeActorId } from './handlers/boundary.js';
90
- const SILENT = { exitCode: 0, stdout: '', stderr: '' };
91
- /** Tools whose signal is a file path (+ content). Mirrors wyrm-push.py. */
92
- const FILE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
93
- /** How many warn-context matches to render at most. */
94
- const WARN_MATCH_CAP = 3;
95
- /** Max levels walked up when resolving cwd → projects.path. */
96
- const PROJECT_WALK_LEVELS = 10;
97
- // ── small helpers (all pure) ────────────────────────────────────────────────
98
- function asString(v) {
99
- return typeof v === 'string' && v.length > 0 ? v : null;
100
- }
101
- /** Strip control chars + cap length before stored text reaches hook output. */
102
- function clean(s, max) {
103
- if (!s)
104
- return '';
105
- // eslint-disable-next-line no-control-regex
106
- return s.replace(/[\x00-\x1f\x7f]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, max);
107
- }
108
- /** Extract the deterministic check signal from a PreToolUse payload, or null
109
- * when the payload is not ours to judge (wrong event/tool/shape → SILENT). */
110
- function extractSignal(payload) {
111
- const event = payload['hook_event_name'];
112
- if (event !== undefined && event !== 'PreToolUse')
113
- return null;
114
- const toolName = payload['tool_name'];
115
- const input = payload['tool_input'];
116
- if (typeof toolName !== 'string' || typeof input !== 'object' || input === null)
117
- return null;
118
- const toolInput = input;
119
- const cwd = asString(payload['cwd']);
120
- if (toolName === 'Bash') {
121
- const command = asString(toolInput['command'])?.trim();
122
- if (!command)
123
- return null;
124
- const capped = command.slice(0, 1000); // FTS probe + display stay bounded
125
- return {
126
- scopes: ['command'],
127
- probeTarget: capped,
128
- // Identity is the FULL command, uncapped: commands differing only past
129
- // any cap are DIFFERENT actions, and the comparison cost is one linear
130
- // normalize pass — the cap above bounds only the recall/display paths.
131
- identity: [command],
132
- display: clean(capped, 80),
133
- startDir: cwd,
134
- absPath: null,
135
- };
136
- }
137
- if (FILE_TOOLS.has(toolName)) {
138
- const filePath = asString(toolInput['file_path']) ?? asString(toolInput['notebook_path']);
139
- if (!filePath)
140
- return null;
141
- // Content signal — extracted/shape-checked (see header for why it is not
142
- // fed to the FTS probe).
143
- const edits = toolInput['edits'];
144
- const content = asString(toolInput['new_string'])
145
- ?? asString(toolInput['content'])
146
- ?? asString(toolInput['new_source'])
147
- ?? (Array.isArray(edits)
148
- ? edits.map((e) => asString(e?.['new_string']) ?? '').join(' ')
149
- : '');
150
- void content; // reserved for a future contains-pattern tier
151
- const abs = isAbsolute(filePath)
152
- ? resolve(filePath)
153
- : cwd && isAbsolute(cwd) ? resolve(cwd, filePath) : null;
154
- return {
155
- scopes: ['file', 'edit'],
156
- probeTarget: basename(filePath).slice(0, 200),
157
- // identity candidates: as-given + absolute, RAW and uncapped (same
158
- // full-identity rule as the Bash candidate — deep paths must not alias
159
- // at a truncation boundary); the project-relative form is appended in
160
- // runGuard once the project root is known.
161
- identity: dedupe([filePath, ...(abs ? [abs] : [])]),
162
- display: clean(filePath, 80),
163
- startDir: abs ? dirname(abs) : cwd,
164
- absPath: abs,
165
- };
166
- }
167
- return null;
168
- }
169
- /** Dedupe identity candidates by their normalizeIdentity image (the form
170
- * every identity comparison uses), keeping the first raw spelling. */
171
- function dedupe(xs) {
172
- const seen = new Set();
173
- const out = [];
174
- for (const x of xs) {
175
- const key = normalizeIdentity(x);
176
- if (key.length === 0 || seen.has(key))
177
- continue;
178
- seen.add(key);
179
- out.push(x);
180
- }
181
- return out;
182
- }
183
- /** Walk up from startDir matching projects.path (indexed UNIQUE lookup per
184
- * level, ≤PROJECT_WALK_LEVELS). Unenrolled → null (global failures still apply). */
185
- function resolveProject(db, startDir) {
186
- if (!startDir)
187
- return { id: null, root: null };
188
- const stmt = db.prepare('SELECT id FROM projects WHERE path = ?');
189
- let p = resolve(startDir);
190
- for (let i = 0; i < PROJECT_WALK_LEVELS; i++) {
191
- const row = stmt.get(p);
192
- if (row)
193
- return { id: row.id, root: p };
194
- const parent = dirname(p);
195
- if (parent === p)
196
- break;
197
- p = parent;
198
- }
199
- return { id: null, root: null };
200
- }
201
- // ── rendering (plain ASCII — spec FR-3 renderer discipline) ─────────────────
202
- function attributionLine(m) {
203
- if (m.recorded_by_agent === 'legacy' && m.run_id === null)
204
- return '';
205
- const run = m.run_id
206
- ? ` (run ${clean(m.run_id, 32)}${m.quarantine_scope === 'run' ? ', run-quarantined' : ''})`
207
- : '';
208
- return ` recorded by: ${clean(m.recorded_by_agent, 64)}${run}\n`;
209
- }
210
- function renderBlock(m, reason, extra) {
211
- const desc = m.description ? ` description: ${clean(m.description, 200)}\n` : '';
212
- const why = m.why_failed ? ` failed because: ${clean(m.why_failed, 300)}\n` : '';
213
- const reasonText = reason === 'exact'
214
- ? 'this exact action is a recorded unresolved failure'
215
- : 'this exact target previously failed and the failure is unresolved';
216
- return (`wyrm-guard: BLOCKED — ${reasonText} (deterministic ${reason} match).\n` +
217
- ` failure #${m.id} [${m.severity}/${m.scope}] seen ${m.occurrences}x, last ${clean(m.last_seen, 32)}\n` +
218
- ` target: ${clean(m.target, 120)}\n` +
219
- desc +
220
- why +
221
- attributionLine(m) +
222
- 'Do NOT retry the same action — pick a different approach. If the underlying\n' +
223
- `issue is truly fixed, resolve the failure first (wyrm_failure_resolve id=${m.id})\n` +
224
- 'and then retry.\n' +
225
- (extra > 0 ? `(+${extra} more recorded match${extra === 1 ? '' : 'es'} — wyrm_failure_list)\n` : ''));
226
- }
227
- function renderWarn(matches, display) {
228
- const shown = matches.slice(0, WARN_MATCH_CAP);
229
- const lines = shown.map((m) => {
230
- const why = m.why_failed ?? m.description;
231
- return `- #${m.id} [${m.severity}/${m.scope}] ${clean(m.target, 80)} ` +
232
- `(seen ${m.occurrences}x, confidence ${m.confidence}): ${clean(why, 160)}`;
233
- });
234
- const extra = matches.length - shown.length;
235
- return [
236
- '<wyrm-memory>',
237
- `wyrm-guard: ${matches.length} recorded unresolved failure(s) look similar to \`${display}\` — check before proceeding:`,
238
- ...lines,
239
- ...(extra > 0 ? [`(+${extra} more — wyrm_failure_list)`] : []),
240
- 'If one applies, do not repeat it — adjust the approach or resolve it first (wyrm_failure_resolve).',
241
- '</wyrm-memory>',
242
- ].join('\n');
243
- }
244
- function warnResult(matches, display) {
245
- const body = {
246
- hookSpecificOutput: {
247
- hookEventName: 'PreToolUse',
248
- additionalContext: renderWarn(matches, display),
249
- },
250
- };
251
- return { exitCode: 0, stdout: JSON.stringify(body) + '\n', stderr: '' };
252
- }
253
- // ── the guard ───────────────────────────────────────────────────────────────
254
- /**
255
- * The whole guard as a pure-ish function of (stdin, env) — exported so tests
256
- * exercise every path in-process. NEVER throws: any internal error returns
257
- * SILENT (fail-open — a broken guard must never block the operator's harness).
258
- */
259
- export function runGuard(stdinText, env = process.env) {
260
- let db = null;
261
- try {
262
- const mode = (env.WYRM_GUARD_MODE ?? 'block').toLowerCase();
263
- if (mode === 'off')
264
- return SILENT;
265
- if (!stdinText || !stdinText.trim())
266
- return SILENT;
267
- const parsed = JSON.parse(stdinText);
268
- if (typeof parsed !== 'object' || parsed === null)
269
- return SILENT;
270
- const signal = extractSignal(parsed);
271
- if (!signal)
272
- return SILENT;
273
- const dbPath = env.WYRM_DB_PATH ?? join(homedir(), '.wyrm', 'wyrm.db');
274
- db = new Database(dbPath, { readonly: true, fileMustExist: true });
275
- db.pragma('busy_timeout = 100'); // a locked DB fails open, never hangs the harness
276
- const { id: projectId, root } = resolveProject(db, signal.startDir);
277
- // Project-relative identity candidate for file probes (recorders commonly
278
- // key file failures by relpath). RAW like every identity candidate.
279
- const identity = [...signal.identity];
280
- if (root && signal.absPath && signal.absPath.startsWith(root + sep)) {
281
- identity.push(relative(root, signal.absPath));
282
- }
283
- // The caller's run identity — explicit (deterministic in (stdin, env)),
284
- // honoring the same WYRM_RUN_ID convention the ambient envelope uses.
285
- const callerRunId = sanitizeActorId(env.WYRM_RUN_ID) ?? null;
286
- const failures = new FailurePatterns(db);
287
- const seen = new Set();
288
- const matches = [];
289
- const identityCandidates = dedupe(identity);
290
- // (a) The checkVerdict stages: the exact-signature tier + FTS fuzzy
291
- // recall for the WARN context. Runs first so a genuine full-signature
292
- // match keeps its 'exact' classification.
293
- for (const scope of signal.scopes) {
294
- try {
295
- const verdict = failures.checkVerdict(scope, signal.probeTarget, '', projectId, { callerRunId });
296
- for (const m of verdict.matches) {
297
- if (!seen.has(m.id)) {
298
- seen.add(m.id);
299
- matches.push(m);
300
- }
301
- }
302
- }
303
- catch {
304
- // e.g. a probe that sanitizes to an empty FTS query — skip this scope
305
- }
306
- }
307
- // (b) The deterministic TARGET-IDENTITY tier (v7 F2 review fix): a
308
- // dedicated description-independent signature-prefix lookup per identity
309
- // candidate — production rows (whose descriptions are never empty) block
310
- // on it. Candidates go in RAW: identity equality runs over the FULL
311
- // normalizeIdentity forms, and the lookup filters identity before its
312
- // result cap — so neither the 200-char signature cap nor higher-ranked
313
- // same-prefix cousins (colon-extended npm scripts, URLs, host:port) can
314
- // alias or crowd out an identical-target row. Stage (a)'s FTS LIMIT 5
315
- // can still be crowded, but it only feeds WARN context — a block never
316
- // depends on FTS recall.
317
- for (const scope of signal.scopes) {
318
- for (const cand of identityCandidates) {
319
- try {
320
- for (const row of failures.targetIdentityMatches(scope, cand, projectId, { callerRunId })) {
321
- if (!seen.has(row.id)) {
322
- seen.add(row.id);
323
- matches.push(failures.toVerdictMatch(row, 'target'));
324
- }
325
- }
326
- }
327
- catch {
328
- // identity probe failure is never fatal — the other stages still ran
329
- }
330
- }
331
- }
332
- if (matches.length === 0)
333
- return SILENT;
334
- // Fuzzy→target upgrade set: FULL normalized identity forms — same truth
335
- // predicate as targetIdentityMatches, so a fuzzy-recalled row upgrades to
336
- // a block only on whole-string identity, never on a shared capped prefix.
337
- const identitySet = new Set(identityCandidates.map((c) => normalizeIdentity(c)));
338
- const blockers = matches
339
- .map((m) => ({
340
- m,
341
- reason: m.match === 'exact'
342
- ? 'exact'
343
- : m.match === 'target' || identitySet.has(normalizeIdentity(m.target))
344
- ? 'target' : null,
345
- }))
346
- .filter((x) => x.reason !== null)
347
- // exact signature identity outranks target identity
348
- .sort((a, b) => (a.reason === b.reason ? 0 : a.reason === 'exact' ? -1 : 1));
349
- if (blockers.length > 0 && mode !== 'warn') {
350
- const top = blockers[0];
351
- // v7 F2 review fix: count the prevented repeat (the FR-2 ROI number).
352
- // The check connection stays READONLY; the block row rides a separate
353
- // short-lived RW connection — strictly best-effort and fail-open.
354
- try {
355
- const rw = new Database(dbPath, { fileMustExist: true });
356
- try {
357
- rw.pragma('busy_timeout = 50'); // a contended ledger is dropped, never waited on
358
- const agentId = sanitizeActorId(env.WYRM_AGENT_ID);
359
- const envelope = agentId !== null || callerRunId !== null
360
- ? { agent_id: agentId, run_id: callerRunId, source: 'env' }
361
- : LEGACY_ENVELOPE;
362
- runWithActor(envelope, () => new FailurePatterns(rw).recordBlock({
363
- blocked: true,
364
- matches: [top.m],
365
- recorded_by_agent: top.m.recorded_by_agent,
366
- run_id: top.m.run_id,
367
- confidence: top.m.confidence,
368
- }, projectId));
369
- }
370
- finally {
371
- rw.close();
372
- }
373
- }
374
- catch {
375
- // fail-open: analytics must never affect the block verdict
376
- }
377
- return {
378
- exitCode: 2,
379
- stdout: '',
380
- stderr: renderBlock(top.m, top.reason, matches.length - 1),
381
- };
382
- }
383
- return warnResult(matches, signal.display);
384
- }
385
- catch (err) {
386
- if (env.WYRM_GUARD_DEBUG === '1') {
387
- const msg = err instanceof Error ? err.message : String(err);
388
- return { exitCode: 0, stdout: '', stderr: `wyrm-guard: internal error (fail-open): ${clean(msg, 200)}\n` };
389
- }
390
- return SILENT;
391
- }
392
- finally {
393
- try {
394
- db?.close();
395
- }
396
- catch { /* readonly close failure is irrelevant */ }
397
- }
398
- }
399
- // ── bin entry ───────────────────────────────────────────────────────────────
400
- // realpathSync so the npm bin symlink (wyrm-guard → dist/wyrm-guard.js) still
401
- // counts as "run directly"; importing this module (tests) never reads stdin.
402
- const __filename = fileURLToPath(import.meta.url);
403
- let isMain = false;
404
- try {
405
- isMain = typeof process.argv[1] === 'string' && realpathSync(process.argv[1]) === __filename;
406
- }
407
- catch {
408
- isMain = false;
409
- }
410
- if (isMain) {
411
- let stdinText = '';
412
- try {
413
- stdinText = readFileSync(0, 'utf-8'); // fd 0 — blocks to EOF, like wyrm-push.py
414
- }
415
- catch {
416
- stdinText = '';
417
- }
418
- const result = runGuard(stdinText);
419
- if (result.stdout)
420
- process.stdout.write(result.stdout);
421
- if (result.stderr)
422
- process.stderr.write(result.stderr);
423
- process.exit(result.exitCode);
424
- }
425
- //# sourceMappingURL=wyrm-guard.js.map
2
+ import S from"better-sqlite3";import{readFileSync as M,realpathSync as I}from"node:fs";import{homedir as L}from"node:os";import{basename as D,dirname as k,isAbsolute as P,join as W,relative as N,resolve as b,sep as O}from"node:path";import{fileURLToPath as j}from"node:url";import{FailurePatterns as R,normalizeIdentity as $}from"./failure-patterns.js";import{LEGACY_ENVELOPE as U,runWithActor as B,sanitizeActorId as T}from"./handlers/boundary.js";const _={exitCode:0,stdout:"",stderr:""},G=new Set(["Edit","Write","MultiEdit","NotebookEdit"]),Y=3,F=10;function p(e){return typeof e=="string"&&e.length>0?e:null}function a(e,t){return e?e.replace(/[\x00-\x1f\x7f]/g," ").replace(/\s+/g," ").trim().slice(0,t):""}function V(e){const t=e.hook_event_name;if(t!==void 0&&t!=="PreToolUse")return null;const o=e.tool_name,s=e.tool_input;if(typeof o!="string"||typeof s!="object"||s===null)return null;const n=s,r=p(e.cwd);if(o==="Bash"){const c=p(n.command)?.trim();if(!c)return null;const d=c.slice(0,1e3);return{scopes:["command"],probeTarget:d,identity:[c],display:a(d,80),startDir:r,absPath:null}}if(G.has(o)){const c=p(n.file_path)??p(n.notebook_path);if(!c)return null;const d=n.edits,g=p(n.new_string)??p(n.content)??p(n.new_source)??(Array.isArray(d)?d.map(h=>p(h?.new_string)??"").join(" "):""),f=P(c)?b(c):r&&P(r)?b(r,c):null;return{scopes:["file","edit"],probeTarget:D(c).slice(0,200),identity:A([c,...f?[f]:[]]),display:a(c,80),startDir:f?k(f):r,absPath:f}}return null}function A(e){const t=new Set,o=[];for(const s of e){const n=$(s);n.length===0||t.has(n)||(t.add(n),o.push(s))}return o}function H(e,t){if(!t)return{id:null,root:null};const o=e.prepare("SELECT id FROM projects WHERE path = ?");let s=b(t);for(let n=0;n<F;n++){const r=o.get(s);if(r)return{id:r.id,root:s};const c=k(s);if(c===s)break;s=c}return{id:null,root:null}}function J(e){if(e.recorded_by_agent==="legacy"&&e.run_id===null)return"";const t=e.run_id?` (run ${a(e.run_id,32)}${e.quarantine_scope==="run"?", run-quarantined":""})`:"";return` recorded by: ${a(e.recorded_by_agent,64)}${t}
3
+ `}function q(e,t,o){const s=e.description?` description: ${a(e.description,200)}
4
+ `:"",n=e.why_failed?` failed because: ${a(e.why_failed,300)}
5
+ `:"";return`wyrm-guard: BLOCKED \u2014 ${t==="exact"?"this exact action is a recorded unresolved failure":"this exact target previously failed and the failure is unresolved"} (deterministic ${t} match).
6
+ failure #${e.id} [${e.severity}/${e.scope}] seen ${e.occurrences}x, last ${a(e.last_seen,32)}
7
+ target: ${a(e.target,120)}
8
+ `+s+n+J(e)+`Do NOT retry the same action \u2014 pick a different approach. If the underlying
9
+ issue is truly fixed, resolve the failure first (wyrm_failure_resolve id=${e.id})
10
+ and then retry.
11
+ `+(o>0?`(+${o} more recorded match${o===1?"":"es"} \u2014 wyrm_failure_list)
12
+ `:"")}function z(e,t){const o=e.slice(0,Y),s=o.map(r=>{const c=r.why_failed??r.description;return`- #${r.id} [${r.severity}/${r.scope}] ${a(r.target,80)} (seen ${r.occurrences}x, confidence ${r.confidence}): ${a(c,160)}`}),n=e.length-o.length;return["<wyrm-memory>",`wyrm-guard: ${e.length} recorded unresolved failure(s) look similar to \`${t}\` \u2014 check before proceeding:`,...s,...n>0?[`(+${n} more \u2014 wyrm_failure_list)`]:[],"If one applies, do not repeat it \u2014 adjust the approach or resolve it first (wyrm_failure_resolve).","</wyrm-memory>"].join(`
13
+ `)}function K(e,t){const o={hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:z(e,t)}};return{exitCode:0,stdout:JSON.stringify(o)+`
14
+ `,stderr:""}}function Q(e,t=process.env){let o=null;try{const s=(t.WYRM_GUARD_MODE??"block").toLowerCase();if(s==="off"||!e||!e.trim())return _;const n=JSON.parse(e);if(typeof n!="object"||n===null)return _;const r=V(n);if(!r)return _;const c=t.WYRM_DB_PATH??W(L(),".wyrm","wyrm.db");o=new S(c,{readonly:!0,fileMustExist:!0}),o.pragma("busy_timeout = 100");const{id:d,root:g}=H(o,r.startDir),f=[...r.identity];g&&r.absPath&&r.absPath.startsWith(g+O)&&f.push(N(g,r.absPath));const h=T(t.WYRM_RUN_ID)??null,w=new R(o),m=new Set,y=[],E=A(f);for(const i of r.scopes)try{const u=w.checkVerdict(i,r.probeTarget,"",d,{callerRunId:h});for(const l of u.matches)m.has(l.id)||(m.add(l.id),y.push(l))}catch{}for(const i of r.scopes)for(const u of E)try{for(const l of w.targetIdentityMatches(i,u,d,{callerRunId:h}))m.has(l.id)||(m.add(l.id),y.push(w.toVerdictMatch(l,"target")))}catch{}if(y.length===0)return _;const C=new Set(E.map(i=>$(i))),v=y.map(i=>({m:i,reason:i.match==="exact"?"exact":i.match==="target"||C.has($(i.target))?"target":null})).filter(i=>i.reason!==null).sort((i,u)=>i.reason===u.reason?0:i.reason==="exact"?-1:1);if(v.length>0&&s!=="warn"){const i=v[0];try{const u=new S(c,{fileMustExist:!0});try{u.pragma("busy_timeout = 50");const l=T(t.WYRM_AGENT_ID);B(l!==null||h!==null?{agent_id:l,run_id:h,source:"env"}:U,()=>new R(u).recordBlock({blocked:!0,matches:[i.m],recorded_by_agent:i.m.recorded_by_agent,run_id:i.m.run_id,confidence:i.m.confidence},d))}finally{u.close()}}catch{}return{exitCode:2,stdout:"",stderr:q(i.m,i.reason,y.length-1)}}return K(y,r.display)}catch(s){if(t.WYRM_GUARD_DEBUG==="1"){const n=s instanceof Error?s.message:String(s);return{exitCode:0,stdout:"",stderr:`wyrm-guard: internal error (fail-open): ${a(n,200)}
15
+ `}}return _}finally{try{o?.close()}catch{}}}const X=j(import.meta.url);let x=!1;try{x=typeof process.argv[1]=="string"&&I(process.argv[1])===X}catch{x=!1}if(x){let e="";try{e=M(0,"utf-8")}catch{e=""}const t=Q(e);t.stdout&&process.stdout.write(t.stdout),t.stderr&&process.stderr.write(t.stderr),process.exit(t.exitCode)}export{Q as runGuard};
package/dist/wyrm-loop.js CHANGED
@@ -1,62 +1,6 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * wyrm-loop the autonomous scheduler.
4
- *
5
- * Long-lived daemon. Wakes up every N seconds, picks the highest-priority
6
- * active goal that hasn't hit its iteration cap, runs ONE OODA iteration
7
- * via AgentLoop, logs to agent_actions + goal_iterations, and goes back
8
- * to sleep. Stops a goal once it completes / blocks / errors.
9
- *
10
- * Usage:
11
- * wyrm-loop # default 10-minute interval, loop forever
12
- * wyrm-loop --interval 60 # 60-second interval
13
- * wyrm-loop --once # one iteration, then exit (cron-friendly)
14
- * wyrm-loop --goal 42 # only this goal
15
- * wyrm-loop --max-steps 3 # max iterations per goal per tick
16
- * wyrm-loop --project /path # scope to project
17
- *
18
- * Environment:
19
- * WYRM_DB_PATH path to wyrm.db (default ~/.wyrm/wyrm.db)
20
- * WYRM_OLLAMA_URL default http://localhost:11434
21
- * OPENAI_API_KEY optional fallback
22
- * WYRM_LOOP_LOG '1' to log every tick to stderr
23
- *
24
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
25
- */
26
- import { WyrmDB } from './database.js';
27
- import { Goals } from './goals.js';
28
- import { AgentLoop } from './agent-loop.js';
29
- import { OutboundMcpClient } from './mcp-client.js';
30
- function parseArgs(argv) {
31
- const out = {
32
- interval: 600,
33
- once: false,
34
- maxSteps: 3,
35
- verbose: process.env.WYRM_LOOP_LOG === '1',
36
- };
37
- for (let i = 0; i < argv.length; i++) {
38
- const a = argv[i];
39
- if (a === '--interval')
40
- out.interval = Math.max(10, parseInt(argv[++i], 10) || 600);
41
- else if (a === '--once')
42
- out.once = true;
43
- else if (a === '--goal')
44
- out.goalId = parseInt(argv[++i], 10);
45
- else if (a === '--max-steps')
46
- out.maxSteps = Math.max(1, Math.min(20, parseInt(argv[++i], 10) || 3));
47
- else if (a === '--project')
48
- out.projectPath = argv[++i];
49
- else if (a === '--verbose' || a === '-v')
50
- out.verbose = true;
51
- else if (a === '--help' || a === '-h') {
52
- console.log(USAGE);
53
- process.exit(0);
54
- }
55
- }
56
- return out;
57
- }
58
- const USAGE = `
59
- wyrm-loop — autonomous OODA scheduler
2
+ import{WyrmDB as m}from"./database.js";import{Goals as f}from"./goals.js";import{AgentLoop as d}from"./agent-loop.js";import{OutboundMcpClient as h}from"./mcp-client.js";function g(e){const t={interval:600,once:!1,maxSteps:3,verbose:process.env.WYRM_LOOP_LOG==="1"};for(let s=0;s<e.length;s++){const o=e[s];o==="--interval"?t.interval=Math.max(10,parseInt(e[++s],10)||600):o==="--once"?t.once=!0:o==="--goal"?t.goalId=parseInt(e[++s],10):o==="--max-steps"?t.maxSteps=Math.max(1,Math.min(20,parseInt(e[++s],10)||3)):o==="--project"?t.projectPath=e[++s]:o==="--verbose"||o==="-v"?t.verbose=!0:(o==="--help"||o==="-h")&&(console.log(w),process.exit(0))}return t}const w=`
3
+ wyrm-loop \u2014 autonomous OODA scheduler
60
4
 
61
5
  wyrm-loop [options]
62
6
 
@@ -68,95 +12,4 @@ Options
68
12
  --project PATH Scope to a project root
69
13
  --verbose, -v Log every tick to stderr
70
14
  --help, -h Show this help
71
- `;
72
- function log(args, msg) {
73
- if (args.verbose)
74
- console.error(`[wyrm-loop ${new Date().toISOString()}] ${msg}`);
75
- }
76
- /** Run a single tick. Returns true if work was done, false on idle. */
77
- async function tick(args, db, goals, agent) {
78
- let goalId = args.goalId;
79
- if (!goalId) {
80
- let projectId;
81
- if (args.projectPath) {
82
- const p = db.getProject(args.projectPath);
83
- if (!p) {
84
- log(args, `project '${args.projectPath}' not registered — idle`);
85
- return false;
86
- }
87
- projectId = p.id;
88
- }
89
- const next = goals.nextToPursue(projectId);
90
- if (!next) {
91
- log(args, 'no active goals — idle');
92
- return false;
93
- }
94
- goalId = next.id;
95
- }
96
- const goal = goals.get(goalId);
97
- if (!goal || goal.status !== 'active') {
98
- log(args, `goal #${goalId} not active — skipping`);
99
- return false;
100
- }
101
- log(args, `pursuing goal #${goalId}: ${goal.title.slice(0, 80)}`);
102
- const results = await agent.pursue(goalId, { max_steps: args.maxSteps });
103
- if (results.length === 0) {
104
- log(args, `goal #${goalId} returned no iterations`);
105
- return false;
106
- }
107
- const last = results[results.length - 1];
108
- log(args, `goal #${goalId} ran ${results.length} iteration(s); last outcome=${last.outcome} (${last.latency_ms}ms, model=${last.model}${last.degraded ? ' DEGRADED' : ''})`);
109
- return true;
110
- }
111
- async function main() {
112
- const args = parseArgs(process.argv.slice(2));
113
- const db = new WyrmDB(process.env.WYRM_DB_PATH);
114
- const goals = new Goals(db.getDatabase());
115
- const mcp = new OutboundMcpClient(db.getDatabase());
116
- // Minimal dispatcher for wyrm-loop — reads only, since this is the
117
- // autonomous path and we want safe defaults. Goals can still call
118
- // write tools via the standard MCP path (wyrm-mcp server), but loop
119
- // ticks stick to a conservative subset.
120
- const dispatcher = async () => {
121
- return { ok: false, error: 'wyrm-loop does not enable internal tool dispatch in this build — use the wyrm-mcp server for full agent runs' };
122
- };
123
- const agent = new AgentLoop(db.getDatabase(), mcp, dispatcher);
124
- let stopped = false;
125
- const stop = () => {
126
- if (stopped)
127
- return;
128
- stopped = true;
129
- log(args, 'shutting down…');
130
- mcp.shutdown().finally(() => {
131
- try {
132
- db.close();
133
- }
134
- catch { /* best-effort */ }
135
- process.exit(0);
136
- });
137
- };
138
- process.on('SIGINT', stop);
139
- process.on('SIGTERM', stop);
140
- if (args.verbose) {
141
- console.error(`wyrm-loop starting · interval=${args.interval}s · once=${args.once} · max-steps=${args.maxSteps}`);
142
- }
143
- // Loop
144
- while (!stopped) {
145
- try {
146
- await tick(args, db, goals, agent);
147
- }
148
- catch (err) {
149
- console.error(`wyrm-loop tick error: ${err.message}`);
150
- }
151
- if (args.once) {
152
- stop();
153
- return;
154
- }
155
- await new Promise(r => { setTimeout(r, args.interval * 1000); });
156
- }
157
- }
158
- main().catch(err => {
159
- console.error(`wyrm-loop fatal: ${err.message}`);
160
- process.exit(1);
161
- });
162
- //# sourceMappingURL=wyrm-loop.js.map
15
+ `;function i(e,t){e.verbose&&console.error(`[wyrm-loop ${new Date().toISOString()}] ${t}`)}async function v(e,t,s,o){let r=e.goalId;if(!r){let l;if(e.projectPath){const u=t.getProject(e.projectPath);if(!u)return i(e,`project '${e.projectPath}' not registered \u2014 idle`),!1;l=u.id}const p=s.nextToPursue(l);if(!p)return i(e,"no active goals \u2014 idle"),!1;r=p.id}const c=s.get(r);if(!c||c.status!=="active")return i(e,`goal #${r} not active \u2014 skipping`),!1;i(e,`pursuing goal #${r}: ${c.title.slice(0,80)}`);const n=await o.pursue(r,{max_steps:e.maxSteps});if(n.length===0)return i(e,`goal #${r} returned no iterations`),!1;const a=n[n.length-1];return i(e,`goal #${r} ran ${n.length} iteration(s); last outcome=${a.outcome} (${a.latency_ms}ms, model=${a.model}${a.degraded?" DEGRADED":""})`),!0}async function x(){const e=g(process.argv.slice(2)),t=new m(process.env.WYRM_DB_PATH),s=new f(t.getDatabase()),o=new h(t.getDatabase()),r=async()=>({ok:!1,error:"wyrm-loop does not enable internal tool dispatch in this build \u2014 use the wyrm-mcp server for full agent runs"}),c=new d(t.getDatabase(),o,r);let n=!1;const a=()=>{n||(n=!0,i(e,"shutting down\u2026"),o.shutdown().finally(()=>{try{t.close()}catch{}process.exit(0)}))};for(process.on("SIGINT",a),process.on("SIGTERM",a),e.verbose&&console.error(`wyrm-loop starting \xB7 interval=${e.interval}s \xB7 once=${e.once} \xB7 max-steps=${e.maxSteps}`);!n;){try{await v(e,t,s,c)}catch(l){console.error(`wyrm-loop tick error: ${l.message}`)}if(e.once){a();return}await new Promise(l=>{setTimeout(l,e.interval*1e3)})}}x().catch(e=>{console.error(`wyrm-loop fatal: ${e.message}`),process.exit(1)});
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contract": "wyrm-cloud-memory-v1",
3
- "version": "7.2.1",
3
+ "version": "7.2.2",
4
4
  "generatedBy": "scripts/gen-tool-manifest.mjs (from the dist/ registry modules — never hand-edited)",
5
5
  "tiers": {
6
6
  "core": [
@@ -1,12 +1,2 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * wyrm-statusline-daemon — long-lived process serving statusline queries.
4
- *
5
- * Auto-spawned by the wyrm-statusline binary on first call. Listens on
6
- * ~/.wyrm/statusline.sock. Idle-times-out after 5 minutes of no requests.
7
- *
8
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
9
- */
10
- import { runDaemon } from './statusline.js';
11
- runDaemon();
12
- //# sourceMappingURL=wyrm-statusline-daemon.js.map
2
+ import{runDaemon as m}from"./statusline.js";m();