svamp-cli 0.2.223-canary.2 → 0.2.224

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 (39) hide show
  1. package/dist/{agentCommands-MGRUIvWB.mjs → agentCommands-BignK4hn.mjs} +5 -5
  2. package/dist/{auth-FvyDIAch.mjs → auth-tP-DHfsI.mjs} +1 -1
  3. package/dist/cli.mjs +61 -61
  4. package/dist/{commands-Cmjb9Je1.mjs → commands-1IupY9qh.mjs} +6 -6
  5. package/dist/{commands-Dm3EQICq.mjs → commands-Bpq8_9OU.mjs} +2 -2
  6. package/dist/{commands-BhFr0kUL.mjs → commands-C1VIwqOH.mjs} +1 -1
  7. package/dist/{commands-_RXnsA1v.mjs → commands-CA8AMvq-.mjs} +1 -1
  8. package/dist/{commands-BiIkCdGB.mjs → commands-DMoXgLUh.mjs} +1 -1
  9. package/dist/{commands-BuecUAk8.mjs → commands-DqvzIDMK.mjs} +13 -1
  10. package/dist/{commands-nlra_-D0.mjs → commands-ez1xdcH5.mjs} +2 -2
  11. package/dist/{fleet-Bi3KIev7.mjs → fleet-B6Rh4mje.mjs} +1 -1
  12. package/dist/{frpc-BpQyeWvI.mjs → frpc-vpwKk0kV.mjs} +1 -1
  13. package/dist/{headlessCli-Be0bYOUB.mjs → headlessCli-C51w8i7T.mjs} +2 -2
  14. package/dist/index.mjs +1 -1
  15. package/dist/package-CzzAMi8h.mjs +63 -0
  16. package/dist/{rpc-CRZWx5YY.mjs → rpc-CcQQrbBO.mjs} +1 -1
  17. package/dist/{rpc-WS0yDVqB.mjs → rpc-D7QFTk_g.mjs} +1 -1
  18. package/dist/{run-8_mnLg6R.mjs → run-BZrPFG_w.mjs} +1 -1
  19. package/dist/{run-DVdqRhtp.mjs → run-C-9ZtEi4.mjs} +77 -33
  20. package/dist/{scheduler-Dx-QrQfP.mjs → scheduler-jlX6Mkxf.mjs} +1 -1
  21. package/dist/{serveCommands-CI2P6buK.mjs → serveCommands-Dhrv-HAD.mjs} +5 -5
  22. package/dist/{serveManager-B-keYGE9.mjs → serveManager-DtBN8E9D.mjs} +2 -2
  23. package/dist/{sideband-BdYb3ezu.mjs → sideband-DxH9bMhv.mjs} +1 -1
  24. package/package.json +3 -3
  25. package/bin/skills/loop/IMPLEMENTATION_PROGRESS.md +0 -49
  26. package/bin/skills/loop/SKILL.md +0 -100
  27. package/bin/skills/loop/bin/channel-core.mjs +0 -161
  28. package/bin/skills/loop/bin/channel-server.mjs +0 -151
  29. package/bin/skills/loop/bin/inject-loop.mjs +0 -90
  30. package/bin/skills/loop/bin/loop-init.mjs +0 -194
  31. package/bin/skills/loop/bin/loop-status.mjs +0 -51
  32. package/bin/skills/loop/bin/precompact.mjs +0 -30
  33. package/bin/skills/loop/bin/routine-core.mjs +0 -126
  34. package/bin/skills/loop/bin/state-fp.mjs +0 -111
  35. package/bin/skills/loop/bin/stop-gate.mjs +0 -349
  36. package/bin/skills/loop/test/test-channel-core.mjs +0 -86
  37. package/bin/skills/loop/test/test-loop-gate.mjs +0 -489
  38. package/bin/skills/loop/test/test-routine-core.mjs +0 -54
  39. package/dist/package-CwGmpH_-.mjs +0 -63
@@ -1,489 +0,0 @@
1
- #!/usr/bin/env node
2
- // Deterministic tests for the loop Stop-gate (no LLM). Proves the harness gate
3
- // blocks early-quit until oracle passes AND a fresh evaluator verdict says done.
4
- import { execFileSync } from 'node:child_process';
5
- import { mkdtempSync, writeFileSync, readFileSync, rmSync, existsSync } from 'node:fs';
6
- import { tmpdir } from 'node:os';
7
- import { join, dirname, resolve } from 'node:path';
8
- import { fileURLToPath } from 'node:url';
9
-
10
- const HERE = dirname(fileURLToPath(import.meta.url));
11
- const INIT = resolve(HERE, '..', 'bin', 'loop-init.mjs');
12
- const STATE_FP = resolve(HERE, '..', 'bin', 'state-fp.mjs');
13
- const node = process.execPath;
14
-
15
- // Loops are SESSION-SCOPED under .svamp/<sid>/loop. Run every project with a session
16
- // id so the test exercises the real production layout + env-resolved hooks.
17
- const SID = 'test-session-0000';
18
-
19
- let pass = 0, fail = 0;
20
- function ok(cond, msg) { if (cond) { pass++; console.log(` ✓ ${msg}`); } else { fail++; console.log(` ✗ ${msg}`); } }
21
-
22
- function git(dir, args) { return execFileSync('git', ['-C', dir, ...args], { encoding: 'utf8' }); }
23
- function sh(dir, cmd) { return execFileSync('bash', ['-lc', cmd], { cwd: dir, encoding: 'utf8' }); }
24
-
25
- // Run the COPIED stop-gate inside the project; return {blocked, reason, exit}.
26
- function runGate(dir, stopHookActive = false, transcriptPath = undefined) {
27
- const gate = join(dir, '.svamp', SID, 'loop', 'bin', 'stop-gate.mjs');
28
- let out = '', code = 0;
29
- try {
30
- // The daemon injects SVAMP_SESSION_ID + CLAUDE_PROJECT_DIR; the gate resolves its
31
- // session-scoped loop dir from them (so a shared settings.json gates the right loop).
32
- out = execFileSync(node, [gate], {
33
- input: JSON.stringify({ stop_hook_active: stopHookActive, hook_event_name: 'Stop', cwd: dir, transcript_path: transcriptPath }),
34
- encoding: 'utf8',
35
- env: { ...process.env, SVAMP_SESSION_ID: SID, CLAUDE_PROJECT_DIR: dir },
36
- });
37
- } catch (e) { out = e.stdout || ''; code = e.status || 1; }
38
- let parsed = null; try { parsed = JSON.parse(out); } catch {}
39
- return { blocked: parsed?.decision === 'block', reason: parsed?.reason || '', exit: code, raw: out };
40
- }
41
- function fp(dir) { return execFileSync(node, [STATE_FP, dir], { encoding: 'utf8' }).trim(); }
42
- function readState(dir) { return JSON.parse(readFileSync(join(dir, '.svamp', SID, 'loop', 'loop-state.json'), 'utf8')); }
43
- function writeVerdict(dir, obj) { writeFileSync(join(dir, '.svamp', SID, 'loop', 'evaluator-verdict.json'), JSON.stringify(obj)); }
44
-
45
- function newProject({ evaluator = 'on', max = 20 } = {}) {
46
- const dir = mkdtempSync(join(tmpdir(), 'loopgate-'));
47
- git(dir, ['init', '-q']);
48
- git(dir, ['config', 'user.email', 't@t']); git(dir, ['config', 'user.name', 't']);
49
- writeFileSync(join(dir, 'answer.txt'), 'TODO\n');
50
- git(dir, ['add', '-A']); git(dir, ['commit', '-qm', 'init']);
51
- execFileSync(node, [INIT, dir, '--session', SID, '--task', 'make answer.txt contain DONE',
52
- '--oracle', 'grep -q DONE answer.txt', '--evaluator', evaluator, '--max', String(max)],
53
- { encoding: 'utf8' });
54
- return dir;
55
- }
56
-
57
- const dirs = [];
58
- try {
59
- // ---- Test 1: oracle fails -> blocked ----
60
- console.log('Test 1: oracle failing blocks stop');
61
- { const d = newProject(); dirs.push(d);
62
- const r = runGate(d);
63
- ok(r.blocked, 'gate blocks when oracle fails');
64
- ok(/oracle FAILED/.test(r.reason), 'reason explains oracle failure');
65
- ok(readState(d).iteration === 1, 'iteration incremented to 1');
66
- }
67
-
68
- // ---- Test 2: oracle passes, evaluator on, no verdict -> blocked asking for verdict ----
69
- console.log('Test 2: oracle pass but missing evaluator verdict blocks');
70
- let dEval;
71
- { const d = newProject(); dirs.push(d); dEval = d;
72
- writeFileSync(join(d, 'answer.txt'), 'DONE\n');
73
- const r = runGate(d);
74
- ok(r.blocked, 'gate blocks with no verdict even though oracle passes');
75
- ok(/evaluator/i.test(r.reason), 'reason asks for an independent evaluator verdict');
76
- }
77
-
78
- // ---- Test 3: fresh matching verdict=done -> allowed ----
79
- console.log('Test 3: oracle pass + fresh verdict done allows stop');
80
- { const d = dEval;
81
- writeVerdict(d, { verdict: 'done', reason: 'looks complete', state_fp: fp(d) });
82
- const r = runGate(d);
83
- ok(!r.blocked && r.exit === 0, 'gate allows stop (loop complete)');
84
- ok(readState(d).active === false && readState(d).phase === 'done', 'state marked done/inactive');
85
- }
86
-
87
- // ---- Test 4: stale verdict (code changed after verdict) -> blocked ----
88
- console.log('Test 4: stale verdict is rejected');
89
- { const d = newProject(); dirs.push(d);
90
- writeFileSync(join(d, 'answer.txt'), 'DONE\n');
91
- writeVerdict(d, { verdict: 'done', reason: 'ok', state_fp: fp(d) });
92
- writeFileSync(join(d, 'answer.txt'), 'DONE \n'); // edit AFTER verdict -> fp changes
93
- const r = runGate(d);
94
- ok(r.blocked, 'gate blocks because verdict no longer matches current code');
95
- ok(/STALE/i.test(r.reason), 'reason flags the verdict as stale');
96
- }
97
-
98
- // ---- Test 5: verdict says continue -> blocked ----
99
- console.log('Test 5: evaluator verdict=continue blocks');
100
- { const d = newProject(); dirs.push(d);
101
- writeFileSync(join(d, 'answer.txt'), 'DONE\n');
102
- writeVerdict(d, { verdict: 'continue', reason: 'edge case missing', guidance: 'handle X', state_fp: fp(d) });
103
- const r = runGate(d);
104
- ok(r.blocked, 'gate blocks when evaluator says continue');
105
- ok(/edge case missing/.test(r.reason), 'reason carries evaluator feedback');
106
- }
107
-
108
- // ---- Test 6: evaluator off -> oracle pass alone allows ----
109
- console.log('Test 6: evaluator disabled, oracle pass allows');
110
- { const d = newProject({ evaluator: 'off' }); dirs.push(d);
111
- writeFileSync(join(d, 'answer.txt'), 'DONE\n');
112
- const r = runGate(d);
113
- ok(!r.blocked, 'gate allows when evaluator off and oracle passes');
114
- }
115
-
116
- // ---- Test 7: max_iterations bound -> STALL hint once, then allows (never infinite) ----
117
- console.log('Test 7: max_iterations → stall hint once, then allows');
118
- { const d = newProject({ evaluator: 'off', max: 1 }); dirs.push(d); // oracle keeps failing (answer.txt=TODO)
119
- const r1 = runGate(d); // iteration -> 1, block (oracle fails)
120
- const r2 = runGate(d, true); // iteration -> 2 > max -> #0156 STALL: block ONCE with options
121
- const r3 = runGate(d, true); // already hinted -> give up, allow
122
- ok(r1.blocked, 'first failing attempt blocks');
123
- // #0156: exceeding the cap with the oracle still failing is a STALL — surface it loudly ONCE
124
- // (don't go silently dormant), presenting extend/resume <options>.
125
- ok(r2.blocked, '#0156: exceeding max_iterations blocks ONCE with a stall hint (not silent)');
126
- ok(/STALL/i.test(r2.reason) && /<options>/.test(r2.reason), '#0156: stall hint explains + presents <options>');
127
- ok(/loop .* --max/.test(r2.reason), '#0156: stall hint gives the resume+extend command');
128
- ok(readState(d).stall_hinted === true, '#0156: state records stall_hinted after the hint');
129
- ok(!r3.blocked, 'after the stall hint the gate allows stop (no infinite loop)');
130
- ok(readState(d).phase === 'gave_up' && readState(d).active === false, 'finally marked gave_up/inactive');
131
- }
132
-
133
- // ---- Test 8: inactive loop -> no-op allow ----
134
- console.log('Test 8: inactive loop is a no-op');
135
- { const d = newProject({ evaluator: 'off' }); dirs.push(d);
136
- const sp = join(d, '.svamp', SID, 'loop', 'loop-state.json');
137
- const s = JSON.parse(readFileSync(sp, 'utf8')); s.active = false; writeFileSync(sp, JSON.stringify(s));
138
- const r = runGate(d);
139
- ok(!r.blocked, 'gate is a safe no-op when loop inactive');
140
- }
141
-
142
- // ---- Test 9: updating LOOP.md after the verdict does NOT invalidate it ----
143
- // (LOOP.md is the loop's memory, not the work product — must be excluded from fp.)
144
- console.log('Test 9: LOOP.md edits do not make a verdict stale');
145
- { const d = newProject(); dirs.push(d);
146
- writeFileSync(join(d, 'answer.txt'), 'DONE\n');
147
- writeVerdict(d, { verdict: 'done', reason: 'complete', state_fp: fp(d) });
148
- // Agent updates its progress memory (session-scoped LOOP.md) AFTER getting the verdict:
149
- const md = join(d, '.svamp', SID, 'loop', 'LOOP.md');
150
- writeFileSync(md, readFileSync(md, 'utf8') + '\n- iter note: done\n');
151
- const r = runGate(d);
152
- ok(!r.blocked, 'gate still allows stop after LOOP.md was updated post-verdict');
153
- }
154
-
155
- // ---- Test 10: runtime budget gives up (allows stop) even if oracle fails ----
156
- console.log('Test 10: runtime budget backstop allows stop');
157
- { const d = newProject({ evaluator: 'off', max: 9999 }); dirs.push(d); // oracle keeps failing
158
- // set a tiny runtime budget and an old start time
159
- const cfgP = join(d, '.svamp', SID, 'loop', 'loop.config.json');
160
- const cfg = JSON.parse(readFileSync(cfgP, 'utf8')); cfg.budget = { max_runtime_sec: 1 }; writeFileSync(cfgP, JSON.stringify(cfg));
161
- const spP = join(d, '.svamp', SID, 'loop', 'loop-state.json');
162
- const sp = JSON.parse(readFileSync(spP, 'utf8')); sp.started_at = new Date(Date.now() - 5000).toISOString(); writeFileSync(spP, JSON.stringify(sp));
163
- const r = runGate(d); // #0156: first over-budget hit → stall hint (block once)
164
- const r2 = runGate(d); // already hinted → allow
165
- ok(r.blocked && /STALL/i.test(r.reason), 'runtime over-budget surfaces a stall hint first (not silent)');
166
- ok(!r2.blocked, 'gate allows stop once runtime budget is exceeded (after the stall hint)');
167
- ok(readState(d).gave_up_reason?.includes('runtime'), 'state records runtime give-up reason');
168
- }
169
-
170
- // ---- Test 11: per-iteration history trail is recorded ----
171
- console.log('Test 11: history.jsonl audit trail');
172
- { const d = newProject({ evaluator: 'off' }); dirs.push(d);
173
- const histPath = join(d, '.svamp', SID, 'loop', 'history.jsonl');
174
- runGate(d); // oracle fails -> continue entry
175
- let lines = readFileSync(histPath, 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
176
- ok(lines.length === 1 && lines[0].decision === 'continue', 'continue iteration recorded in history');
177
- writeFileSync(join(d, 'answer.txt'), 'DONE\n');
178
- runGate(d); // now passes -> done entry
179
- lines = readFileSync(histPath, 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
180
- ok(lines.length === 2 && lines[1].decision === 'done' && lines[1].oracle === true, 'done iteration recorded with oracle=true');
181
- }
182
-
183
- // ---- Test 12: token budget from transcript gives up ----
184
- console.log('Test 12: token budget backstop (from transcript)');
185
- { const d = newProject({ evaluator: 'off', max: 9999 }); dirs.push(d); // oracle keeps failing
186
- const cfgP = join(d, '.svamp', SID, 'loop', 'loop.config.json');
187
- const cfg = JSON.parse(readFileSync(cfgP, 'utf8')); cfg.budget = { max_tokens: 1000 }; writeFileSync(cfgP, JSON.stringify(cfg));
188
- const tp = join(d, 'transcript.jsonl');
189
- writeFileSync(tp, [
190
- JSON.stringify({ message: { usage: { input_tokens: 400, output_tokens: 300 } } }),
191
- JSON.stringify({ message: { usage: { input_tokens: 500, output_tokens: 200, cache_read_input_tokens: 50 } } }),
192
- ].join('\n')); // total = 1450 > 1000
193
- const r = runGate(d, false, tp); // #0156: first over-budget hit → stall hint
194
- const r2 = runGate(d, false, tp); // already hinted → allow
195
- ok(r.blocked && /STALL/i.test(r.reason), 'token over-budget surfaces a stall hint first (not silent)');
196
- ok(!r2.blocked, 'gate allows stop once token budget is exceeded (after the stall hint)');
197
- ok(readState(d).gave_up_reason?.includes('token'), 'state records token give-up reason');
198
- ok(readState(d).tokens_used === 1450, 'tokens summed from transcript');
199
- }
200
-
201
- // ---- Test 12b: #0156 rolling token-RATE budget (tokens in the last hour) ----
202
- console.log('Test 12b: rolling token-per-hour budget (last hour only)');
203
- { const d = newProject({ evaluator: 'off', max: 9999 }); dirs.push(d);
204
- const cfgP = join(d, '.svamp', SID, 'loop', 'loop.config.json');
205
- const cfg = JSON.parse(readFileSync(cfgP, 'utf8')); cfg.budget = { max_tokens_per_hour: 1000 }; writeFileSync(cfgP, JSON.stringify(cfg));
206
- const tp = join(d, 'transcript.jsonl');
207
- const recent = new Date().toISOString();
208
- const old = new Date(Date.now() - 2 * 3600 * 1000).toISOString(); // 2h ago — OUTSIDE the 1h window
209
- writeFileSync(tp, [
210
- JSON.stringify({ timestamp: old, message: { usage: { input_tokens: 5000, output_tokens: 5000 } } }), // old → ignored
211
- JSON.stringify({ timestamp: recent, message: { usage: { input_tokens: 800, output_tokens: 400 } } }), // 1200 in the last hour > 1000
212
- ].join('\n'));
213
- const r = runGate(d, false, tp); // over the hourly rate → stall hint
214
- const r2 = runGate(d, false, tp); // allow
215
- ok(r.blocked && /STALL/i.test(r.reason) && /token rate/i.test(r.reason), 'rolling token-rate over-budget → stall hint (counts only the last hour)');
216
- ok(!r2.blocked, 'allows stop after the stall hint');
217
- ok((readState(d).gave_up_reason || '').includes('token rate'), 'gave_up reason names the token rate');
218
- }
219
-
220
- // ---- Test 12c: #0156 null max + a budget RAISES the iteration ceiling (long-running) ----
221
- console.log('Test 12c: null max + budget is not clamped to 200');
222
- { const d = newProject({ evaluator: 'off', max: 1 }); dirs.push(d); // oracle keeps failing
223
- const cfgP = join(d, '.svamp', SID, 'loop', 'loop.config.json');
224
- const cfg = JSON.parse(readFileSync(cfgP, 'utf8')); cfg.max_iterations = null; cfg.budget = { max_tokens_per_hour: 1000000 }; writeFileSync(cfgP, JSON.stringify(cfg));
225
- const spP = join(d, '.svamp', SID, 'loop', 'loop-state.json');
226
- const sp = JSON.parse(readFileSync(spP, 'utf8')); sp.iteration = 300; writeFileSync(spP, JSON.stringify(sp)); // past the OLD 200 ceiling
227
- const r = runGate(d); // no transcript → 0 tokens in window; iteration 300 < raised ceiling → CONTINUE, not give up
228
- ok(r.blocked && !/STALL/i.test(r.reason), 'null max + budget: iter 300 does NOT give up at 200 — bounded by the token rate, runs long');
229
- }
230
-
231
- // ---- Test 13: null max_iterations must NOT block forever (hard fallback ceiling) ----
232
- console.log('Test 13: null max_iterations is bounded by hard ceiling');
233
- { const d = newProject({ evaluator: 'off' }); dirs.push(d); // oracle keeps failing
234
- const cfgP = join(d, '.svamp', SID, 'loop', 'loop.config.json');
235
- const cfg = JSON.parse(readFileSync(cfgP, 'utf8')); cfg.max_iterations = null; writeFileSync(cfgP, JSON.stringify(cfg));
236
- const spP = join(d, '.svamp', SID, 'loop', 'loop-state.json');
237
- const sp = JSON.parse(readFileSync(spP, 'utf8')); sp.iteration = 10000; writeFileSync(spP, JSON.stringify(sp)); // #0156: at the hard ceiling (raised 200→10000)
238
- const r = runGate(d); // #0156: stall hint first
239
- const r2 = runGate(d); // then allow
240
- ok(r.blocked && /STALL/i.test(r.reason), 'hard-ceiling hit surfaces a stall hint first');
241
- ok(!r2.blocked, 'gate allows stop at the hard fallback ceiling even with null max_iterations');
242
- }
243
-
244
- // ---- Test 14: non-git project fingerprint is content-sensitive (CRITICAL #1) ----
245
- console.log('Test 14: non-git fingerprint reflects content (no stale-verdict hole)');
246
- { const a = mkdtempSync(join(tmpdir(), 'nogit-')); dirs.push(a);
247
- writeFileSync(join(a, 'f.txt'), 'AAA');
248
- const fpA = execFileSync(node, [STATE_FP, a], { encoding: 'utf8' }).trim();
249
- writeFileSync(join(a, 'f.txt'), 'BBB different');
250
- const fpB = execFileSync(node, [STATE_FP, a], { encoding: 'utf8' }).trim();
251
- ok(fpA !== fpB, 'non-git fingerprint changes when file content changes');
252
- }
253
-
254
- // ---- Test 15: oracle that exits 0 with >1MB output is a PASS, not a failure (#3) ----
255
- console.log('Test 15: large passing-oracle output is not misread as failure');
256
- { const d = newProject({ evaluator: 'off' }); dirs.push(d);
257
- const cfgP = join(d, '.svamp', SID, 'loop', 'loop.config.json');
258
- const cfg = JSON.parse(readFileSync(cfgP, 'utf8'));
259
- cfg.oracle = { command: `node -e "console.log('x'.repeat(2000000)); process.exit(0)"` }; // 2MB, exit 0
260
- writeFileSync(cfgP, JSON.stringify(cfg));
261
- const r = runGate(d);
262
- ok(!r.blocked && readState(d).phase === 'done', 'oracle with 2MB output + exit 0 → done (not ENOBUFS failure)');
263
- }
264
-
265
- // ---- Test 16: huge finite max_iterations is CLAMPED to the hard ceiling (#1) ----
266
- console.log('Test 16: huge max_iterations is clamped');
267
- { const d = newProject({ evaluator: 'off' }); dirs.push(d); // oracle keeps failing
268
- const cfgP = join(d, '.svamp', SID, 'loop', 'loop.config.json');
269
- const cfg = JSON.parse(readFileSync(cfgP, 'utf8')); cfg.max_iterations = 1e9; writeFileSync(cfgP, JSON.stringify(cfg));
270
- const spP = join(d, '.svamp', SID, 'loop', 'loop-state.json');
271
- const sp = JSON.parse(readFileSync(spP, 'utf8')); sp.iteration = 10001; writeFileSync(spP, JSON.stringify(sp));
272
- runGate(d); // #0156: stall hint first
273
- const r = runGate(d); // then allow
274
- ok(!r.blocked && /max_iterations \(10000\)/.test(readState(d).gave_up_reason || ''), 'max_iterations 1e9 clamped to 10000 → gives up');
275
- }
276
-
277
- // ---- Test 17: non-numeric iteration is coerced (cap stays alive) (#2) ----
278
- console.log('Test 17: corrupt non-numeric iteration is coerced');
279
- { const d = newProject({ evaluator: 'off' }); dirs.push(d);
280
- const spP = join(d, '.svamp', SID, 'loop', 'loop-state.json');
281
- const sp = JSON.parse(readFileSync(spP, 'utf8')); sp.iteration = 'not-a-number'; writeFileSync(spP, JSON.stringify(sp));
282
- runGate(d); // oracle fails -> blocks, iteration coerced to 0+1
283
- ok(readState(d).iteration === 1, 'non-numeric iteration coerced to a number (cap not disabled)');
284
- }
285
-
286
- // ---- Test 18: non-git symlink content is reflected in the fingerprint (#5) ----
287
- console.log('Test 18: non-git symlink target changes the fingerprint');
288
- { const { symlinkSync, unlinkSync, writeFileSync: wf } = await import('node:fs');
289
- const d = mkdtempSync(join(tmpdir(), 'nogit-link-')); dirs.push(d);
290
- wf(join(d, 'targetA'), 'AAA'); wf(join(d, 'targetB'), 'BBB');
291
- symlinkSync('targetA', join(d, 'link'));
292
- const fpA = execFileSync(node, [STATE_FP, d], { encoding: 'utf8' }).trim();
293
- unlinkSync(join(d, 'link')); symlinkSync('targetB', join(d, 'link'));
294
- const fpB = execFileSync(node, [STATE_FP, d], { encoding: 'utf8' }).trim();
295
- ok(fpA !== fpB, 'changing a symlink target changes the non-git fingerprint (no stale-verdict hole)');
296
- }
297
-
298
- // ---- Test 19: two sessions in ONE dir get isolated loops; a non-loop sibling no-ops ----
299
- console.log('Test 19: session-scoped isolation in a shared working dir');
300
- { const dir = mkdtempSync(join(tmpdir(), 'loopshare-')); dirs.push(dir);
301
- git(dir, ['init', '-q']); git(dir, ['config', 'user.email', 't@t']); git(dir, ['config', 'user.name', 't']);
302
- writeFileSync(join(dir, 'answer.txt'), 'TODO\n');
303
- git(dir, ['add', '-A']); git(dir, ['commit', '-qm', 'init']);
304
- const A = 'sess-aaaa', B = 'sess-bbbb';
305
- // A failing oracle so the gate blocks (and increments) rather than completing instantly.
306
- const O = 'grep -q DONE answer.txt';
307
- execFileSync(node, [INIT, dir, '--session', A, '--task', 'task A', '--oracle', O, '--evaluator', 'off', '--max', '20'], { encoding: 'utf8' });
308
- execFileSync(node, [INIT, dir, '--session', B, '--task', 'task B', '--oracle', O, '--evaluator', 'off', '--max', '20'], { encoding: 'utf8' });
309
- const runAs = (sid) => {
310
- // The SHARED settings.json now points at B's gate copy; run THAT script as each session.
311
- const gate = join(dir, '.svamp', B, 'loop', 'bin', 'stop-gate.mjs');
312
- let out = ''; try { out = execFileSync(node, [gate], { input: '{}', encoding: 'utf8', env: { ...process.env, SVAMP_SESSION_ID: sid, CLAUDE_PROJECT_DIR: dir } }); } catch (e) { out = e.stdout || ''; }
313
- let p = null; try { p = JSON.parse(out); } catch {}
314
- return { blocked: p?.decision === 'block', raw: out };
315
- };
316
- const stateOf = (sid) => JSON.parse(readFileSync(join(dir, '.svamp', sid, 'loop', 'loop-state.json'), 'utf8'));
317
- runAs(A); // oracle-less + evaluator-off: not done -> blocks, increments A only
318
- ok(stateOf(A).iteration === 1, 'session A loop advanced to iter 1');
319
- ok((stateOf(B).iteration ?? 0) === 0, 'session B loop untouched (no cross-talk)');
320
- const sib = runAs('sess-cccc-noloop'); // a sibling with no loop
321
- ok(!sib.blocked && sib.raw === '', 'non-loop sibling session no-ops (allows stop)');
322
- }
323
-
324
- // ---- Test 20: a re-armed loop must NOT inherit the previous loop's verdict (no false-close) ----
325
- // Repro of the iter-0 false-close: loop A finishes and leaves a {done, state_fp} verdict;
326
- // a NEW loop B re-uses the same session dir; state-fp excludes LOOP.md + .svamp/, so an
327
- // unchanged tree keeps the same fp → without a fix, B inherits A's done verdict and closes
328
- // at iteration 0 having done nothing. loop-init must clear the stale verdict on re-arm.
329
- console.log('Test 20: re-armed loop does not inherit the prior loop\'s verdict');
330
- { const d = newProject({ evaluator: 'on' }); dirs.push(d);
331
- const verdictPath = join(d, '.svamp', SID, 'loop', 'evaluator-verdict.json');
332
- // Loop A completes legitimately (oracle passes + fresh done verdict).
333
- writeFileSync(join(d, 'answer.txt'), 'DONE\n');
334
- writeVerdict(d, { verdict: 'done', reason: 'A complete', state_fp: fp(d) });
335
- ok(existsSync(verdictPath), 'precondition: loop A left a verdict on disk');
336
- // Re-arm a DIFFERENT loop B in the SAME session, WITHOUT touching the work tree.
337
- execFileSync(node, [INIT, d, '--session', SID, '--task', 'task B (different)',
338
- '--oracle', 'grep -q DONE answer.txt', '--evaluator', 'on', '--max', '20'], { encoding: 'utf8' });
339
- ok(!existsSync(verdictPath), 're-init cleared the stale evaluator verdict');
340
- // A re-armed loop must adopt the NEW task, not inherit the previous loop's LOOP.md.
341
- const reMd = readFileSync(join(d, '.svamp', SID, 'loop', 'LOOP.md'), 'utf8');
342
- ok(reMd.includes('task B (different)') && !reMd.includes('make answer.txt contain DONE'),
343
- 're-arm overwrote LOOP.md with the new task (no stale task carried over)');
344
- // B must BLOCK at iter 0 (needs its OWN verdict), not false-close on A's.
345
- const r = runGate(d);
346
- ok(r.blocked && /evaluator/i.test(r.reason), 'loop B blocks for a fresh verdict (no iter-0 false-close)');
347
- ok(readState(d).phase !== 'done', 'loop B did not close as done at iteration 0');
348
- }
349
-
350
- // ---- Test 21: inject-loop self-heals a clobbered Stop gate (#0072) -------
351
- console.log('Test 21: inject-loop re-installs a dropped Stop gate (shared-tree self-heal)');
352
- { const INJECT = resolve(HERE, '..', 'bin', 'inject-loop.mjs');
353
- const d = newProject(); dirs.push(d);
354
- const settingsPath = join(d, '.claude', 'settings.json');
355
- const stopGate = join(d, '.svamp', SID, 'loop', 'bin', 'stop-gate.mjs');
356
- const runInject = () => execFileSync(node, [INJECT], {
357
- input: JSON.stringify({ hook_event_name: 'SessionStart', cwd: d }),
358
- encoding: 'utf8', env: { ...process.env, SVAMP_SESSION_ID: SID, CLAUDE_PROJECT_DIR: d },
359
- });
360
- const hasStop = () => { const s = JSON.parse(readFileSync(settingsPath, 'utf8'));
361
- return Array.isArray(s.hooks?.Stop) && s.hooks.Stop.some((g) => Array.isArray(g?.hooks)
362
- && g.hooks.some((h) => typeof h?.command === 'string' && h.command.includes(stopGate))); };
363
- ok(hasStop(), 'precondition: loop-init installed the Stop gate');
364
- // Simulate a shared-tree clobber: another tool rewrites settings.json, dropping the Stop hook.
365
- const clob = JSON.parse(readFileSync(settingsPath, 'utf8'));
366
- delete clob.hooks.Stop; clob.permissions = { allow: ['Bash(ls:*)'] };
367
- writeFileSync(settingsPath, JSON.stringify(clob, null, 2));
368
- ok(!hasStop(), 'precondition: Stop gate removed by the clobber');
369
- runInject();
370
- ok(hasStop(), 'inject-loop re-installed the missing Stop gate');
371
- ok(JSON.parse(readFileSync(settingsPath, 'utf8')).permissions?.allow?.includes('Bash(ls:*)'),
372
- 'self-heal preserved unrelated settings (merge, not clobber)');
373
- runInject();
374
- ok(JSON.parse(readFileSync(settingsPath, 'utf8')).hooks.Stop.length === 1,
375
- 'self-heal is idempotent when the gate is intact');
376
- // Inactive loop (cancelled) must NOT be resurrected by inject-loop.
377
- const stateP = join(d, '.svamp', SID, 'loop', 'loop-state.json');
378
- const st = JSON.parse(readFileSync(stateP, 'utf8')); st.active = false; writeFileSync(stateP, JSON.stringify(st));
379
- const clob2 = JSON.parse(readFileSync(settingsPath, 'utf8')); delete clob2.hooks.Stop; writeFileSync(settingsPath, JSON.stringify(clob2, null, 2));
380
- runInject();
381
- ok(!hasStop(), 'inactive loop: inject-loop does not re-install (respects cancel)');
382
- }
383
-
384
- // ---- Test 22: block reason NAMES the pending issue ids (#0103 PART 1) ----
385
- console.log('Test 22: block reason names the active issue ids');
386
- { const dir = mkdtempSync(join(tmpdir(), 'loopgate-')); dirs.push(dir);
387
- git(dir, ['init', '-q']); git(dir, ['config', 'user.email', 't@t']); git(dir, ['config', 'user.name', 't']);
388
- writeFileSync(join(dir, 'answer.txt'), 'x'); git(dir, ['add', '-A']); git(dir, ['commit', '-qm', 'init']);
389
- // An oracle that fails and prints a pending list (like `svamp issue pending`).
390
- execFileSync(node, [INIT, dir, '--session', SID, '--task', 't',
391
- '--oracle', 'bash -c \'echo "2 pending: #0001 #0002"; exit 1\'', '--evaluator', 'off', '--max', '20'], { encoding: 'utf8' });
392
- const r = runGate(dir);
393
- ok(r.blocked && r.reason.includes('#0001') && r.reason.includes('#0002') && /Finish the active issues/i.test(r.reason),
394
- 'block reason names the pending issue ids (#0001, #0002)');
395
- }
396
-
397
- // ---- Test 23: inbox guard blocks stop on an UNHANDLED message, bounded (#0103 PART 2) ----
398
- console.log('Test 23: inbox guard blocks an otherwise-complete loop, and is bounded');
399
- { const d = newProject({ evaluator: 'on' }); dirs.push(d);
400
- writeFileSync(join(d, 'answer.txt'), 'DONE\n'); // oracle passes
401
- writeVerdict(d, { verdict: 'done', reason: 'ok', state_fp: fp(d) });
402
- const inboxP = join(d, '.svamp', SID, 'inbox.json');
403
- writeFileSync(inboxP, JSON.stringify([{ messageId: 'm1', from: 'agent:x', read: false, handled: false }]));
404
- const r1 = runGate(d);
405
- ok(r1.blocked && /unhandled inbox/i.test(r1.reason), 'an unhandled inbox message blocks the otherwise-complete loop');
406
- ok(readState(d).phase !== 'done', 'did not close while an inbox message is unhandled');
407
- // Bounded: inbox_blocks is now 1 → a second check ALLOWS stop even though the message is still pending.
408
- const r2 = runGate(d);
409
- ok(!r2.blocked && readState(d).phase === 'done', 'inbox guard is bounded — second time allows stop (no flood-trap)');
410
- }
411
-
412
- // ---- Test 24: inbox guard ALLOWS stop when inbox is empty / all-handled (#0103 PART 2) ----
413
- console.log('Test 24: inbox guard allows stop when nothing is pending');
414
- { const d = newProject({ evaluator: 'on' }); dirs.push(d);
415
- writeFileSync(join(d, 'answer.txt'), 'DONE\n');
416
- writeVerdict(d, { verdict: 'done', reason: 'ok', state_fp: fp(d) });
417
- writeFileSync(join(d, '.svamp', SID, 'inbox.json'), JSON.stringify([{ messageId: 'a', read: true }, { messageId: 'b', handled: true }]));
418
- const r = runGate(d);
419
- ok(!r.blocked && readState(d).phase === 'done', 'all-handled inbox allows the loop to complete');
420
- }
421
-
422
- // ---- Test 25: NON-URGENT inbox hint at settle — blocks ONCE, then allows (de-duped) (#0146) ----
423
- console.log('Test 25: non-urgent inbox hint blocks once at settle, then allows (de-duped)');
424
- { const d = newProject({ evaluator: 'on' }); dirs.push(d);
425
- writeFileSync(join(d, 'answer.txt'), 'DONE\n'); // oracle passes
426
- writeVerdict(d, { verdict: 'done', reason: 'ok', state_fp: fp(d) });
427
- // Disable the #0103 urgent/unhandled guard so we exercise the non-urgent hint in isolation
428
- // (otherwise the #0103 guard would block first on the same genuinely-pending message).
429
- const cfgP = join(d, '.svamp', SID, 'loop', 'loop.config.json');
430
- writeFileSync(cfgP, JSON.stringify({ ...JSON.parse(readFileSync(cfgP, 'utf8')), inbox_guard: false }));
431
- // A genuinely-unseen (read=false, handled=false), non-urgent, non-awaiting-reply message.
432
- writeFileSync(join(d, '.svamp', SID, 'inbox.json'), JSON.stringify([
433
- { messageId: 'n1', from: 'agent:peer', subject: 'fyi', read: false, handled: false, urgency: 'normal' },
434
- ]));
435
- const r1 = runGate(d);
436
- ok(r1.blocked && /unread inbox message/i.test(r1.reason), 'non-urgent unread message hints (blocks once) at the settle point');
437
- ok(/fyi/.test(r1.reason), 'hint names the subject');
438
- const hinted = JSON.parse(readFileSync(join(d, '.svamp', SID, 'loop', 'hinted-inbox.json'), 'utf8'));
439
- ok(hinted.includes('n1'), 'hinted id recorded for de-dupe');
440
- // Second settle: same message id already hinted → allows the loop to end (non-blocking thereafter).
441
- const r2 = runGate(d);
442
- ok(!r2.blocked && readState(d).phase === 'done', 'already-hinted message does not re-block — loop completes');
443
- }
444
-
445
- // ---- Test 26: urgent + awaiting-reply messages are NOT hinted by the non-urgent path (#0146) ----
446
- console.log('Test 26: urgent / awaiting-reply messages skip the non-urgent hint');
447
- { const d = newProject({ evaluator: 'on' }); dirs.push(d);
448
- writeFileSync(join(d, 'answer.txt'), 'DONE\n');
449
- writeVerdict(d, { verdict: 'done', reason: 'ok', state_fp: fp(d) });
450
- // All handled (no #0103 block); one urgent, one awaiting-reply, one already-read → none qualify.
451
- writeFileSync(join(d, '.svamp', SID, 'inbox.json'), JSON.stringify([
452
- { messageId: 'u1', read: false, handled: true, urgency: 'urgent' },
453
- { messageId: 'a1', read: false, handled: true, channelId: 'c', correlationId: 'x' },
454
- { messageId: 'r1', read: true, handled: true, urgency: 'normal' },
455
- ]));
456
- const r = runGate(d);
457
- ok(!r.blocked && readState(d).phase === 'done', 'no qualifying non-urgent unread message → loop completes (no noise)');
458
- }
459
-
460
- // ---- Test 27: #0166 hooks-only pre-install — dormant gate, merges (doesn't clobber) user hooks ----
461
- console.log('Test 27: --hooks-only installs a DORMANT gate + merges user hooks (seamless start/stop)');
462
- { const d = mkdtempSync(join(tmpdir(), 'hooksonly-')); dirs.push(d);
463
- // Pre-existing USER hooks that must survive.
464
- const sp = join(d, '.claude', 'settings.json');
465
- execFileSync('bash', ['-lc', `mkdir -p "${join(d, '.claude')}"`]);
466
- writeFileSync(sp, JSON.stringify({ hooks: {
467
- Stop: [{ hooks: [{ type: 'command', command: 'echo user-stop' }] }],
468
- PreToolUse: [{ hooks: [{ type: 'command', command: 'echo user-pretool' }] }],
469
- } }));
470
- execFileSync(node, [INIT, d, '--session', SID, '--hooks-only'], { encoding: 'utf8' });
471
- execFileSync(node, [INIT, d, '--session', SID, '--hooks-only'], { encoding: 'utf8' }); // 2x → no dup
472
- const s = JSON.parse(readFileSync(sp, 'utf8'));
473
- const cmds = (ev) => (s.hooks[ev] || []).flatMap((g) => (g.hooks || []).map((h) => h.command));
474
- ok(cmds('PreToolUse').some((c) => c.includes('user-pretool')), 'user PreToolUse hook preserved');
475
- ok(cmds('Stop').some((c) => c.includes('user-stop')), 'user Stop hook preserved (merge, not clobber)');
476
- ok(cmds('Stop').filter((c) => c.includes('stop-gate.mjs')).length === 1, 'gate Stop hook present exactly once (no dup on re-run)');
477
- ok(!existsSync(join(d, '.svamp', SID, 'loop', 'loop.config.json')), 'hooks-only does NOT create a loop (no config)');
478
- ok(!existsSync(join(d, '.svamp', SID, 'loop', 'loop-state.json')), 'hooks-only does NOT create loop-state');
479
- ok(existsSync(join(d, '.svamp', SID, 'loop', 'bin', 'stop-gate.mjs')), 'gate scripts copied');
480
- // The dormant gate must ALLOW stop (no active loop) — else every session could never end a turn.
481
- const r = runGate(d);
482
- ok(!r.blocked, 'dormant gate (no loop) allows the turn to end');
483
- }
484
-
485
- console.log(`\n${fail === 0 ? '✅' : '❌'} ${pass} passed, ${fail} failed`);
486
- process.exit(fail === 0 ? 0 : 1);
487
- } finally {
488
- for (const d of dirs) { try { rmSync(d, { recursive: true, force: true }); } catch {} }
489
- }
@@ -1,54 +0,0 @@
1
- #!/usr/bin/env node
2
- // Deterministic tests for routine-core (cron, next-fire, templating, validation).
3
- import { parseCron, cronMatches, nextFire, renderTemplate, validateRoutine, inZone } from '../bin/routine-core.mjs';
4
-
5
- let pass = 0, fail = 0;
6
- function ok(c, m) { if (c) { pass++; console.log(` ✓ ${m}`); } else { fail++; console.log(` ✗ ${m}`); } }
7
- function throws(fn, m) { try { fn(); fail++; console.log(` ✗ ${m} (did not throw)`); } catch { pass++; console.log(` ✓ ${m}`); } }
8
-
9
- // Local-time Date helper (cron matches on local time).
10
- const D = (y, mo, d, h, mi) => new Date(y, mo - 1, d, h, mi, 0, 0);
11
-
12
- console.log('cron parsing');
13
- ok(parseCron('*/5 * * * *').minute.has(5), 'parses */5 minutes');
14
- throws(() => parseCron('* * * *'), 'rejects 4-field cron');
15
- throws(() => parseCron('99 * * * *'), 'rejects out-of-range value');
16
-
17
- console.log('cron matching');
18
- ok(cronMatches('*/5 * * * *', D(2026, 6, 10, 9, 5)), '*/5 matches minute 5');
19
- ok(!cronMatches('*/5 * * * *', D(2026, 6, 10, 9, 3)), '*/5 does not match minute 3');
20
- ok(cronMatches('0 9 * * *', D(2026, 6, 10, 9, 0)), '0 9 matches 09:00');
21
- ok(!cronMatches('0 9 * * *', D(2026, 6, 10, 10, 0)), '0 9 does not match 10:00');
22
- // 2026-06-10 is a Wednesday (dow 3); 2026-06-13 is Saturday (dow 6)
23
- ok(cronMatches('0 9 * * 1-5', D(2026, 6, 10, 9, 0)), 'weekday range matches Wednesday');
24
- ok(!cronMatches('0 9 * * 1-5', D(2026, 6, 13, 9, 0)), 'weekday range excludes Saturday');
25
- // dom+dow both restricted => OR semantics (match if EITHER matches)
26
- ok(cronMatches('0 0 10 * 5', D(2026, 6, 10, 0, 0)), 'dom OR dow: dom=10 matches even though dow!=Fri');
27
- ok(!cronMatches('0 0 13 * 5', D(2026, 6, 10, 0, 0)), 'dom OR dow: neither dom=13 nor dow=Fri matches');
28
-
29
- console.log('next-fire');
30
- { const n = nextFire('*/5 * * * *', D(2026, 6, 10, 9, 2));
31
- ok(n.getHours() === 9 && n.getMinutes() === 5, `next */5 after 09:02 is 09:05 (got ${n.getHours()}:${n.getMinutes()})`); }
32
- { const n = nextFire('0 0 * * *', D(2026, 6, 10, 9, 2));
33
- ok(n.getDate() === 11 && n.getHours() === 0 && n.getMinutes() === 0, 'next daily-midnight rolls to next day 00:00'); }
34
-
35
- console.log('timezone');
36
- { const utc = new Date('2026-06-10T12:00:00Z'); // June → EDT (UTC-4) → 08:00 New York
37
- ok(inZone(utc, 'America/New_York').getHours() === 8, 'inZone converts UTC to tz wall-clock');
38
- ok(cronMatches('0 8 * * *', inZone(utc, 'America/New_York')), 'cron 0 8 matches in America/New_York'); }
39
-
40
- console.log('templating');
41
- ok(renderTemplate('PR #${body.pr} by ${body.user}', { body: { pr: 42, user: 'wei' } }) === 'PR #42 by wei', 'templates body fields');
42
- ok(renderTemplate('q=${query.q}', { query: { q: 'hi' } }) === 'q=hi', 'templates query fields');
43
- ok(renderTemplate('missing=[${body.nope}]', { body: {} }) === 'missing=[]', 'missing var -> empty');
44
- ok(renderTemplate('obj=${body.o}', { body: { o: { a: 1 } } }) === 'obj={"a":1}', 'object var -> JSON');
45
-
46
- console.log('validation');
47
- const good = { session_id: 's1', name: 'nightly', trigger: { type: 'schedule', cron: '0 2 * * *', missed: 'skip' }, action: { kind: 'message', template: 'go' }, overlap: 'queue' };
48
- ok(validateRoutine(good).length === 0, 'valid routine passes');
49
- ok(validateRoutine({ name: 'x', trigger: { type: 'schedule', cron: 'bad' }, action: { kind: 'message' } }).length >= 2, 'invalid routine collects errors');
50
- ok(validateRoutine({ session_id: 's', name: 'w', trigger: { type: 'webhook', key: 'k' }, action: { kind: 'loop', task_template: 'do ${body.x}' } }).length === 0, 'webhook+loop routine valid');
51
- ok(validateRoutine({ session_id: 's', name: 'w', trigger: { type: 'webhook', public: true }, action: { kind: 'loop', task_template: 'x' } }).some((e) => /public/.test(e)), 'public webhook + loop action rejected (unauth task injection)');
52
-
53
- console.log(`\n${fail === 0 ? '✅' : '❌'} ${pass} passed, ${fail} failed`);
54
- process.exit(fail === 0 ? 0 : 1);
@@ -1,63 +0,0 @@
1
- var name = "svamp-cli";
2
- var version = "0.2.223-canary.2";
3
- var description = "Svamp CLI — AI workspace daemon on Hypha Cloud";
4
- var author = "Amun AI AB";
5
- var license = "SEE LICENSE IN LICENSE";
6
- var type = "module";
7
- var bin = {
8
- svamp: "./bin/svamp.mjs"
9
- };
10
- var files = [
11
- "dist",
12
- "bin"
13
- ];
14
- var main = "./dist/index.mjs";
15
- var exports$1 = {
16
- ".": "./dist/index.mjs",
17
- "./cli": "./dist/cli.mjs"
18
- };
19
- var scripts = {
20
- build: "rm -rf dist bin/skills bin/commands && mkdir -p bin/skills && cp -r ../../skills/artifact bin/skills/artifact && cp -r ../../skills/loop bin/skills/loop && cp -r ../../skills/crew bin/skills/crew && cp -r ../../commands bin/commands && tsc --noEmit && pkgroll",
21
- typecheck: "tsc --noEmit",
22
- test: "npx tsx test/test-context-window.mjs && npx tsx test/test-ratelimit-retry.mjs && npx tsx test/test-instance-config.mjs && npx tsx test/test-authorize.mjs && npx tsx test/test-normalize-allowed-user.mjs && npx tsx test/test-share-url.mjs && npx tsx test/test-update-sharing-normalization.mjs && npx tsx test/test-sharing-notify-sync.mjs && npx tsx test/test-staged-homes-sweep.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-isolation-decision.mjs && npx tsx test/test-loop-activation.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-claude-auth.mjs && npx tsx test/test-btw-proxy-env.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-inbox-guard.mjs && npx tsx test/test-auto-topic.mjs && npx tsx test/test-project-info.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-backend.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-session-send-query.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs && npx tsx test/test-supervisor-lock.mjs && node test/test-supervisor-restart.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-session-consolidation.mjs && npx tsx test/test-inbox-store.mjs && npx tsx test/test-inbox.mjs && npx tsx test/test-inbox-cross-machine.mjs && npx tsx test/test-issue-store.mjs && npx tsx test/test-issue-close-gate.mjs && npx tsx test/test-issue-rpc.mjs && npx tsx test/test-issue-pause.mjs && npx tsx test/test-workflow-store.mjs && npx tsx test/test-workflow-rpc.mjs && npx tsx test/test-workflow-scheduler.mjs && npx tsx test/test-workflow-runs.mjs && npx tsx test/test-serve-link-subdomain.mjs && npx tsx test/test-short-id.mjs && npx tsx test/test-transcript-edit.mjs && npx tsx test/test-edit-history.mjs && npx tsx test/test-friendly-name.mjs && npx tsx test/test-session-rpc-dispatch.mjs && npx tsx test/test-sandbox-cli.mjs && npx tsx test/test-serve-manager.mjs && npx tsx test/test-serve-stability.mjs && npx tsx test/test-frpc-e2e.mjs --unit-only && npx tsx test/test-frpc-status.mjs && node test/pinnedClaudeCode.test.mjs && node test/fleet.test.mjs && npx tsx test/test-session-file.mjs && npx tsx test/test-channel-rpc.mjs && npx tsx test/test-wise-agent.mjs && npx tsx test/test-channel-agent.mjs && npx tsx test/test-channels-service.mjs && npx tsx test/test-hotreload-seam.mjs && npx tsx test/test-hot-reload.mjs && npx tsx test/test-hot-reload-live.mjs && npx tsx test/test-session-core.mjs && npx tsx test/test-channel-async-reply.mjs && npx tsx test/test-channel-binding.mjs && npx tsx test/test-channel-identity.mjs && npx tsx test/test-shared-session-identity.mjs && npx tsx test/test-wise-agent-auth.mjs && npx tsx test/test-channel-http.mjs && npx tsx test/test-channel-upload.mjs && npx tsx test/test-wise-voice.mjs && npx tsx test/test-wise-headless.mjs && npx tsx test/test-wise-machine.mjs && npx tsx test/test-crew-merge.mjs && npx tsx test/test-crew-verdict-routing.mjs && npx tsx test/test-crew-standalone.mjs && npx tsx test/test-session-links.mjs && npx tsx test/test-issue-loop-pause-guard.mjs",
23
- "test:hypha": "node --no-warnings test/test-hypha-service.mjs",
24
- dev: "tsx src/cli.ts",
25
- "dev:daemon": "tsx src/cli.ts daemon start-sync",
26
- "test:e2e": "node --no-warnings test/e2e-session-tests.mjs",
27
- "test:frpc": "npx tsx test/test-frpc-e2e.mjs"
28
- };
29
- var dependencies = {
30
- "@agentclientprotocol/sdk": "^0.14.1",
31
- "@modelcontextprotocol/sdk": "^1.25.3",
32
- "hypha-rpc": "0.21.42",
33
- "node-pty": "1.2.0-beta.11",
34
- ws: "^8.18.0",
35
- yaml: "^2.8.2",
36
- zod: "^3.24.4"
37
- };
38
- var devDependencies = {
39
- "@types/node": ">=20",
40
- "@types/ws": "^8.5.14",
41
- pkgroll: "^2.14.2",
42
- tsx: "^4.20.6",
43
- typescript: "5.9.3"
44
- };
45
- var packageManager = "yarn@1.22.22";
46
- var _package = {
47
- name: name,
48
- version: version,
49
- description: description,
50
- author: author,
51
- license: license,
52
- type: type,
53
- bin: bin,
54
- files: files,
55
- main: main,
56
- exports: exports$1,
57
- scripts: scripts,
58
- dependencies: dependencies,
59
- devDependencies: devDependencies,
60
- packageManager: packageManager
61
- };
62
-
63
- export { author, bin, _package as default, dependencies, description, devDependencies, exports$1 as exports, files, license, main, name, packageManager, scripts, type, version };