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
package/dist/audit.js CHANGED
@@ -1,460 +1,5 @@
1
- /**
2
- * Compliance audit trail (Tier 3.9) — per-agent attribution + versioned
3
- * Ed25519 signing (v7 F2, T010).
4
- *
5
- * Hash-chained event log: each row records sha256(prev_hash || payload ||
6
- * timestamp). Tampering with any historical entry breaks the chain.
7
- * Optional Ed25519 signing layered on top provides a SOC2 / HIPAA-grade
8
- * audit surface — every AI decision, prompt, tool call, code edit, with
9
- * the prompt that caused it.
10
- *
11
- * `wyrm_audit_export(range)` produces a tamper-evident JSON bundle that
12
- * an external auditor can verify offline:
13
- * 1. Recompute the hash chain from genesis through the range.
14
- * 2. Verify Ed25519 signatures against the operator's public key(s).
15
- * 3. Confirm no gaps in IDs.
16
- *
17
- * ## Signed-payload versioning (v7 F2, T010)
18
- *
19
- * Migration 20's `agent_id`/`run_id` columns are NOT part of the hash-chain
20
- * input — the chain rule is byte-identical to 6.x:
21
- *
22
- * row_hash = sha256(`${prev_hash}|${event_kind}|${payload_json}|${logged_at}`)
23
- *
24
- * so a 6.x DB opened under v7 keeps verifying and the v1→v2 transition can
25
- * never break the chain (old rows verify as v1, new rows as v2, one chain).
26
- * Attribution is instead bound TAMPER-EVIDENTLY into the Ed25519 signature
27
- * via a payload-version byte:
28
- *
29
- * v1 (legacy 6.x): signed message = utf8(row_hash) — no attribution
30
- * stored signature = bare base64 (unchanged on old rows)
31
- * v2 (v7): signed message = 0x02 ‖ utf8(JSON.stringify(
32
- * { row_hash, agent_id, run_id })) — attribution bound
33
- * stored signature = `v2:<key_fp16>:<base64>`
34
- *
35
- * The leading 0x02 version byte domain-separates the payloads (a v1 message
36
- * is 64 ASCII-hex bytes and can never begin with 0x02), and the canonical
37
- * JSON body (fixed key order, NULLs preserved) makes the agent_id/run_id
38
- * field boundaries unambiguous — no delimiter-shifting forgeries.
39
- * `key_fp16` = first 16 hex chars of sha256(SPKI DER of the signing public
40
- * key): the "which key" answer, claimed in the row and CONFIRMED whenever
41
- * public keys are handed to `verify()`.
42
- *
43
- * ## The rule for 6.x verifiers (explicit — locked by tests/audit-attribution.test.ts)
44
- *
45
- * The verify structure 6.x actually shipped (`verify()` / `verifyBundle()`)
46
- * checks ONLY the hash chain and never parses the `signature` column.
47
- * Therefore:
48
- * (a) a 6.x verifier verifies BOTH v1 and v2 rows exactly as today — the
49
- * chain rule is unchanged, mixed v1/v2 chains pass, and exported
50
- * bundles remain version-1 bundles that 6.x `verifyBundle()` accepts;
51
- * (b) a 6.x auditor's out-of-band Ed25519 step ("signature of the
52
- * row_hash") FAILS CLOSED on a v2 row: the stored `v2:`-prefixed value
53
- * is not the base64 of an Ed25519 signature over the row_hash, so the
54
- * 6.x signature check reports INVALID — it can never silently accept a
55
- * v7 signature (and so can never silently accept forged attribution).
56
- * The chain walk itself stays green.
57
- *
58
- * Signing is OPT-IN (Articles I & VII): no key configured → rows are written
59
- * unsigned, byte-identical to 6.x behavior. `WYRM_AUDIT_SIGNING_KEY` holds a
60
- * PEM-encoded (pkcs8) Ed25519 private key, or a path to one.
61
- *
62
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
63
- * @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
64
- */
65
- import { createHash, createPrivateKey, createPublicKey, sign as edSign, verify as edVerify, } from 'crypto';
66
- import { readFileSync } from 'fs';
67
- import { getActor } from './handlers/boundary.js';
68
- import { resolveAttribution } from './attribution.js';
69
- const GENESIS_HASH = 'genesis';
70
- // ==================== v7 F2 (T010) — versioned signed payload ====================
71
- /** Stored-signature prefix marking a v2 (attribution-binding) signature. */
72
- export const AUDIT_SIG_V2_PREFIX = 'v2:';
73
- /** The payload-version byte prepended to the v2 signed message. */
74
- const V2_VERSION_BYTE = 0x02;
75
- /** Hex chars of sha256(SPKI DER) used as the key fingerprint. */
76
- const KEY_FP_LEN = 16;
77
- const KEY_FP_RE = new RegExp(`^[0-9a-f]{${KEY_FP_LEN}}$`);
78
- /**
79
- * Classify a stored signature string by payload version.
80
- * Bare base64 (anything without the `v2:` prefix) = v1 legacy; `v2:<fp>:<b64>`
81
- * = v2. Returns null for unsigned (NULL/empty). Never throws.
82
- */
83
- export function parseAuditSignature(signature) {
84
- if (typeof signature !== 'string' || signature.length === 0)
85
- return null;
86
- if (signature.startsWith(AUDIT_SIG_V2_PREFIX)) {
87
- const rest = signature.slice(AUDIT_SIG_V2_PREFIX.length);
88
- const sep = rest.indexOf(':');
89
- const fp = sep >= 0 ? rest.slice(0, sep) : '';
90
- const b64 = sep >= 0 ? rest.slice(sep + 1) : '';
91
- return { version: 2, key_fp: KEY_FP_RE.test(fp) ? fp : null, sig_b64: b64 };
92
- }
93
- return { version: 1, key_fp: null, sig_b64: signature };
94
- }
95
- /** Fingerprint of an Ed25519 public key: sha256 over the SPKI DER, first 16 hex. */
96
- export function auditKeyFingerprint(publicKey) {
97
- const key = typeof publicKey === 'string' ? createPublicKey(publicKey) : publicKey;
98
- const der = key.export({ type: 'spki', format: 'der' });
99
- return createHash('sha256').update(der).digest('hex').slice(0, KEY_FP_LEN);
100
- }
101
- /**
102
- * Build the exact byte string an audit Ed25519 signature covers.
103
- * v1: utf8(row_hash) — the legacy 6.x payload, no attribution.
104
- * v2: 0x02 version byte ‖ utf8(JSON.stringify({row_hash, agent_id, run_id})).
105
- */
106
- export function buildSignedPayload(version, fields) {
107
- if (version === 1)
108
- return Buffer.from(fields.row_hash, 'utf-8');
109
- const body = JSON.stringify({
110
- row_hash: fields.row_hash,
111
- agent_id: fields.agent_id ?? null,
112
- run_id: fields.run_id ?? null,
113
- });
114
- return Buffer.concat([Buffer.from([V2_VERSION_BYTE]), Buffer.from(body, 'utf-8')]);
115
- }
116
- /** Materialize a signer from a PEM (pkcs8) Ed25519 private key; the public
117
- * half and fingerprint are derived. Throws on non-Ed25519 keys. */
118
- export function createAuditSigner(privateKeyPem) {
119
- const privateKey = createPrivateKey(privateKeyPem);
120
- if (privateKey.asymmetricKeyType !== 'ed25519') {
121
- throw new Error(`audit signing key must be Ed25519 (got ${privateKey.asymmetricKeyType})`);
122
- }
123
- const pub = createPublicKey(privateKey);
124
- return {
125
- privateKey,
126
- publicKeyPem: pub.export({ type: 'spki', format: 'pem' }).toString(),
127
- key_fp: auditKeyFingerprint(pub),
128
- };
129
- }
130
- /** Sign the v2 payload for a row. Returns the stored form `v2:<fp>:<base64>`. */
131
- export function signAuditEntry(signer, fields) {
132
- const sig = edSign(null, buildSignedPayload(2, fields), signer.privateKey);
133
- return `${AUDIT_SIG_V2_PREFIX}${signer.key_fp}:${sig.toString('base64')}`;
134
- }
135
- /**
136
- * Opt-in signer bootstrap (Articles I & VII): `WYRM_AUDIT_SIGNING_KEY` is a
137
- * PEM string or a path to a PEM file. Absent → null (rows stay unsigned —
138
- * exactly the 6.x behavior). Unusable → warn on STDERR (stdout is the MCP
139
- * wire) and run unsigned; never throws.
140
- */
141
- export function loadAuditSignerFromEnv(env = process.env) {
142
- const raw = env.WYRM_AUDIT_SIGNING_KEY?.trim();
143
- if (!raw)
144
- return null;
145
- try {
146
- const pem = raw.includes('-----BEGIN') ? raw : readFileSync(raw, 'utf-8');
147
- return createAuditSigner(pem);
148
- }
149
- catch (e) {
150
- console.error(`[wyrm] WYRM_AUDIT_SIGNING_KEY unusable (${e.message}) — audit rows will be unsigned`);
151
- return null;
152
- }
153
- }
154
- /**
155
- * Verify ONE row's signature against a set of public keys (PEM or KeyObject).
156
- * Fail-closed: malformed signatures, unknown fingerprints, undecodable
157
- * base64, and crypto-layer rejections all report ok=false. Never throws.
158
- */
159
- export function verifyAuditEntrySignature(row, publicKeys) {
160
- const parsed = parseAuditSignature(row.signature);
161
- if (!parsed)
162
- return { ok: true, version: null, key_fp: null };
163
- const keys = [];
164
- for (const k of publicKeys) {
165
- try {
166
- const key = typeof k === 'string' ? createPublicKey(k) : k;
167
- keys.push({ key, fp: auditKeyFingerprint(key) });
168
- }
169
- catch { /* unusable provided key — skipped (signed rows then fail closed) */ }
170
- }
171
- const sig = Buffer.from(parsed.sig_b64, 'base64');
172
- if (parsed.version === 2) {
173
- if (!parsed.key_fp) {
174
- return { ok: false, version: 2, key_fp: null, reason: 'malformed v2 signature (missing key fingerprint)' };
175
- }
176
- const candidates = keys.filter((k) => k.fp === parsed.key_fp);
177
- if (candidates.length === 0) {
178
- return { ok: false, version: 2, key_fp: parsed.key_fp, reason: `no provided key matches fingerprint ${parsed.key_fp}` };
179
- }
180
- const payload = buildSignedPayload(2, row);
181
- for (const c of candidates) {
182
- try {
183
- if (edVerify(null, payload, c.key, sig))
184
- return { ok: true, version: 2, key_fp: parsed.key_fp };
185
- }
186
- catch { /* fail closed */ }
187
- }
188
- return {
189
- ok: false, version: 2, key_fp: parsed.key_fp,
190
- reason: 'v2 signature does not verify (row_hash/agent_id/run_id tampered or wrong key)',
191
- };
192
- }
193
- // v1: legacy signature over the bare row_hash; no fingerprint — try every key.
194
- const payload = buildSignedPayload(1, row);
195
- for (const c of keys) {
196
- try {
197
- if (edVerify(null, payload, c.key, sig))
198
- return { ok: true, version: 1, key_fp: c.fp };
199
- }
200
- catch { /* fail closed */ }
201
- }
202
- return { ok: false, version: 1, key_fp: null, reason: 'v1 signature does not verify under any provided key' };
203
- }
204
- /** Shared by verify()/verifyBundle(): version counts, who/which-run/which-key,
205
- * and (when keys are provided) the signature-layer check. */
206
- function summarizeAttribution(rows, publicKeys) {
207
- const versions = { v1: 0, v2: 0, unsigned: 0 };
208
- const agents = new Set();
209
- const runs = new Set();
210
- const keysSeen = new Set();
211
- const checkSigs = Array.isArray(publicKeys) && publicKeys.length > 0;
212
- // Materialize keys ONCE (not per row).
213
- const materialized = [];
214
- if (checkSigs) {
215
- for (const k of publicKeys) {
216
- try {
217
- materialized.push(createPublicKey(k));
218
- }
219
- catch { /* skipped — fail closed below */ }
220
- }
221
- }
222
- const report = { checked: 0, valid: 0, invalid: 0 };
223
- for (const row of rows) {
224
- const att = resolveAttribution(row);
225
- agents.add(att.actor);
226
- if (att.run_id)
227
- runs.add(att.run_id);
228
- const parsed = parseAuditSignature(row.signature);
229
- if (!parsed)
230
- versions.unsigned++;
231
- else if (parsed.version === 2) {
232
- versions.v2++;
233
- if (parsed.key_fp)
234
- keysSeen.add(parsed.key_fp);
235
- }
236
- else
237
- versions.v1++;
238
- if (checkSigs && parsed) {
239
- report.checked++;
240
- const res = verifyAuditEntrySignature(row, materialized);
241
- if (res.ok) {
242
- report.valid++;
243
- if (res.key_fp)
244
- keysSeen.add(res.key_fp);
245
- }
246
- else {
247
- report.invalid++;
248
- if (report.first_invalid_id === undefined) {
249
- report.first_invalid_id = row.id;
250
- report.reason = res.reason;
251
- }
252
- }
253
- }
254
- }
255
- return {
256
- payload_versions: versions,
257
- attribution: { agents: [...agents].sort(), runs: [...runs].sort(), keys: [...keysSeen].sort() },
258
- signatures: checkSigs ? report : undefined,
259
- sigOk: !checkSigs || report.invalid === 0,
260
- sigReason: report.first_invalid_id !== undefined
261
- ? `signature invalid at id ${report.first_invalid_id}: ${report.reason}`
262
- : undefined,
263
- };
264
- }
265
- export class AuditLog {
266
- db;
267
- signer;
268
- constructor(db, signer) {
269
- this.db = db;
270
- this.signer = signer ?? null;
271
- }
272
- /** Swap or clear the signing identity (key rotation, tests). */
273
- setSigner(signer) {
274
- this.signer = signer;
275
- }
276
- /** Get the hash of the most recent entry (or 'genesis' if empty). */
277
- latestHash() {
278
- const row = this.db.prepare('SELECT row_hash FROM audit_log ORDER BY id DESC LIMIT 1').get();
279
- return row?.row_hash ?? GENESIS_HASH;
280
- }
281
- /** Append a hash-chained event. Returns the inserted row.
282
- *
283
- * v7 F2 review fix: the head-read + INSERT pair runs in ONE transaction
284
- * declared IMMEDIATE, so BEGIN takes the cross-process write lock BEFORE
285
- * latestHash() reads the chain head. Two concurrent OS processes appending
286
- * (the T010 per-agent-key fleet shape — audit ops are not in
287
- * DAEMON_WRITE_OPS, so fleet agents write audit_log directly) could
288
- * otherwise both read head H and both insert prev_hash=H, FORKING the
289
- * chain — after which verify() reports the compliance log as tampered
290
- * forever on legitimate writes. */
291
- log(input) {
292
- const payloadJson = JSON.stringify(input.payload);
293
- const loggedAt = new Date().toISOString();
294
- // v7 F2 (T009): attribution columns ride beside the chained fields. The
295
- // hash input is UNCHANGED (6.x verifiers keep verifying); the v2 signed
296
- // payload (T010) is what binds agent_id/run_id tamper-evidently.
297
- const ambient = getActor();
298
- const agentId = input.agent_id !== undefined ? input.agent_id : ambient.agent_id;
299
- const runId = input.run_id !== undefined ? input.run_id : ambient.run_id;
300
- const append = this.db.transaction(() => {
301
- const prevHash = this.latestHash();
302
- const hashInput = `${prevHash}|${input.event_kind}|${payloadJson}|${loggedAt}`;
303
- const rowHash = createHash('sha256').update(hashInput).digest('hex');
304
- // v7 F2 (T010): explicit caller-supplied signature wins (legacy v1 path,
305
- // stored as-is); otherwise a configured signer auto-signs the v2 payload.
306
- const signature = input.signature != null
307
- ? input.signature
308
- : this.signer
309
- ? signAuditEntry(this.signer, { row_hash: rowHash, agent_id: agentId, run_id: runId })
310
- : null;
311
- const info = this.db.prepare(`
1
+ import{createHash as g,createPrivateKey as w,createPublicKey as c,sign as $,verify as y}from"crypto";import{readFileSync as R}from"fs";import{getActor as I}from"./handlers/boundary.js";import{resolveAttribution as O}from"./attribution.js";const h="genesis",p="v2:",x=2,k=16,b=new RegExp(`^[0-9a-f]{${k}}$`);function m(s){if(typeof s!="string"||s.length===0)return null;if(s.startsWith(p)){const e=s.slice(p.length),t=e.indexOf(":"),o=t>=0?e.slice(0,t):"",r=t>=0?e.slice(t+1):"";return{version:2,key_fp:b.test(o)?o:null,sig_b64:r}}return{version:1,key_fp:null,sig_b64:s}}function E(s){const t=(typeof s=="string"?c(s):s).export({type:"spki",format:"der"});return g("sha256").update(t).digest("hex").slice(0,k)}function v(s,e){if(s===1)return Buffer.from(e.row_hash,"utf-8");const t=JSON.stringify({row_hash:e.row_hash,agent_id:e.agent_id??null,run_id:e.run_id??null});return Buffer.concat([Buffer.from([x]),Buffer.from(t,"utf-8")])}function A(s){const e=w(s);if(e.asymmetricKeyType!=="ed25519")throw new Error(`audit signing key must be Ed25519 (got ${e.asymmetricKeyType})`);const t=c(e);return{privateKey:e,publicKeyPem:t.export({type:"spki",format:"pem"}).toString(),key_fp:E(t)}}function D(s,e){const t=$(null,v(2,e),s.privateKey);return`${p}${s.key_fp}:${t.toString("base64")}`}function Y(s=process.env){const e=s.WYRM_AUDIT_SIGNING_KEY?.trim();if(!e)return null;try{const t=e.includes("-----BEGIN")?e:R(e,"utf-8");return A(t)}catch(t){return console.error(`[wyrm] WYRM_AUDIT_SIGNING_KEY unusable (${t.message}) \u2014 audit rows will be unsigned`),null}}function T(s,e){const t=m(s.signature);if(!t)return{ok:!0,version:null,key_fp:null};const o=[];for(const n of e)try{const d=typeof n=="string"?c(n):n;o.push({key:d,fp:E(d)})}catch{}const r=Buffer.from(t.sig_b64,"base64");if(t.version===2){if(!t.key_fp)return{ok:!1,version:2,key_fp:null,reason:"malformed v2 signature (missing key fingerprint)"};const n=o.filter(a=>a.fp===t.key_fp);if(n.length===0)return{ok:!1,version:2,key_fp:t.key_fp,reason:`no provided key matches fingerprint ${t.key_fp}`};const d=v(2,s);for(const a of n)try{if(y(null,d,a.key,r))return{ok:!0,version:2,key_fp:t.key_fp}}catch{}return{ok:!1,version:2,key_fp:t.key_fp,reason:"v2 signature does not verify (row_hash/agent_id/run_id tampered or wrong key)"}}const i=v(1,s);for(const n of o)try{if(y(null,i,n.key,r))return{ok:!0,version:1,key_fp:n.fp}}catch{}return{ok:!1,version:1,key_fp:null,reason:"v1 signature does not verify under any provided key"}}function S(s,e){const t={v1:0,v2:0,unsigned:0},o=new Set,r=new Set,i=new Set,n=Array.isArray(e)&&e.length>0,d=[];if(n)for(const l of e)try{d.push(c(l))}catch{}const a={checked:0,valid:0,invalid:0};for(const l of s){const f=O(l);o.add(f.actor),f.run_id&&r.add(f.run_id);const _=m(l.signature);if(_?_.version===2?(t.v2++,_.key_fp&&i.add(_.key_fp)):t.v1++:t.unsigned++,n&&_){a.checked++;const u=T(l,d);u.ok?(a.valid++,u.key_fp&&i.add(u.key_fp)):(a.invalid++,a.first_invalid_id===void 0&&(a.first_invalid_id=l.id,a.reason=u.reason))}}return{payload_versions:t,attribution:{agents:[...o].sort(),runs:[...r].sort(),keys:[...i].sort()},signatures:n?a:void 0,sigOk:!n||a.invalid===0,sigReason:a.first_invalid_id!==void 0?`signature invalid at id ${a.first_invalid_id}: ${a.reason}`:void 0}}class C{db;signer;constructor(e,t){this.db=e,this.signer=t??null}setSigner(e){this.signer=e}latestHash(){return this.db.prepare("SELECT row_hash FROM audit_log ORDER BY id DESC LIMIT 1").get()?.row_hash??h}log(e){const t=JSON.stringify(e.payload),o=new Date().toISOString(),r=I(),i=e.agent_id!==void 0?e.agent_id:r.agent_id,n=e.run_id!==void 0?e.run_id:r.run_id,d=this.db.transaction(()=>{const a=this.latestHash(),l=`${a}|${e.event_kind}|${t}|${o}`,f=g("sha256").update(l).digest("hex"),_=e.signature!=null?e.signature:this.signer?D(this.signer,{row_hash:f,agent_id:i,run_id:n}):null;return this.db.prepare(`
312
2
  INSERT INTO audit_log
313
3
  (event_kind, actor, project_id, payload_json, prev_hash, row_hash, signature, logged_at, agent_id, run_id)
314
4
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
315
- `).run(input.event_kind, input.actor ?? null, input.project_id ?? null, payloadJson, prevHash, rowHash, signature, loggedAt, agentId, runId);
316
- return info.lastInsertRowid;
317
- });
318
- return this.get(append.immediate());
319
- }
320
- get(id) {
321
- return this.db.prepare('SELECT * FROM audit_log WHERE id = ?').get(id) ?? null;
322
- }
323
- /** Verify the entire chain from the first row through the latest.
324
- * Useful for periodic self-audits.
325
- *
326
- * v7 F2 (T010): pass `publicKeys` (PEM strings) to additionally verify the
327
- * Ed25519 signature layer; the report then answers who / which-run /
328
- * which-key. Without keys the result is the 6.x chain-only semantics plus
329
- * the attribution summary. */
330
- verify(opts) {
331
- const rows = this.db.prepare('SELECT * FROM audit_log ORDER BY id ASC').all();
332
- if (rows.length === 0) {
333
- return {
334
- ok: true, total: 0, verified: 0,
335
- payload_versions: { v1: 0, v2: 0, unsigned: 0 },
336
- attribution: { agents: [], runs: [], keys: [] },
337
- ...(opts?.publicKeys?.length ? { signatures: { checked: 0, valid: 0, invalid: 0 } } : {}),
338
- };
339
- }
340
- let expectedPrev = GENESIS_HASH;
341
- for (const row of rows) {
342
- if (row.prev_hash !== expectedPrev) {
343
- return {
344
- ok: false, total: rows.length, verified: row.id - 1,
345
- first_invalid_id: row.id,
346
- reason: `prev_hash mismatch: expected ${expectedPrev}, got ${row.prev_hash}`,
347
- };
348
- }
349
- const recomputed = createHash('sha256')
350
- .update(`${row.prev_hash}|${row.event_kind}|${row.payload_json}|${row.logged_at}`)
351
- .digest('hex');
352
- if (recomputed !== row.row_hash) {
353
- return {
354
- ok: false, total: rows.length, verified: row.id - 1,
355
- first_invalid_id: row.id,
356
- reason: `row_hash mismatch at id ${row.id}: recomputed ${recomputed}, stored ${row.row_hash}`,
357
- };
358
- }
359
- expectedPrev = row.row_hash;
360
- }
361
- const s = summarizeAttribution(rows, opts?.publicKeys);
362
- return {
363
- ok: s.sigOk,
364
- total: rows.length,
365
- verified: rows.length,
366
- ...(s.sigOk ? {} : { reason: s.sigReason }),
367
- payload_versions: s.payload_versions,
368
- attribution: s.attribution,
369
- ...(s.signatures ? { signatures: s.signatures } : {}),
370
- };
371
- }
372
- /** Export a date-ranged tamper-evident bundle. Range is inclusive on
373
- * both ends. Verification still walks from genesis to ensure no gaps. */
374
- export(opts) {
375
- const params = [];
376
- let where = '';
377
- if (opts.range_start) {
378
- where += ' AND logged_at >= ?';
379
- params.push(opts.range_start);
380
- }
381
- if (opts.range_end) {
382
- where += ' AND logged_at <= ?';
383
- params.push(opts.range_end);
384
- }
385
- const entries = this.db.prepare(`SELECT * FROM audit_log WHERE 1=1 ${where} ORDER BY id ASC`).all(...params);
386
- return {
387
- version: 1,
388
- range: {
389
- start: opts.range_start ?? entries[0]?.logged_at ?? new Date(0).toISOString(),
390
- end: opts.range_end ?? entries[entries.length - 1]?.logged_at ?? new Date().toISOString(),
391
- },
392
- exported_at: new Date().toISOString(),
393
- entry_count: entries.length,
394
- first_id: entries[0]?.id ?? null,
395
- last_id: entries[entries.length - 1]?.id ?? null,
396
- genesis_prev_hash: entries[0]?.prev_hash ?? GENESIS_HASH,
397
- final_row_hash: entries[entries.length - 1]?.row_hash ?? GENESIS_HASH,
398
- entries,
399
- };
400
- }
401
- /** Statically verify a previously-exported bundle (no DB required).
402
- * Useful for the external auditor who only has the JSON file.
403
- *
404
- * The chain walk is the unchanged 6.x structure — it never parses
405
- * signatures, so mixed v1/v2 bundles verify exactly as they did under 6.x.
406
- * v7 F2 (T010): optional `publicKeys` adds the signature-layer check, same
407
- * as the instance `verify()`. */
408
- static verifyBundle(bundle, opts) {
409
- if (bundle.version !== 1) {
410
- return { ok: false, total: bundle.entry_count, verified: 0, reason: `Unknown bundle version ${bundle.version}` };
411
- }
412
- let expectedPrev = bundle.genesis_prev_hash;
413
- let verified = 0;
414
- for (const row of bundle.entries) {
415
- if (row.prev_hash !== expectedPrev) {
416
- return {
417
- ok: false, total: bundle.entries.length, verified,
418
- first_invalid_id: row.id,
419
- reason: `prev_hash mismatch: expected ${expectedPrev}, got ${row.prev_hash}`,
420
- };
421
- }
422
- const recomputed = createHash('sha256')
423
- .update(`${row.prev_hash}|${row.event_kind}|${row.payload_json}|${row.logged_at}`)
424
- .digest('hex');
425
- if (recomputed !== row.row_hash) {
426
- return {
427
- ok: false, total: bundle.entries.length, verified,
428
- first_invalid_id: row.id,
429
- reason: `row_hash mismatch at id ${row.id}`,
430
- };
431
- }
432
- expectedPrev = row.row_hash;
433
- verified++;
434
- }
435
- if (bundle.entries.length > 0 && bundle.final_row_hash !== expectedPrev) {
436
- return {
437
- ok: false, total: bundle.entries.length, verified,
438
- reason: `final_row_hash mismatch`,
439
- };
440
- }
441
- const s = summarizeAttribution(bundle.entries, opts?.publicKeys);
442
- return {
443
- ok: s.sigOk,
444
- total: bundle.entries.length,
445
- verified,
446
- ...(s.sigOk ? {} : { reason: s.sigReason }),
447
- payload_versions: s.payload_versions,
448
- attribution: s.attribution,
449
- ...(s.signatures ? { signatures: s.signatures } : {}),
450
- };
451
- }
452
- /** Slim query for recent entries — UI / dashboards. */
453
- recent(limit = 100, kind) {
454
- if (kind) {
455
- return this.db.prepare('SELECT * FROM audit_log WHERE event_kind = ? ORDER BY id DESC LIMIT ?').all(kind, limit);
456
- }
457
- return this.db.prepare('SELECT * FROM audit_log ORDER BY id DESC LIMIT ?').all(limit);
458
- }
459
- }
460
- //# sourceMappingURL=audit.js.map
5
+ `).run(e.event_kind,e.actor??null,e.project_id??null,t,a,f,_,o,i,n).lastInsertRowid});return this.get(d.immediate())}get(e){return this.db.prepare("SELECT * FROM audit_log WHERE id = ?").get(e)??null}verify(e){const t=this.db.prepare("SELECT * FROM audit_log ORDER BY id ASC").all();if(t.length===0)return{ok:!0,total:0,verified:0,payload_versions:{v1:0,v2:0,unsigned:0},attribution:{agents:[],runs:[],keys:[]},...e?.publicKeys?.length?{signatures:{checked:0,valid:0,invalid:0}}:{}};let o=h;for(const i of t){if(i.prev_hash!==o)return{ok:!1,total:t.length,verified:i.id-1,first_invalid_id:i.id,reason:`prev_hash mismatch: expected ${o}, got ${i.prev_hash}`};const n=g("sha256").update(`${i.prev_hash}|${i.event_kind}|${i.payload_json}|${i.logged_at}`).digest("hex");if(n!==i.row_hash)return{ok:!1,total:t.length,verified:i.id-1,first_invalid_id:i.id,reason:`row_hash mismatch at id ${i.id}: recomputed ${n}, stored ${i.row_hash}`};o=i.row_hash}const r=S(t,e?.publicKeys);return{ok:r.sigOk,total:t.length,verified:t.length,...r.sigOk?{}:{reason:r.sigReason},payload_versions:r.payload_versions,attribution:r.attribution,...r.signatures?{signatures:r.signatures}:{}}}export(e){const t=[];let o="";e.range_start&&(o+=" AND logged_at >= ?",t.push(e.range_start)),e.range_end&&(o+=" AND logged_at <= ?",t.push(e.range_end));const r=this.db.prepare(`SELECT * FROM audit_log WHERE 1=1 ${o} ORDER BY id ASC`).all(...t);return{version:1,range:{start:e.range_start??r[0]?.logged_at??new Date(0).toISOString(),end:e.range_end??r[r.length-1]?.logged_at??new Date().toISOString()},exported_at:new Date().toISOString(),entry_count:r.length,first_id:r[0]?.id??null,last_id:r[r.length-1]?.id??null,genesis_prev_hash:r[0]?.prev_hash??h,final_row_hash:r[r.length-1]?.row_hash??h,entries:r}}static verifyBundle(e,t){if(e.version!==1)return{ok:!1,total:e.entry_count,verified:0,reason:`Unknown bundle version ${e.version}`};let o=e.genesis_prev_hash,r=0;for(const n of e.entries){if(n.prev_hash!==o)return{ok:!1,total:e.entries.length,verified:r,first_invalid_id:n.id,reason:`prev_hash mismatch: expected ${o}, got ${n.prev_hash}`};if(g("sha256").update(`${n.prev_hash}|${n.event_kind}|${n.payload_json}|${n.logged_at}`).digest("hex")!==n.row_hash)return{ok:!1,total:e.entries.length,verified:r,first_invalid_id:n.id,reason:`row_hash mismatch at id ${n.id}`};o=n.row_hash,r++}if(e.entries.length>0&&e.final_row_hash!==o)return{ok:!1,total:e.entries.length,verified:r,reason:"final_row_hash mismatch"};const i=S(e.entries,t?.publicKeys);return{ok:i.sigOk,total:e.entries.length,verified:r,...i.sigOk?{}:{reason:i.sigReason},payload_versions:i.payload_versions,attribution:i.attribution,...i.signatures?{signatures:i.signatures}:{}}}recent(e=100,t){return t?this.db.prepare("SELECT * FROM audit_log WHERE event_kind = ? ORDER BY id DESC LIMIT ?").all(t,e):this.db.prepare("SELECT * FROM audit_log ORDER BY id DESC LIMIT ?").all(e)}}export{p as AUDIT_SIG_V2_PREFIX,C as AuditLog,E as auditKeyFingerprint,v as buildSignedPayload,A as createAuditSigner,Y as loadAuditSignerFromEnv,m as parseAuditSignature,D as signAuditEntry,T as verifyAuditEntrySignature};
@@ -1,23 +1,4 @@
1
- /**
2
- * Auto-extraction (bet #2) — turn freeform text (a session, notes, a transcript)
3
- * into candidate memories that land in the REVIEW QUEUE for operator approval.
4
- *
5
- * Closes the "just talk, it remembers" gap WITHOUT an expensive cloud LLM, via a
6
- * pluggable LOCAL extractor:
7
- * - LLM: any Ollama model (`WYRM_EXTRACT_MODEL`) — the slot the future DragonSpark
8
- * nano-LLM drops into. Structured JSON out, robustly parsed.
9
- * - Deterministic fallback: sentence segmentation + signal markers + classifyCapture,
10
- * used when no model is configured or the LLM is unreachable.
11
- * Always produces candidates, never blocks a write, never throws. Candidates are
12
- * stored with needs_review=1 so the operator vets them (wyrm_review) — nothing
13
- * auto-trusted, matching Wyrm's distillation-queue discipline.
14
- *
15
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
16
- * @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
17
- */
18
- import { classifyCapture } from './capture.js';
19
- const KINDS = ['truth', 'failure', 'decision', 'pattern', 'lesson'];
20
- const EXTRACT_PROMPT = (text) => `You extract DURABLE, REUSABLE memories from a developer's working notes/transcript.
1
+ import{classifyCapture as l}from"./capture.js";const d=["truth","failure","decision","pattern","lesson"],p=t=>`You extract DURABLE, REUSABLE memories from a developer's working notes/transcript.
21
2
  Output ONLY a JSON array, no prose. Each item: {"kind": one of ["truth","failure","decision","pattern","lesson"], "text": "<one self-contained sentence>"}.
22
3
  - truth: a validated fact/constraint ("X uses Y", "Z is required").
23
4
  - failure: an approach that FAILED and why (so it is not repeated).
@@ -28,122 +9,6 @@ Skip chit-chat, questions, and ephemeral status. Extract 0-8 items; fewer + high
28
9
 
29
10
  NOTES:
30
11
  """
31
- ${text.slice(0, 8000)}
12
+ ${t.slice(0,8e3)}
32
13
  """
33
- JSON:`;
34
- /** Robustly pull a candidate array out of an LLM response (small models wrap JSON in prose/markdown). */
35
- export function parseCandidates(raw) {
36
- const m = raw.match(/\[[\s\S]*\]/);
37
- if (!m)
38
- return [];
39
- let arr;
40
- try {
41
- arr = JSON.parse(m[0]);
42
- }
43
- catch {
44
- return [];
45
- }
46
- if (!Array.isArray(arr))
47
- return [];
48
- const out = [];
49
- const seen = new Set();
50
- for (const it of arr) {
51
- if (!it || typeof it !== 'object')
52
- continue;
53
- const kind = it.kind;
54
- const text = it.text;
55
- if (typeof text !== 'string' || text.trim().length < 8)
56
- continue;
57
- const t = text.trim().slice(0, 1000);
58
- const key = t.toLowerCase().replace(/\s+/g, ' ').slice(0, 80);
59
- if (seen.has(key))
60
- continue;
61
- seen.add(key);
62
- out.push({ kind: KINDS.includes(kind) ? kind : 'pattern', text: t, confidence: 0.6 });
63
- if (out.length >= 12)
64
- break;
65
- }
66
- return out;
67
- }
68
- async function extractViaLLM(text, model, url) {
69
- try {
70
- const res = await fetch(`${url.replace(/\/$/, '')}/api/generate`, {
71
- method: 'POST',
72
- headers: { 'Content-Type': 'application/json' },
73
- body: JSON.stringify({ model, prompt: EXTRACT_PROMPT(text), stream: false, options: { temperature: 0 } }),
74
- signal: AbortSignal.timeout(120_000),
75
- });
76
- if (!res.ok)
77
- return null;
78
- const data = await res.json();
79
- return data.response ? parseCandidates(data.response) : null;
80
- }
81
- catch {
82
- return null;
83
- }
84
- }
85
- /** No-LLM extractor: segment into statements, keep durable-looking ones, classify. */
86
- export function extractDeterministic(text) {
87
- const segs = text.split(/(?<=[.!?])\s+|\n+/).map((s) => s.trim()).filter((s) => s.length >= 20 && s.length <= 400);
88
- const out = [];
89
- const seen = new Set();
90
- for (const s of segs) {
91
- if (!/\b(use[sd]?|decided|because|fail(s|ed)?|error|always|never|must|should|learned|lesson|turned out|prefer|avoid|root cause|fixed|broke|requires?)\b/i.test(s))
92
- continue;
93
- const key = s.toLowerCase().replace(/\s+/g, ' ').slice(0, 80);
94
- if (seen.has(key))
95
- continue;
96
- seen.add(key);
97
- const c = classifyCapture(s);
98
- const kind = /\b(fail(s|ed)?|error|broke|bug|mistake)\b/i.test(s) ? 'failure'
99
- : c.type === 'truth' ? 'truth'
100
- : /\bbecause\b/i.test(s) ? 'decision'
101
- : c.subtype === 'lesson' ? 'lesson'
102
- : 'pattern';
103
- out.push({ kind, text: s, confidence: (c.confidence ?? 60) / 100 });
104
- if (out.length >= 8)
105
- break;
106
- }
107
- return out;
108
- }
109
- /**
110
- * Extract candidate memories from text. Tries the configured local LLM first
111
- * (if any), falls back to deterministic. Never throws.
112
- */
113
- export async function extractCandidates(text, opts = {}) {
114
- const url = opts.ollamaUrl || process.env.WYRM_OLLAMA_URL || 'http://localhost:11434';
115
- const model = opts.model || process.env.WYRM_EXTRACT_MODEL || '';
116
- if (model) {
117
- const llm = await extractViaLLM(text, model, url);
118
- if (llm && llm.length)
119
- return { candidates: llm, method: 'llm', model };
120
- }
121
- return { candidates: extractDeterministic(text), method: 'deterministic' };
122
- }
123
- /**
124
- * Escape SQL LIKE metacharacters (`%`, `_`, and the escape char itself) so a
125
- * CONTENT-DERIVED dedup signature can be bound into `LIKE ? ESCAPE '\'`
126
- * without acting as a wildcard (F3 security pass #1, confirmed finding: the
127
- * ax:/harvest sig is derived from caller text, so a learning containing `%`
128
- * previously turned the dedup probe into a broad-match pattern that silently
129
- * suppressed legitimate capture — counted as `skipped`, never surfaced).
130
- * Every sig-dedup probe (run debrief, wyrm_auto_capture, harvest MCP + CLI)
131
- * routes through this one helper.
132
- */
133
- export function escapeLikePattern(s) {
134
- return s.replace(/[\\%_]/g, (ch) => '\\' + ch);
135
- }
136
- /** Map a candidate to its review-queue memory-artifact shape (needs_review=1). */
137
- export function candidateToArtifact(c) {
138
- const map = {
139
- failure: 'anti_pattern',
140
- decision: 'reasoning_trace',
141
- lesson: 'lesson',
142
- pattern: 'pattern',
143
- truth: 'heuristic',
144
- };
145
- // A stable signature for dedup against re-extraction of the same text.
146
- const sig = 'ax:' + c.text.toLowerCase().replace(/\s+/g, ' ').trim().slice(0, 64);
147
- return { kind: map[c.kind], problem: c.text, tags: ['auto-extract', `intent:${c.kind}`, sig], confidence: c.confidence };
148
- }
149
- //# sourceMappingURL=auto-capture.js.map
14
+ JSON:`;function f(t){const n=t.match(/\[[\s\S]*\]/);if(!n)return[];let r;try{r=JSON.parse(n[0])}catch{return[]}if(!Array.isArray(r))return[];const s=[],e=new Set;for(const a of r){if(!a||typeof a!="object")continue;const o=a.kind,i=a.text;if(typeof i!="string"||i.trim().length<8)continue;const c=i.trim().slice(0,1e3),u=c.toLowerCase().replace(/\s+/g," ").slice(0,80);if(!e.has(u)&&(e.add(u),s.push({kind:d.includes(o)?o:"pattern",text:c,confidence:.6}),s.length>=12))break}return s}async function h(t,n,r){try{const s=await fetch(`${r.replace(/\/$/,"")}/api/generate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model:n,prompt:p(t),stream:!1,options:{temperature:0}}),signal:AbortSignal.timeout(12e4)});if(!s.ok)return null;const e=await s.json();return e.response?f(e.response):null}catch{return null}}function m(t){const n=t.split(/(?<=[.!?])\s+|\n+/).map(e=>e.trim()).filter(e=>e.length>=20&&e.length<=400),r=[],s=new Set;for(const e of n){if(!/\b(use[sd]?|decided|because|fail(s|ed)?|error|always|never|must|should|learned|lesson|turned out|prefer|avoid|root cause|fixed|broke|requires?)\b/i.test(e))continue;const a=e.toLowerCase().replace(/\s+/g," ").slice(0,80);if(s.has(a))continue;s.add(a);const o=l(e),i=/\b(fail(s|ed)?|error|broke|bug|mistake)\b/i.test(e)?"failure":o.type==="truth"?"truth":/\bbecause\b/i.test(e)?"decision":o.subtype==="lesson"?"lesson":"pattern";if(r.push({kind:i,text:e,confidence:(o.confidence??60)/100}),r.length>=8)break}return r}async function b(t,n={}){const r=n.ollamaUrl||process.env.WYRM_OLLAMA_URL||"http://localhost:11434",s=n.model||process.env.WYRM_EXTRACT_MODEL||"";if(s){const e=await h(t,s,r);if(e&&e.length)return{candidates:e,method:"llm",model:s}}return{candidates:m(t),method:"deterministic"}}function k(t){return t.replace(/[\\%_]/g,n=>"\\"+n)}function y(t){const n={failure:"anti_pattern",decision:"reasoning_trace",lesson:"lesson",pattern:"pattern",truth:"heuristic"},r="ax:"+t.text.toLowerCase().replace(/\s+/g," ").trim().slice(0,64);return{kind:n[t.kind],problem:t.text,tags:["auto-extract",`intent:${t.kind}`,r],confidence:t.confidence}}export{y as candidateToArtifact,k as escapeLikePattern,b as extractCandidates,m as extractDeterministic,f as parseCandidates};