svamp-cli 0.2.118 → 0.2.119

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.
@@ -0,0 +1,91 @@
1
+ // checklist.mjs — the loop-engineering task/criteria atom.
2
+ // See docs/svamp-loop-engineering-vision.md. A checklist is a list of evaluable
3
+ // items persisted as JSON, in two layered scopes:
4
+ // session: <loopDir>/checklist.json (this session's goal)
5
+ // project: <projectDir>/.svamp/checklist.json (durable invariants, all sessions)
6
+ // The effective checklist a session enforces = project ∪ session. Each item is
7
+ // oracle-checked (a pass/fail command) or agent-evaluated. Done ≠ gone: a passing
8
+ // item STAYS in the list and is re-verified every loop, so it can regress to failing.
9
+ // The supervisor only lets the turn end when ALL effective items are passing.
10
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
11
+ import { join, dirname } from 'node:path';
12
+ import { execSync } from 'node:child_process';
13
+
14
+ export function sessionChecklistPath(loopDir) { return join(loopDir, 'checklist.json'); }
15
+ export function projectChecklistPath(projectDir) { return join(projectDir, '.svamp', 'checklist.json'); }
16
+
17
+ const STATUSES = ['pending', 'passing', 'failing'];
18
+
19
+ function readOne(path, scope) {
20
+ try {
21
+ if (!existsSync(path)) return [];
22
+ const j = JSON.parse(readFileSync(path, 'utf-8'));
23
+ const items = Array.isArray(j) ? j : (Array.isArray(j?.items) ? j.items : []);
24
+ return items.map((it, i) => ({
25
+ id: typeof it.id === 'string' && it.id ? it.id : `${scope}-${i}`,
26
+ text: String(it?.text ?? '').trim(),
27
+ // 'done' is a friendly alias for 'passing'.
28
+ status: it?.status === 'done' ? 'passing' : (STATUSES.includes(it?.status) ? it.status : 'pending'),
29
+ oracle: typeof it?.oracle === 'string' && it.oracle.trim() ? it.oracle.trim() : null,
30
+ scope,
31
+ })).filter((it) => it.text);
32
+ } catch { return []; }
33
+ }
34
+
35
+ /** Effective checklist = project invariants ∪ session goals (project first, then session). */
36
+ export function readEffectiveChecklist(loopDir, projectDir) {
37
+ return [
38
+ ...readOne(projectChecklistPath(projectDir), 'project'),
39
+ ...readOne(sessionChecklistPath(loopDir), 'session'),
40
+ ];
41
+ }
42
+
43
+ /**
44
+ * Run each item's oracle (if it has one) and return items with refreshed status.
45
+ * Items WITHOUT an oracle keep their stored status (those are agent-evaluated, not
46
+ * machine-checkable here). This is the per-loop regression check: a previously
47
+ * passing item whose oracle now fails flips to 'failing'.
48
+ */
49
+ export function evaluateChecklist(items, projectDir, timeoutSec = 600) {
50
+ return items.map((it) => {
51
+ if (!it.oracle) return it;
52
+ try {
53
+ execSync(it.oracle, { cwd: projectDir, stdio: 'pipe', maxBuffer: 16 * 1024 * 1024, timeout: timeoutSec * 1000 });
54
+ return { ...it, status: 'passing' };
55
+ } catch {
56
+ return { ...it, status: 'failing' };
57
+ }
58
+ });
59
+ }
60
+
61
+ /** True when every effective item is passing (an empty list is trivially satisfied). */
62
+ export function allPassing(items) {
63
+ return items.length === 0 ? true : items.every((it) => it.status === 'passing');
64
+ }
65
+
66
+ /** A one-line summary for the gate's history/state. */
67
+ export function summarize(items) {
68
+ const pass = items.filter((i) => i.status === 'passing').length;
69
+ const fail = items.filter((i) => i.status === 'failing').length;
70
+ return `${pass}/${items.length} passing${fail ? `, ${fail} failing` : ''}`;
71
+ }
72
+
73
+ /**
74
+ * Persist refreshed statuses back to each scope's file, so the UI renderer + the
75
+ * agent see live state. Writes the canonical { items: [...] } shape (scope stripped —
76
+ * it's implied by which file the item lives in).
77
+ */
78
+ export function writeChecklistStatuses(loopDir, projectDir, items) {
79
+ const targets = [
80
+ ['session', sessionChecklistPath(loopDir)],
81
+ ['project', projectChecklistPath(projectDir)],
82
+ ];
83
+ for (const [scope, path] of targets) {
84
+ const scoped = items.filter((it) => it.scope === scope).map(({ scope: _s, ...rest }) => rest);
85
+ if (scoped.length === 0 && !existsSync(path)) continue; // don't create empty files
86
+ try {
87
+ mkdirSync(dirname(path), { recursive: true });
88
+ writeFileSync(path, JSON.stringify({ items: scoped }, null, 2));
89
+ } catch { /* best-effort persistence */ }
90
+ }
91
+ }
@@ -0,0 +1,65 @@
1
+ // test-checklist.mjs — the loop-engineering checklist atom (read/merge/evaluate/persist).
2
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import {
6
+ readEffectiveChecklist, evaluateChecklist, allPassing, summarize,
7
+ writeChecklistStatuses, sessionChecklistPath, projectChecklistPath,
8
+ } from '../bin/checklist.mjs';
9
+
10
+ let passed = 0, failed = 0;
11
+ function ok(cond, msg) { if (cond) { passed++; console.log(` ✓ ${msg}`); } else { failed++; console.log(` ✗ ${msg}`); } }
12
+ function eq(a, b, msg) { ok(JSON.stringify(a) === JSON.stringify(b), `${msg} (got ${JSON.stringify(a)})`); }
13
+
14
+ const root = mkdtempSync(join(tmpdir(), 'cl-test-'));
15
+ const projectDir = root;
16
+ const loopDir = join(root, '.svamp', 'sess1', 'loop');
17
+ mkdirSync(loopDir, { recursive: true });
18
+ mkdirSync(join(root, '.svamp'), { recursive: true });
19
+
20
+ console.log('scope merge + normalization');
21
+ writeFileSync(projectChecklistPath(projectDir), JSON.stringify({ items: [
22
+ { text: 'tests pass', oracle: 'true', status: 'passing' },
23
+ ] }));
24
+ writeFileSync(sessionChecklistPath(loopDir), JSON.stringify({ items: [
25
+ { text: 'add feature', status: 'done' }, // 'done' alias → passing
26
+ { text: ' ', status: 'pending' }, // blank → dropped
27
+ { text: 'no TODOs', oracle: 'false' }, // defaults to pending
28
+ ] }));
29
+ let eff = readEffectiveChecklist(loopDir, projectDir);
30
+ eq(eff.length, 3, 'effective = project ∪ session, blanks dropped');
31
+ eq(eff[0].scope, 'project', 'project items come first');
32
+ eq(eff[0].text, 'tests pass', 'project item text');
33
+ eq(eff[1].status, 'passing', "'done' normalized to passing");
34
+ ok(eff.map(i => i.scope).join(',') === 'project,session,session', 'scope tags correct');
35
+
36
+ console.log('evaluate — oracle pass/fail drives status (regression check)');
37
+ const evaluated = evaluateChecklist(eff, projectDir);
38
+ eq(evaluated.find(i => i.text === 'tests pass').status, 'passing', 'oracle `true` → passing');
39
+ eq(evaluated.find(i => i.text === 'no TODOs').status, 'failing', 'oracle `false` → failing');
40
+ eq(evaluated.find(i => i.text === 'add feature').status, 'passing', 'no-oracle item keeps stored status');
41
+
42
+ console.log('allPassing gate');
43
+ ok(!allPassing(evaluated), 'not all passing while one oracle fails');
44
+ ok(allPassing([]), 'empty list is trivially satisfied');
45
+ ok(allPassing(evaluated.map(i => ({ ...i, status: 'passing' }))), 'all passing → true');
46
+
47
+ console.log('summarize');
48
+ ok(summarize(evaluated).startsWith('2/3 passing'), `summary reads "${summarize(evaluated)}"`);
49
+
50
+ console.log('persist statuses back to the right scope files');
51
+ writeChecklistStatuses(loopDir, projectDir, evaluated);
52
+ const proj = JSON.parse(readFileSync(projectChecklistPath(projectDir), 'utf-8'));
53
+ const sess = JSON.parse(readFileSync(sessionChecklistPath(loopDir), 'utf-8'));
54
+ eq(proj.items.length, 1, 'project file holds only project items');
55
+ eq(sess.items.length, 2, 'session file holds only session items');
56
+ ok(proj.items[0].scope === undefined, 'scope stripped from persisted file');
57
+ ok(sess.items.find(i => i.text === 'no TODOs').status === 'failing', 'failing status persisted (UI will show it)');
58
+
59
+ // regression: a re-read after persist is stable
60
+ const reEff = readEffectiveChecklist(loopDir, projectDir);
61
+ eq(reEff.length, 3, 're-read after persist is stable');
62
+
63
+ rmSync(root, { recursive: true, force: true });
64
+ console.log(`\nchecklist: ${passed} passed, ${failed} failed`);
65
+ process.exit(failed ? 1 : 0);
package/dist/cli.mjs CHANGED
@@ -390,7 +390,7 @@ async function main() {
390
390
  } else if (!subcommand || subcommand === "start") {
391
391
  await handleInteractiveCommand();
392
392
  } else if (subcommand === "--version" || subcommand === "-v") {
393
- const pkg = await import('./package-CxWiFy_P.mjs').catch(() => ({ default: { version: "unknown" } }));
393
+ const pkg = await import('./package-B-M4dhbv.mjs').catch(() => ({ default: { version: "unknown" } }));
394
394
  console.log(`svamp version: ${pkg.default.version}`);
395
395
  } else {
396
396
  console.error(`Unknown command: ${subcommand}`);
@@ -1,5 +1,5 @@
1
1
  var name = "svamp-cli";
2
- var version = "0.2.118";
2
+ var version = "0.2.119";
3
3
  var description = "Svamp CLI — AI workspace daemon on Hypha Cloud";
4
4
  var author = "Amun AI AB";
5
5
  var license = "SEE LICENSE IN LICENSE";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svamp-cli",
3
- "version": "0.2.118",
3
+ "version": "0.2.119",
4
4
  "description": "Svamp CLI — AI workspace daemon on Hypha Cloud",
5
5
  "author": "Amun AI AB",
6
6
  "license": "SEE LICENSE IN LICENSE",