svamp-cli 0.2.223-canary.2 → 0.2.223

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-Be_5WSKW.mjs} +5 -5
  2. package/dist/{auth-FvyDIAch.mjs → auth-CKjLWEcA.mjs} +1 -1
  3. package/dist/cli.mjs +61 -61
  4. package/dist/{commands-BuecUAk8.mjs → commands-B_XZmurW.mjs} +1 -1
  5. package/dist/{commands-Cmjb9Je1.mjs → commands-CK3ZbA0p.mjs} +6 -6
  6. package/dist/{commands-BiIkCdGB.mjs → commands-ClmIatXG.mjs} +1 -1
  7. package/dist/{commands-Dm3EQICq.mjs → commands-DGD5uSeq.mjs} +2 -2
  8. package/dist/{commands-_RXnsA1v.mjs → commands-DeBy9HEU.mjs} +1 -1
  9. package/dist/{commands-BhFr0kUL.mjs → commands-Dw2YesDl.mjs} +1 -1
  10. package/dist/{commands-nlra_-D0.mjs → commands-DzwKn57T.mjs} +2 -2
  11. package/dist/{fleet-Bi3KIev7.mjs → fleet-CPm4Bm3m.mjs} +1 -1
  12. package/dist/{frpc-BpQyeWvI.mjs → frpc-C2eRCDNy.mjs} +1 -1
  13. package/dist/{headlessCli-Be0bYOUB.mjs → headlessCli-Bt3ZIoTG.mjs} +2 -2
  14. package/dist/index.mjs +1 -1
  15. package/dist/package-BL4Hd940.mjs +63 -0
  16. package/dist/{rpc-WS0yDVqB.mjs → rpc-CekroPL2.mjs} +1 -1
  17. package/dist/{rpc-CRZWx5YY.mjs → rpc-CrjSXvID.mjs} +1 -1
  18. package/dist/{run-DVdqRhtp.mjs → run-BqvXq2yD.mjs} +10 -24
  19. package/dist/{run-8_mnLg6R.mjs → run-DUquoPkl.mjs} +1 -1
  20. package/dist/{scheduler-Dx-QrQfP.mjs → scheduler-agjaeQjl.mjs} +1 -1
  21. package/dist/{serveCommands-CI2P6buK.mjs → serveCommands-Bc4ZhEqi.mjs} +5 -5
  22. package/dist/{serveManager-B-keYGE9.mjs → serveManager-Ddqi1kbZ.mjs} +2 -2
  23. package/dist/{sideband-BdYb3ezu.mjs → sideband-lLT0D1PF.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,51 +0,0 @@
1
- #!/usr/bin/env node
2
- // loop-status.mjs — show a loop's current state + per-iteration history timeline.
3
- // Usage: node loop-status.mjs [project-dir] [--session <id>] [--json] [-n <count>]
4
- import { readFileSync, existsSync, readdirSync } from 'node:fs';
5
- import { join, resolve } from 'node:path';
6
-
7
- const args = process.argv.slice(2);
8
- const json = args.includes('--json');
9
- const nIdx = args.indexOf('-n');
10
- const limit = nIdx !== -1 ? Number(args[nIdx + 1]) : 20;
11
- const sIdx = args.indexOf('--session');
12
- const sessionId = sIdx !== -1 ? args[sIdx + 1] : (process.env.SVAMP_SESSION_ID || null);
13
- const dir = resolve(args.find((a) => !a.startsWith('-') && a !== String(limit) && a !== sessionId) || process.cwd());
14
-
15
- // Find the loop home: explicit session → .svamp/<sid>/loop; else any session-scoped
16
- // loop that has state; else .svamp/loop; else legacy .claude/loop.
17
- function pickLoopDir(root, sid) {
18
- const cands = [];
19
- if (sid) cands.push(join(root, '.svamp', sid, 'loop'));
20
- try { for (const e of readdirSync(join(root, '.svamp'), { withFileTypes: true })) if (e.isDirectory() && e.name !== sid) cands.push(join(root, '.svamp', e.name, 'loop')); } catch {}
21
- cands.push(join(root, '.svamp', 'loop'), join(root, '.claude', 'loop'));
22
- for (const c of cands) if (existsSync(join(c, 'loop-state.json'))) return c;
23
- return sid ? join(root, '.svamp', sid, 'loop') : cands[cands.length - 1];
24
- }
25
- const LOOP_DIR = pickLoopDir(dir, sessionId);
26
-
27
- const readJSON = (p, f) => { try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return f; } };
28
- const state = readJSON(join(LOOP_DIR, 'loop.config.json'), null)
29
- ? readJSON(join(LOOP_DIR, 'loop-state.json'), {})
30
- : null;
31
-
32
- let history = [];
33
- try {
34
- history = readFileSync(join(LOOP_DIR, 'history.jsonl'), 'utf8')
35
- .split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
36
- } catch {}
37
-
38
- if (json) {
39
- console.log(JSON.stringify({ state, history: history.slice(-limit) }, null, 2));
40
- } else if (!state) {
41
- console.log('No loop configured in', dir);
42
- } else {
43
- const icon = { done: '✅', gave_up: '🛑', continue: '🔄', running: '🔄' };
44
- console.log(`Loop in ${dir}`);
45
- console.log(` phase: ${state.phase || '?'} | iteration: ${state.iteration ?? 0}${state.gave_up_reason ? ` | gave up: ${state.gave_up_reason}` : ''}`);
46
- if (state.last_oracle) console.log(` oracle: ${String(state.last_oracle).split('\n')[0]}`);
47
- console.log(` history (last ${Math.min(limit, history.length)} of ${history.length}):`);
48
- for (const h of history.slice(-limit)) {
49
- console.log(` ${icon[h.decision] || '·'} iter ${h.iteration} [${h.decision}] ${h.ts}${h.reason ? ' — ' + h.reason : ''}`);
50
- }
51
- }
@@ -1,30 +0,0 @@
1
- #!/usr/bin/env node
2
- // precompact.mjs — Claude Code `PreCompact` hook. Records a compaction event in
3
- // the loop's history timeline (so the monitor shows context resets) and reminds
4
- // the agent that LOOP.md is the durable handoff across compaction.
5
- import { readFileSync, appendFileSync } from 'node:fs';
6
- import { dirname, join, resolve } from 'node:path';
7
- import { fileURLToPath } from 'node:url';
8
-
9
- const HERE = dirname(fileURLToPath(import.meta.url));
10
- // Resolve the loop home from the per-process env (shared-settings safe); fallback to
11
- // the parent of this copied bin/ dir.
12
- const SID = process.env.SVAMP_SESSION_ID || null;
13
- const PROJECT_ENV = process.env.CLAUDE_PROJECT_DIR || null;
14
- const LOOP_DIR = (SID && PROJECT_ENV) ? join(PROJECT_ENV, '.svamp', SID, 'loop') : resolve(HERE, '..');
15
-
16
- let active = false;
17
- try { active = JSON.parse(readFileSync(join(LOOP_DIR, 'loop-state.json'), 'utf8')).active !== false; } catch {}
18
- if (!active) process.exit(0);
19
-
20
- let iteration = 0;
21
- try { iteration = JSON.parse(readFileSync(join(LOOP_DIR, 'loop-state.json'), 'utf8')).iteration || 0; } catch {}
22
- try {
23
- appendFileSync(join(LOOP_DIR, 'history.jsonl'),
24
- JSON.stringify({ ts: new Date().toISOString(), iteration, decision: 'compaction' }) + '\n');
25
- } catch {}
26
-
27
- // Context is about to be compacted — LOOP.md (auto-injected on the next turn) is
28
- // the durable handoff. Nudge the agent to ensure its progress is captured there.
29
- process.stdout.write('Context is being compacted. LOOP.md is your durable memory across this reset — ensure your latest progress, plan, and remaining work are written there before continuing.');
30
- process.exit(0);
@@ -1,126 +0,0 @@
1
- #!/usr/bin/env node
2
- // routine-core.mjs — dependency-free primitives for the routine layer:
3
- // * cron matching + next-fire (standard 5-field cron)
4
- // * payload templating (${body.x}/${query.x}/${now}) for trigger -> action
5
- // * routine spec validation
6
- // No external deps (mirrors the loop gate). Pure + unit-testable.
7
-
8
- // ---- cron ---------------------------------------------------------------
9
- const FIELD_RANGES = [
10
- [0, 59], // minute
11
- [0, 23], // hour
12
- [1, 31], // day of month
13
- [1, 12], // month
14
- [0, 6], // day of week (0=Sun)
15
- ];
16
-
17
- function parseField(token, [min, max]) {
18
- const set = new Set();
19
- for (const part of token.split(',')) {
20
- let m;
21
- if (part === '*') { for (let i = min; i <= max; i++) set.add(i); }
22
- else if ((m = part.match(/^\*\/(\d+)$/))) { const s = +m[1]; for (let i = min; i <= max; i += s) set.add(i); }
23
- else if ((m = part.match(/^(\d+)-(\d+)\/(\d+)$/))) { const [, a, b, s] = m; for (let i = +a; i <= +b; i += +s) set.add(i); }
24
- else if ((m = part.match(/^(\d+)-(\d+)$/))) { const [, a, b] = m; for (let i = +a; i <= +b; i++) set.add(i); }
25
- else if ((m = part.match(/^(\d+)$/))) { set.add(+m[1]); }
26
- else throw new Error(`invalid cron field: "${token}"`);
27
- }
28
- for (const v of set) if (v < min || v > max) throw new Error(`cron value ${v} out of range [${min},${max}]`);
29
- return set;
30
- }
31
-
32
- export function parseCron(expr) {
33
- const fields = String(expr).trim().split(/\s+/);
34
- if (fields.length !== 5) throw new Error(`cron must have 5 fields, got ${fields.length}: "${expr}"`);
35
- const [minute, hour, dom, month, dow] = fields.map((f, i) => parseField(f, FIELD_RANGES[i]));
36
- const domRestricted = fields[2] !== '*';
37
- const dowRestricted = fields[4] !== '*';
38
- return { minute, hour, dom, month, dow, domRestricted, dowRestricted };
39
- }
40
-
41
- /** Does `date` (minute resolution) match the cron expr? */
42
- export function cronMatches(expr, date) {
43
- const c = typeof expr === 'string' ? parseCron(expr) : expr;
44
- if (!c.minute.has(date.getMinutes())) return false;
45
- if (!c.hour.has(date.getHours())) return false;
46
- if (!c.month.has(date.getMonth() + 1)) return false;
47
- const domOk = c.dom.has(date.getDate());
48
- const dowOk = c.dow.has(date.getDay());
49
- // Standard cron: when BOTH dom and dow are restricted, match is OR; else AND.
50
- if (c.domRestricted && c.dowRestricted) return domOk || dowOk;
51
- return domOk && dowOk;
52
- }
53
-
54
- /** First firing strictly after `from` (defaults to now-ish; caller passes Date). */
55
- // Return a Date whose LOCAL fields equal the wall-clock time in `tz`, so
56
- // cronMatches (which reads local getHours/getMinutes/...) effectively matches
57
- // against that timezone. Falls back to the input on any error.
58
- export function inZone(date, tz) {
59
- if (!tz) return date;
60
- try {
61
- const p = new Intl.DateTimeFormat('en-US', { timeZone: tz, hour12: false,
62
- year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
63
- .formatToParts(date).reduce((o, x) => { o[x.type] = x.value; return o; }, {});
64
- return new Date(+p.year, +p.month - 1, +p.day, +(p.hour === '24' ? 0 : p.hour), +p.minute);
65
- } catch { return date; }
66
- }
67
-
68
- export function nextFire(expr, from, tz) {
69
- const c = parseCron(expr);
70
- const d = new Date(from.getTime());
71
- d.setSeconds(0, 0);
72
- d.setMinutes(d.getMinutes() + 1); // strictly after
73
- const limit = 366 * 24 * 60; // scan up to ~1 year of minutes
74
- for (let i = 0; i < limit; i++) {
75
- if (cronMatches(c, tz ? inZone(d, tz) : d)) return new Date(d.getTime());
76
- d.setMinutes(d.getMinutes() + 1);
77
- }
78
- return null;
79
- }
80
-
81
- // ---- templating ---------------------------------------------------------
82
- function resolvePath(ctx, path) {
83
- return path.split('.').reduce((o, k) => (o == null ? undefined : o[k]), ctx);
84
- }
85
- /** Replace ${a.b.c} from ctx; missing -> empty string. No code eval. */
86
- export function renderTemplate(template, ctx) {
87
- return String(template).replace(/\$\{([\w.$]+)\}/g, (_, p) => {
88
- const v = resolvePath(ctx, p);
89
- return v == null ? '' : (typeof v === 'object' ? JSON.stringify(v) : String(v));
90
- });
91
- }
92
-
93
- // ---- spec validation ----------------------------------------------------
94
- const TRIGGER_TYPES = ['manual', 'schedule', 'webhook', 'api'];
95
- const ACTION_KINDS = ['message', 'loop'];
96
- const OVERLAP = ['queue', 'skip', 'replace'];
97
-
98
- export function validateRoutine(r) {
99
- const errs = [];
100
- if (!r || typeof r !== 'object') return ['routine must be an object'];
101
- if (!r.session_id) errs.push('session_id required');
102
- if (!r.name) errs.push('name required');
103
- const t = r.trigger;
104
- if (!t || !TRIGGER_TYPES.includes(t.type)) errs.push(`trigger.type must be one of ${TRIGGER_TYPES.join('|')}`);
105
- if (t?.type === 'schedule') {
106
- try { parseCron(t.cron); } catch (e) { errs.push(`trigger.cron: ${e.message}`); }
107
- if (t.missed && !['catchup', 'skip'].includes(t.missed)) errs.push('trigger.missed must be catchup|skip');
108
- }
109
- const a = r.action;
110
- if (!a || !ACTION_KINDS.includes(a.kind)) errs.push(`action.kind must be one of ${ACTION_KINDS.join('|')}`);
111
- if (a?.kind === 'message' && !a.template) errs.push('action.template required for message action');
112
- if (a?.kind === 'loop' && !a.loop && !a.task_template) errs.push('action.loop or action.task_template required for loop action');
113
- if (r.overlap && !OVERLAP.includes(r.overlap)) errs.push(`overlap must be one of ${OVERLAP.join('|')}`);
114
- // Security: a public (keyless) webhook + loop action = unauthenticated task
115
- // injection into the session. Require a key for loop actions.
116
- if ((t?.type === 'webhook' || t?.type === 'api') && t.public && a?.kind === 'loop')
117
- errs.push('a public webhook/api may not use a loop action (unauthenticated task injection) — use a message action or require a key');
118
- return errs;
119
- }
120
-
121
- if (import.meta.url === `file://${process.argv[1]}`) {
122
- const [cmd, ...rest] = process.argv.slice(2);
123
- if (cmd === 'next') console.log(nextFire(rest.join(' '), new Date())?.toISOString() || 'no fire');
124
- else if (cmd === 'matches') console.log(cronMatches(rest.slice(0, 5).join(' '), new Date()));
125
- else console.log('usage: routine-core.mjs next "<cron>" | matches "<cron>"');
126
- }
@@ -1,111 +0,0 @@
1
- #!/usr/bin/env node
2
- // state-fp.mjs — deterministic fingerprint of a project's working-tree state.
3
- // Used to tie an evaluator verdict to the exact code it judged: if the actor
4
- // edits anything after the evaluator ran, the fingerprint changes and the
5
- // stale verdict is rejected by the Stop gate.
6
- import { execFileSync } from 'node:child_process';
7
- import { createHash } from 'node:crypto';
8
- import { readFileSync, statSync, readdirSync, readlinkSync } from 'node:fs';
9
- import { join } from 'node:path';
10
-
11
- function git(dir, args) {
12
- try {
13
- return execFileSync('git', ['-C', dir, ...args], {
14
- encoding: 'utf8', maxBuffer: 64 * 1024 * 1024,
15
- stdio: ['ignore', 'pipe', 'ignore'], // swallow "not a git repository" noise
16
- });
17
- } catch {
18
- return '';
19
- }
20
- }
21
-
22
- // Paths excluded from the fingerprint: the loop's own bookkeeping AND its
23
- // memory file (LOOP.md). The fingerprint must track the WORK PRODUCT only —
24
- // LOOP.md/state/verdict change every iteration and would otherwise make every
25
- // verdict look stale (the agent updates LOOP.md progress as it works).
26
- function excludedPaths(dir) {
27
- // The loop's bookkeeping + memory now live under .svamp/<sid>/loop/ (and legacy
28
- // loops under .claude/loop/). Both — plus a root LOOP.md a legacy loop may have
29
- // left — are excluded so the fingerprint tracks the WORK PRODUCT only.
30
- return { loopFile: 'LOOP.md', prefixes: ['.svamp/', '.claude/loop/'] };
31
- }
32
-
33
- const WALK_SKIP = new Set(['.git', 'node_modules', '.svamp', '.expo', 'dist', 'build']);
34
-
35
- /** Fallback for non-git (or no-HEAD) dirs: hash all files on disk so the
36
- * fingerprint actually reflects content. Without this, git plumbing returns
37
- * empty strings and every state hashes identically — accepting stale verdicts. */
38
- function walkFingerprint(dir, isExcluded) {
39
- const h = createHash('sha256');
40
- h.update('WALK\0');
41
- const files = [];
42
- const links = [];
43
- const walk = (abs, rel) => {
44
- let entries; try { entries = readdirSync(abs, { withFileTypes: true }); } catch { return; }
45
- for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
46
- const childRel = rel ? `${rel}/${e.name}` : e.name;
47
- if (WALK_SKIP.has(e.name) || isExcluded(childRel)) continue;
48
- const childAbs = join(abs, e.name);
49
- // Symlinks: hash the link TARGET (don't follow — avoids cycles/escape) so a
50
- // work product reached via a symlink still changes the fingerprint.
51
- if (e.isSymbolicLink()) { try { links.push([childRel, readlinkSync(childAbs)]); } catch { links.push([childRel, 'unreadable']); } }
52
- else if (e.isDirectory()) walk(childAbs, childRel);
53
- else if (e.isFile()) files.push([childRel, childAbs]);
54
- }
55
- };
56
- walk(dir, '');
57
- for (const [rel, abs] of files.sort((a, b) => a[0].localeCompare(b[0]))) {
58
- let part = rel + ':';
59
- try {
60
- const st = statSync(abs);
61
- part += st.size < 4 * 1024 * 1024
62
- ? st.size + ':' + createHash('sha256').update(readFileSync(abs)).digest('hex')
63
- : st.size + ':skip';
64
- } catch { part += 'missing'; }
65
- h.update(part + '\0');
66
- }
67
- h.update('SYMLINKS\0');
68
- for (const [rel, target] of links.sort((a, b) => a[0].localeCompare(b[0]))) h.update(rel + ':->' + target + '\0');
69
- return h.digest('hex');
70
- }
71
-
72
- /** Fingerprint = HEAD + tracked diff + untracked file contents (size+sha).
73
- * Falls back to a filesystem walk when the dir is not a git repo / has no HEAD. */
74
- export function stateFingerprint(dir) {
75
- const { loopFile, prefixes } = excludedPaths(dir);
76
- const isExcluded = (p) => p === loopFile || prefixes.some((pre) => p.startsWith(pre));
77
- const head = git(dir, ['rev-parse', 'HEAD']).trim();
78
- if (!head) return walkFingerprint(dir, isExcluded); // non-git / no commit yet
79
- // Exclude bookkeeping/memory from the tracked diff too (in case they're committed).
80
- const diff = git(dir, ['-c', 'core.quotepath=false', 'diff', 'HEAD', '--',
81
- '.', `:(exclude)${loopFile}`, ':(exclude).claude/loop', ':(exclude).svamp']);
82
- // KNOWN LIMITATION (review #8): --exclude-standard omits gitignored files, so a
83
- // loop whose work product lands in a gitignored path (e.g. dist/) won't change
84
- // the fingerprint. Acceptable since work products are normally tracked; loops
85
- // targeting gitignored output should commit/track it or run in a non-git dir.
86
- const untrackedList = git(dir, ['ls-files', '--others', '--exclude-standard'])
87
- .split('\n').map((s) => s.trim()).filter(Boolean)
88
- .filter((p) => !isExcluded(p))
89
- .sort();
90
- const h = createHash('sha256');
91
- h.update('HEAD\0' + head + '\0DIFF\0' + diff + '\0UNTRACKED\0');
92
- for (const rel of untrackedList) {
93
- const abs = join(dir, rel);
94
- let part = rel + ':';
95
- try {
96
- const st = statSync(abs);
97
- if (st.isFile() && st.size < 4 * 1024 * 1024) {
98
- part += st.size + ':' + createHash('sha256').update(readFileSync(abs)).digest('hex');
99
- } else {
100
- part += st.size + ':skip';
101
- }
102
- } catch { part += 'missing'; }
103
- h.update(part + '\0');
104
- }
105
- return h.digest('hex');
106
- }
107
-
108
- if (import.meta.url === `file://${process.argv[1]}`) {
109
- const dir = process.argv[2] || process.cwd();
110
- process.stdout.write(stateFingerprint(dir) + '\n');
111
- }