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,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.0",
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();