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.
- package/LICENSE +26 -667
- package/NOTICE +14 -33
- package/dist/activation.d.ts.map +1 -1
- package/dist/activation.js +1 -44
- package/dist/activation.js.map +1 -1
- package/dist/agent-daemon.js +4 -281
- package/dist/agent-loop.js +7 -332
- package/dist/analytics.js +13 -236
- package/dist/attribution.js +1 -49
- package/dist/audit.js +2 -457
- package/dist/auto-capture.js +3 -138
- package/dist/auto-orchestrator.js +1 -325
- package/dist/autoconfig.js +39 -840
- package/dist/buddy-runner.js +1 -109
- package/dist/buddy.js +14 -564
- package/dist/build-flags.js +1 -17
- package/dist/capabilities.js +3 -183
- package/dist/capture.js +1 -56
- package/dist/causality.js +6 -107
- package/dist/cli.js +20 -281
- package/dist/cloud/cli.js +5 -541
- package/dist/cloud/client.js +1 -221
- package/dist/cloud/crypto.js +1 -85
- package/dist/cloud/machine-id.js +2 -113
- package/dist/cloud/recovery.js +1 -60
- package/dist/cloud/sync-engine.js +7 -543
- package/dist/cloud-backup.js +5 -579
- package/dist/cloud-profile.js +1 -138
- package/dist/cloud-sync-entrypoint.js +1 -47
- package/dist/cloud-sync.js +2 -309
- package/dist/constellation.js +12 -168
- package/dist/context-build-budgeted.js +4 -144
- package/dist/context-ranking.js +1 -69
- package/dist/crypto.js +1 -179
- package/dist/daemon-write-endpoint.js +1 -290
- package/dist/daemon-writer.js +2 -406
- package/dist/database.js +43 -1110
- package/dist/deprecations.js +2 -162
- package/dist/design.js +13 -141
- package/dist/event-replication.js +1 -112
- package/dist/events-sse.js +7 -43
- package/dist/events.js +6 -238
- package/dist/failure-patterns.js +42 -659
- package/dist/federation.js +12 -236
- package/dist/goals.js +13 -101
- package/dist/golden.js +3 -355
- package/dist/handlers/agent.js +4 -165
- package/dist/handlers/alias-adapters.js +1 -129
- package/dist/handlers/aliases.js +1 -171
- package/dist/handlers/audit.js +1 -87
- package/dist/handlers/boundary.js +1 -221
- package/dist/handlers/capture.js +73 -1109
- package/dist/handlers/causality.js +7 -114
- package/dist/handlers/cloud.js +85 -382
- package/dist/handlers/companion.js +28 -459
- package/dist/handlers/datalake.js +7 -187
- package/dist/handlers/dispatch-context.js +0 -22
- package/dist/handlers/entity.js +25 -256
- package/dist/handlers/events.js +16 -335
- package/dist/handlers/failure.js +13 -340
- package/dist/handlers/goals.js +4 -296
- package/dist/handlers/intelligence.js +126 -674
- package/dist/handlers/invoicing.js +1 -70
- package/dist/handlers/mcpclient.js +6 -137
- package/dist/handlers/orchestration.js +40 -125
- package/dist/handlers/output-schemas.js +1 -24
- package/dist/handlers/presence.js +3 -99
- package/dist/handlers/project.js +28 -182
- package/dist/handlers/prompts.js +6 -157
- package/dist/handlers/quest.js +4 -224
- package/dist/handlers/recall.js +11 -218
- package/dist/handlers/registry.js +1 -167
- package/dist/handlers/resources.js +1 -288
- package/dist/handlers/review.js +11 -74
- package/dist/handlers/run.js +17 -487
- package/dist/handlers/search.js +15 -326
- package/dist/handlers/session.js +28 -615
- package/dist/handlers/share.js +8 -184
- package/dist/handlers/shims.js +1 -464
- package/dist/handlers/skill.js +67 -449
- package/dist/handlers/survivors.js +1 -120
- package/dist/handlers/symbols.js +8 -109
- package/dist/handlers/syncops.js +4 -302
- package/dist/handlers/types.js +1 -27
- package/dist/harvest.js +5 -191
- package/dist/hours.js +7 -156
- package/dist/http-auth.js +3 -321
- package/dist/http-fast.js +21 -1137
- package/dist/icons.js +1 -47
- package/dist/index.js +2 -924
- package/dist/indexer.js +4 -145
- package/dist/intelligence.js +31 -261
- package/dist/internal-dispatch.js +3 -212
- package/dist/keyset.js +1 -110
- package/dist/knowledge-graph.js +12 -176
- package/dist/license.d.ts +11 -0
- package/dist/license.d.ts.map +1 -1
- package/dist/license.js +2 -414
- package/dist/license.js.map +1 -1
- package/dist/logger.js +2 -199
- package/dist/maintenance.js +2 -148
- package/dist/mcp-client.js +6 -262
- package/dist/memory-artifacts.js +30 -449
- package/dist/migrate-prompt.js +2 -124
- package/dist/migrations.js +40 -655
- package/dist/performance.js +1 -228
- package/dist/presence.js +11 -140
- package/dist/priority-embed.js +5 -164
- package/dist/providers/embedding-provider.js +1 -196
- package/dist/readonly-gate.js +1 -29
- package/dist/rehydration.js +9 -157
- package/dist/reindex.js +1 -88
- package/dist/render-target.js +21 -514
- package/dist/render.js +4 -280
- package/dist/repl-guard.js +1 -173
- package/dist/replication-daemon-entrypoint.js +1 -31
- package/dist/replication-daemon.js +2 -262
- package/dist/resilience.js +1 -591
- package/dist/reverse-bridge.js +5 -360
- package/dist/security.js +1 -244
- package/dist/session-seen.js +3 -51
- package/dist/setup.js +1 -260
- package/dist/skill-author.js +5 -168
- package/dist/spec-kit.js +1 -191
- package/dist/sqlite-busy.js +1 -154
- package/dist/statusline.js +11 -315
- package/dist/sub-agent.js +13 -262
- package/dist/summarizer.js +13 -139
- package/dist/symbols.js +7 -283
- package/dist/sync.js +5 -359
- package/dist/tasks-dispatch.js +1 -84
- package/dist/tasks.js +1 -282
- package/dist/token-budget.js +1 -143
- package/dist/tool-analytics.js +7 -129
- package/dist/tool-annotations.js +1 -365
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/tool-profiles.js +1 -75
- package/dist/trace-harvest.js +6 -244
- package/dist/types.js +1 -30
- package/dist/ui-dashboard.js +41 -50
- package/dist/ulid.js +1 -81
- package/dist/validate.js +1 -129
- package/dist/vault.js +1 -534
- package/dist/vectors.js +3 -184
- package/dist/version-check.js +4 -136
- package/dist/visibility.js +19 -155
- package/dist/wyrm-cli.js +98 -2451
- package/dist/wyrm-cli.js.map +1 -1
- package/dist/wyrm-guard.js +14 -424
- package/dist/wyrm-loop.js +3 -150
- package/dist/wyrm-manifest.json +1 -1
- package/dist/wyrm-statusline-daemon.js +1 -11
- package/dist/wyrm-statusline.js +4 -56
- package/dist/wyrm-ui.js +9 -77
- package/package.json +4 -2
package/dist/wyrm-guard.js
CHANGED
|
@@ -1,425 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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)});
|
package/dist/wyrm-manifest.json
CHANGED
|
@@ -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();
|