scriptonia 0.7.0 → 0.9.0-rc.1

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/README.md CHANGED
@@ -15,8 +15,8 @@ never see one), and your agent's `AGENTS.md` / skill now know about `scriptonia
15
15
  plan`. Then, in your project:
16
16
 
17
17
  ```sh
18
- scriptonia add feedback.txt call.vtt notes.md # feed customer signal
19
- scriptonia plan "let compliance filter the audit log by actor"
18
+ npx scriptonia add feedback.txt call.vtt notes.md # feed customer signal
19
+ npx scriptonia plan "let compliance filter the audit log by actor"
20
20
  # → writes PLAN.md: goal, cited evidence, non-goals, real file paths, acceptance criteria
21
21
  claude "execute PLAN.md" # your agent ships it → PR
22
22
  ```
@@ -24,7 +24,7 @@ claude "execute PLAN.md" # your agent ships it → PR
24
24
  Left a review note? The plan regenerates:
25
25
 
26
26
  ```sh
27
- scriptonia comment plan/<slug> "default to all actors, and gate behind the admin role"
27
+ npx scriptonia comment plan/<slug> "default to all actors, and gate behind the admin role"
28
28
  # → PLAN.md v2 with your note folded in
29
29
  ```
30
30
 
@@ -32,19 +32,28 @@ scriptonia comment plan/<slug> "default to all actors, and gate behind the admin
32
32
 
33
33
  | step | command | what happens |
34
34
  |---|---|---|
35
- | **SIGNAL** | `scriptonia add <files>` | Ingest text · md · vtt as customer signal (deduped, auto-sourced). |
36
- | **PLAN** | `scriptonia plan "<issue>"` | Retrieve the relevant signal, check prior decisions, and write **PLAN.md** — every claim cites a signal id; contradictions are gated **UNRESOLVED** until a human approves. |
37
- | **BUILD** | `claude "execute PLAN.md"` | Your agent runs the plan verbatim. Respects the Non-goals fence; cites sources in the PR. |
38
- | **REFINE** | `scriptonia comment plan/<slug> "…"` | A note regenerates the plan (v+1). A comment approving an override flips a contradiction to **RESOLVED**. |
35
+ | **SIGNAL** | `npx scriptonia add <files>` | Ingest text · md · vtt as customer signal (deduped, auto-sourced). |
36
+ | **PLAN** | `npx scriptonia plan "<issue>"` | Retrieve the relevant signal, check prior decisions, and write **PLAN.md** — every claim cites a signal id; contradictions are gated **UNRESOLVED** until a human approves. |
37
+ | **BUILD** | `claude "execute PLAN.md"` | Your agent implements against the plan's cited criteria and Non-goals fence. |
38
+ | **REFINE** | `npx scriptonia comment plan/<slug> "…"` | A note regenerates the plan (v+1). A comment approving an override flips a contradiction to **RESOLVED**. |
39
+ | **VERIFY** | `npx scriptonia verify` | Freeze the Git/product-memory snapshot and consume declared CI/JUnit/static evidence. It never executes repository commands. |
39
40
 
40
41
  ## Commands
41
42
 
42
43
  ```
43
44
  setup login init [per repo]
44
- signal add <files> (--source, --segment, or cat x | scriptonia add -)
45
+ signal add <files> (--source, --segment, or cat x | npx scriptonia add -)
45
46
  plan plan "<issue>" (--out <file>, --refresh <slug>)
46
47
  plans list plans + their slugs
47
48
  comment plan/<slug> "…" refine → regenerates PLAN.md
49
+ capture run -- <agent-command> capture exactly the command supplied
50
+ runs | run show <id> inspect captured agent sessions
51
+ verify diff-check [--base <ref>] sub-second deterministic policy pass
52
+ verify [--base <ref>] complete evidence report
53
+ gate [--base <ref>] CI-compatible aggregate exit status
54
+ learn learn <finding-id> confirmed finding → draft eval
55
+ eval run [id] | eval doctor run and maintain regression cases
56
+ memory decide | pulse | why | recall local-first product/codebase memory
48
57
  inspect scriptonia the loop + what's next
49
58
  contexts | query "<topic>" | link "<id>" | sync
50
59
  account status | logout | skill | mcp
@@ -52,10 +61,14 @@ account status | logout | skill | mcp
52
61
 
53
62
  ## One brain per repo (for people juggling projects)
54
63
 
55
- `scriptonia init` in a repo gives it its own brain at
64
+ `npx scriptonia init` in a repo gives it its own brain at
56
65
  `~/.scriptonia/projects/<slug>/`, scans the code into a repo map (so plan steps
57
- name real files), and writes an `AGENTS.md` section. Confidential signal stays
58
- in your home dir only `PLAN.md` and the `AGENTS.md` note land in the repo.
66
+ name real files), and writes an `AGENTS.md` section. Customer signal you submit
67
+ is stored and embedded on Scriptonia's servers; it is not written into the
68
+ repository. V2 policies, eval definitions, and operational brain data are
69
+ local-first. Deterministic verification does not upload the codebase. When
70
+ semantic evaluation is explicitly enabled, only the bounded evidence bundle is
71
+ sent through the authenticated Scriptonia gateway.
59
72
 
60
73
  Skip `init` if you have one product — `login → add → plan` uses your default
61
74
  brain and scans whatever repo you run `plan` in.
@@ -71,4 +84,5 @@ Exposes `get_context`, `link_back`, `flag_contradiction` from your stored login.
71
84
  ## Self-hosting
72
85
 
73
86
  Defaults to `https://scriptonia.dev`. Point elsewhere with
74
- `SCRIPTONIA_URL=https://your.host scriptonia login` (or `--url`). Node 18+.
87
+ `SCRIPTONIA_URL=https://your.host npx scriptonia login` (or `--url`). Node 24 LTS
88
+ is the V2 release baseline; Node 22 remains in the compatibility matrix.
@@ -2,12 +2,12 @@
2
2
  /**
3
3
  * scriptonia — issue → executable plan for your coding agent.
4
4
  *
5
- * scriptonia login browser sign-in; wires your agent
6
- * scriptonia add <files> feed customer signal (text · md · vtt)
7
- * scriptonia plan "<issue>" → PLAN.md: sourced, contradiction-checked
8
- * scriptonia comment plan/<slug> refine → regenerates PLAN.md
9
- * scriptonia init [per repo] give a repo its own brain
10
- * scriptonia mcp MCP over stdio (for MCP-native agents)
5
+ * npx scriptonia login browser sign-in; wires your agent
6
+ * npx scriptonia add <files> feed customer signal (text · md · vtt)
7
+ * npx scriptonia plan "<issue>" → PLAN.md: sourced, contradiction-checked
8
+ * npx scriptonia comment plan/<slug> refine → regenerates PLAN.md
9
+ * npx scriptonia init [per repo] give a repo its own brain
10
+ * npx scriptonia mcp MCP over stdio (for MCP-native agents)
11
11
  *
12
12
  * After `login`, everything lives in ~/.scriptonia/. No env vars, no key
13
13
  * pasting. Pure ESM, zero build step — runs on Node 18+.
@@ -19,6 +19,7 @@ import path from "node:path";
19
19
  import readline from "node:readline";
20
20
  import { spawn, execSync } from "node:child_process";
21
21
  import { randomBytes } from "node:crypto";
22
+ import { runVerification } from "./verify.mjs";
22
23
 
23
24
  const DEFAULT_URL = process.env.SCRIPTONIA_URL || "https://scriptonia.dev";
24
25
  const CONFIG_DIR = path.join(os.homedir(), ".scriptonia");
@@ -30,7 +31,7 @@ const [, , cmd, ...rest] = process.argv;
30
31
  const commands = {
31
32
  login, init, sync,
32
33
  add, ingest: add, push: add,
33
- plan, plans, comment,
34
+ plan, plans, comment, verify,
34
35
  query, contexts, link,
35
36
  status, credits, logout, skill: reinstallSkill, mcp,
36
37
  flow, guide: flow,
@@ -63,7 +64,7 @@ async function login(args) {
63
64
  const project = u.searchParams.get("project") ?? "";
64
65
  if (!token || returnedState !== state) {
65
66
  res.writeHead(400, { "content-type": "text/html" });
66
- res.end(pageHtml("Login failed", "State mismatch — run <code>scriptonia login</code> again."));
67
+ res.end(pageHtml("Login failed", "State mismatch — run <code>npx scriptonia login</code> again."));
67
68
  return;
68
69
  }
69
70
  res.writeHead(200, { "content-type": "text/html" });
@@ -90,13 +91,13 @@ async function login(args) {
90
91
 
91
92
  const C = colors();
92
93
  console.log(`\n ${C.g}✓${C.r} Signed in as ${result.email} ${C.dim}(project "${result.project}", with sample data to try)${C.r}`);
93
- console.log(` ${C.g}✓${C.r} Agent wired — skill + AGENTS.md know about \`scriptonia plan\``);
94
+ console.log(` ${C.g}✓${C.r} Agent wired — skill + AGENTS.md know about \`npx scriptonia plan\``);
94
95
  console.log(`\n ${C.b}The loop — feed signal, turn an issue into a plan your agent runs:${C.r}\n`);
95
- console.log(` ${C.g}scriptonia add${C.r} feedback.txt call.vtt ${C.dim}feed customer signal${C.r}`);
96
- console.log(` ${C.g}scriptonia plan${C.r} "the thing to build" ${C.dim}→ PLAN.md (sourced, checked)${C.r}`);
96
+ console.log(` ${C.g}npx scriptonia add${C.r} feedback.txt call.vtt ${C.dim}feed customer signal${C.r}`);
97
+ console.log(` ${C.g}npx scriptonia plan${C.r} "the thing to build" ${C.dim}→ PLAN.md (sourced, checked)${C.r}`);
97
98
  console.log(` ${C.g}claude${C.r} "execute PLAN.md" ${C.dim}your agent ships it${C.r}`);
98
- console.log(`\n ${C.dim}Try it on the sample data now:${C.r} ${C.g}scriptonia plan "filter audit log by actor"${C.r}`);
99
- console.log(` ${C.dim}Working across repos? Run${C.r} ${C.g}scriptonia init${C.r} ${C.dim}in each for its own brain.${C.r}\n`);
99
+ console.log(`\n ${C.dim}Try it on the sample data now:${C.r} ${C.g}npx scriptonia plan "filter audit log by actor"${C.r}`);
100
+ console.log(` ${C.dim}Working across repos? Run${C.r} ${C.g}npx scriptonia init${C.r} ${C.dim}in each for its own brain.${C.r}\n`);
100
101
  }
101
102
 
102
103
  // ─────────────────────────────────────────────────────────────
@@ -108,7 +109,7 @@ async function flow() {
108
109
  if (!cfg) {
109
110
  console.log(`\n ${C.b}SCRIPTONIA${C.r} · turn customer signal into plans your agent can execute\n`);
110
111
  console.log(` One command sets everything up:\n`);
111
- console.log(` ${C.g}scriptonia login${C.r}\n`);
112
+ console.log(` ${C.g}npx scriptonia login${C.r}\n`);
112
113
  return;
113
114
  }
114
115
 
@@ -127,7 +128,7 @@ async function flow() {
127
128
  const recentPlan = o?.plans?.[0]?.slug ?? null;
128
129
 
129
130
  console.log(`\n ${C.b}SCRIPTONIA${C.r} · issue → executable plan for your agent`);
130
- console.log(` ${C.dim}${cfg.email} · project ${o?.project ?? projName}${repo ? "" : C.dim + " (global — run `scriptonia init` in a repo for its own brain)"}${C.r}\n`);
131
+ console.log(` ${C.dim}${cfg.email} · project ${o?.project ?? projName}${repo ? "" : C.dim + " (global — run `npx scriptonia init` in a repo for its own brain)"}${C.r}\n`);
131
132
 
132
133
  const row = (n, name, desc, cmd) =>
133
134
  console.log(` ${C.v}${n}${C.r} ${C.b}${name.padEnd(7)}${C.r} ${C.dim}${desc.padEnd(34)}${C.r} ${cmd ? C.g + cmd + C.r : ""}`);
@@ -135,10 +136,11 @@ async function flow() {
135
136
  const cr = o?.credits;
136
137
  const creditStr = cr && cr.metered !== false ? ` · ${cr.plan} ${cr.used}/${cr.cap} credits` : "";
137
138
  console.log(` ${C.dim}THE LOOP${C.r} ${C.dim}${s.total} signals · ${plansN} plans${creditStr}${C.r}`);
138
- row("1", "SIGNAL", "feed customer reality (text·md·vtt)", `scriptonia add <files>`);
139
- row("2", "PLAN", "issue → PLAN.md, sourced & checked", `scriptonia plan "<issue>"`);
139
+ row("1", "SIGNAL", "feed customer reality (text·md·vtt)", `npx scriptonia add <files>`);
140
+ row("2", "PLAN", "issue → PLAN.md, sourced & checked", `npx scriptonia plan "<issue>"`);
140
141
  row("3", "BUILD", "your agent executes PLAN.md → PR", `claude "execute PLAN.md"`);
141
- row("4", "REFINE", "a note regenerates the plan", `scriptonia comment plan/<slug> "…"`);
142
+ row("4", "REFINE", "a note regenerates the plan", `npx scriptonia comment plan/<slug> "…"`);
143
+ row("5", "VERIFY", "PR diff → PASS / WARN / BLOCK", `npx scriptonia verify`);
142
144
 
143
145
  console.log(`\n ${C.b}NEXT${C.r} ${nextStep(s, plansN, recentPlan, repo, C)}`);
144
146
  if (cr && cr.metered !== false && cr.available <= 0) {
@@ -149,10 +151,10 @@ async function flow() {
149
151
 
150
152
  function nextStep(s, plansN, recentPlan, repo, C) {
151
153
  if (s.total === 0)
152
- return `${C.g}scriptonia add feedback.txt call.vtt${C.r} ${C.dim}— feed the brain customer signal${C.r}`;
154
+ return `${C.g}npx scriptonia add feedback.txt call.vtt${C.r} ${C.dim}— feed the brain customer signal${C.r}`;
153
155
  if (plansN === 0)
154
- return `${C.g}scriptonia plan "the thing you're about to build"${C.r} ${C.dim}— writes PLAN.md${C.r}`;
155
- return `${C.g}claude "execute PLAN.md"${C.r} ${C.dim}— ship it · or refine: ${C.r}${C.g}scriptonia comment plan/${recentPlan} "…"${C.r}`;
156
+ return `${C.g}npx scriptonia plan "the thing you're about to build"${C.r} ${C.dim}— writes PLAN.md${C.r}`;
157
+ return `${C.g}claude "execute PLAN.md"${C.r} ${C.dim}— then gate the diff: ${C.r}${C.g}npx scriptonia verify${C.r}`;
156
158
  }
157
159
 
158
160
  // ─────────────────────────────────────────────────────────────
@@ -164,11 +166,11 @@ async function add(args) {
164
166
  const items = args.filter((a) => !a.startsWith("--"));
165
167
  if (items.length === 0) {
166
168
  fail(
167
- 'usage: scriptonia add <file...> e.g. scriptonia add slack.txt notes.md call.vtt\n' +
168
- ' scriptonia add "Issue #42: …" inline signal — also analyzed against the brain → context.md\n' +
169
+ 'usage: npx scriptonia add <file...> e.g. npx scriptonia add slack.txt notes.md call.vtt\n' +
170
+ ' npx scriptonia add "Issue #42: …" inline signal — also analyzed against the brain → context.md\n' +
169
171
  " flags: --source slack|ticket|sales_call|voice_note|founder_note|email|notion|github\n" +
170
172
  " --segment enterprise|mid_market|smb|free\n" +
171
- " pipe: cat thread.txt | scriptonia add -",
173
+ " pipe: cat thread.txt | npx scriptonia add -",
172
174
  );
173
175
  }
174
176
 
@@ -252,12 +254,12 @@ async function add(args) {
252
254
  if (cfg.inited) appendLog(cfg.dir, "add", `context.md synthesized (${(q.sources ?? []).length} cited, ${unresolved} contradictions)`);
253
255
  }
254
256
  } catch { /* analysis is best-effort; the signal is already ingested */ }
255
- console.log(`\n ${C.b}Next:${C.r} ${C.g}scriptonia plan "${items[0].slice(0, 48)}${items[0].length > 48 ? "…" : ""}"${C.r}\n`);
257
+ console.log(`\n ${C.b}Next:${C.r} ${C.g}npx scriptonia plan "${items[0].slice(0, 48)}${items[0].length > 48 ? "…" : ""}"${C.r}\n`);
256
258
  return;
257
259
  }
258
260
 
259
261
  if (created > 0) {
260
- console.log(` ${C.dim}The brain is enriching them now. Then:${C.r} ${C.g}scriptonia plan "<issue>"${C.r}\n`);
262
+ console.log(` ${C.dim}The brain is enriching them now. Then:${C.r} ${C.g}npx scriptonia plan "<issue>"${C.r}\n`);
261
263
  }
262
264
  }
263
265
 
@@ -280,7 +282,7 @@ async function contexts() {
280
282
 
281
283
  if (o.contexts_count === 0) {
282
284
  console.log(`\n ${C.dim}No contexts yet.${C.r}`);
283
- if ((o.signals?.total ?? 0) === 0) console.log(` Add signal first: ${C.g}scriptonia add <files>${C.r}\n`);
285
+ if ((o.signals?.total ?? 0) === 0) console.log(` Add signal first: ${C.g}npx scriptonia add <files>${C.r}\n`);
284
286
  else console.log(` The brain is still indexing ${o.signals.total} signals — check back in a minute.\n`);
285
287
  return;
286
288
  }
@@ -291,7 +293,7 @@ async function contexts() {
291
293
  console.log(` ${C.g}${String(c.strength).padStart(4)}${C.r} ${C.b}${c.title}${C.r} ${C.dim}${c.type} · v${c.version} · ${c.segment}${C.r}${warn}`);
292
294
  console.log(` ${C.v}${cfg.url}/contexts/${c.slug}${C.r}`);
293
295
  }
294
- console.log(`\n ${C.dim}Turn any of these into a plan:${C.r} ${C.g}scriptonia plan "<the issue>"${C.r}\n`);
296
+ console.log(`\n ${C.dim}Turn any of these into a plan:${C.r} ${C.g}npx scriptonia plan "<the issue>"${C.r}\n`);
295
297
  }
296
298
 
297
299
  // ─────────────────────────────────────────────────────────────
@@ -305,14 +307,14 @@ async function plans() {
305
307
  const o = await res.json();
306
308
  const list = o.plans ?? [];
307
309
  if (list.length === 0) {
308
- console.log(`\n ${C.dim}No plans yet.${C.r} Generate one: ${C.g}scriptonia plan "<issue>"${C.r}\n`);
310
+ console.log(`\n ${C.dim}No plans yet.${C.r} Generate one: ${C.g}npx scriptonia plan "<issue>"${C.r}\n`);
309
311
  return;
310
312
  }
311
313
  console.log(`\n ${C.b}${list.length} plan${list.length > 1 ? "s" : ""}${C.r} ${C.dim}· ${o.project}${C.r}\n`);
312
314
  for (const p of list) {
313
315
  console.log(` ${C.b}${p.slug}${C.r} ${C.dim}v${p.version}${C.r}`);
314
316
  console.log(` ${C.dim}${p.issue}${C.r}`);
315
- console.log(` ${C.g}scriptonia plan --refresh ${p.slug}${C.r} ${C.dim}·${C.r} ${C.g}scriptonia comment plan/${p.slug} "…"${C.r}`);
317
+ console.log(` ${C.g}npx scriptonia plan --refresh ${p.slug}${C.r} ${C.dim}·${C.r} ${C.g}npx scriptonia comment plan/${p.slug} "…"${C.r}`);
316
318
  }
317
319
  console.log("");
318
320
  }
@@ -329,8 +331,8 @@ async function comment(args) {
329
331
  const text = positional.slice(1).join(" ").trim();
330
332
  if (!target || !text) {
331
333
  fail(
332
- 'usage: scriptonia comment <slug> "<your note>" comment on a context\n' +
333
- ' scriptonia comment plan/<slug> "<your note>" comment on a plan → regenerates PLAN.md\n' +
334
+ 'usage: npx scriptonia comment <slug> "<your note>" comment on a context\n' +
335
+ ' npx scriptonia comment plan/<slug> "<your note>" comment on a plan → regenerates PLAN.md\n' +
334
336
  ' --as "Name" attribute the comment (defaults to you)',
335
337
  );
336
338
  }
@@ -346,7 +348,7 @@ async function comment(args) {
346
348
  });
347
349
  const data = await res.json().catch(() => ({}));
348
350
  if (res.status === 402) failCredits(data);
349
- if (res.status === 404) fail(`no plan "${slug}" — generate one first: scriptonia plan "<issue>"`);
351
+ if (res.status === 404) fail(`no plan "${slug}" — generate one first: npx scriptonia plan "<issue>"`);
350
352
  if (!res.ok) fail(`comment failed (${res.status}): ${JSON.stringify(data).slice(0, 200)}`);
351
353
  // Approval-chain receipt: which human note produced which plan version.
352
354
  if (cfg.inited) {
@@ -370,11 +372,11 @@ async function comment(args) {
370
372
  });
371
373
  const data = await res.json().catch(() => ({}));
372
374
  if (res.status === 402) failCredits(data);
373
- if (res.status === 404) fail(`no context "${target}" — see \`scriptonia contexts\``);
375
+ if (res.status === 404) fail(`no context "${target}" — see \`npx scriptonia contexts\``);
374
376
  if (!res.ok) fail(`comment failed (${res.status}): ${JSON.stringify(data)}`);
375
377
 
376
378
  console.log(`\n ${C.g}✓${C.r} comment posted on ${C.b}${target}${C.r} ${C.dim}(v${data.from_version})${C.r}`);
377
- console.log(` ${C.dim}the brain is absorbing it — it'll bump the version. check:${C.r} ${C.g}scriptonia contexts${C.r}\n`);
379
+ console.log(` ${C.dim}the brain is absorbing it — it'll bump the version. check:${C.r} ${C.g}npx scriptonia contexts${C.r}\n`);
378
380
  }
379
381
 
380
382
  // ─────────────────────────────────────────────────────────────
@@ -384,7 +386,7 @@ async function query(args) {
384
386
  const cfg = activeCreds();
385
387
  const flags = parseFlags(args);
386
388
  const q = args.filter((a) => !a.startsWith("--")).join(" ").trim();
387
- if (!q) fail('usage: scriptonia query "<topic>" [--window 30d] [--segment enterprise] [--json]');
389
+ if (!q) fail('usage: npx scriptonia query "<topic>" [--window 30d] [--segment enterprise] [--json]');
388
390
 
389
391
  const res = await fetch(`${cfg.url}/api/query`, {
390
392
  method: "POST",
@@ -420,7 +422,7 @@ async function link(args) {
420
422
  // ─────────────────────────────────────────────────────────────
421
423
  async function status() {
422
424
  const cfg = loadConfig();
423
- if (!cfg) { console.log("not logged in — run `scriptonia login`"); return; }
425
+ if (!cfg) { console.log("not logged in — run `npx scriptonia login`"); return; }
424
426
  const active = activeCreds();
425
427
  const C = colors();
426
428
  console.log(`signed in as ${cfg.email}`);
@@ -446,7 +448,7 @@ async function credits() {
446
448
  if (c.metered === false) { console.log(`\n ${C.dim}This project isn't metered.${C.r}\n`); return; }
447
449
  console.log(`\n ${C.b}${c.plan.toUpperCase()}${C.r} · ${cfg.project}`);
448
450
  console.log(` ${creditBar(c.used, c.cap, C)} ${C.b}${c.available}${C.r} of ${c.cap} credits left ${C.dim}(resets ${c.resets_at})${C.r}`);
449
- console.log(`\n ${C.dim}Plans & comments cost 5 · queries 1 · adding signal is free.${C.r}`);
451
+ console.log(`\n ${C.dim}Plans, comments & verification cost 5 · queries 1 · adding signal is free.${C.r}`);
450
452
  if (c.available <= 0) console.log(` ${C.w}Out of credits.${C.r} ${c.topup}`);
451
453
  else console.log(` ${C.dim}Need more? ${c.topup}${C.r}`);
452
454
  console.log("");
@@ -505,7 +507,7 @@ function skillMarkdown() {
505
507
  name: scriptonia
506
508
  description: >
507
509
  BEFORE building any user-facing change — a feature, a bug fix, a refactor with
508
- UX impact — run \`scriptonia plan "<the issue>"\`. It writes PLAN.md here: a
510
+ UX impact — run \`npx scriptonia plan "<the issue>"\`. It writes PLAN.md here: a
509
511
  hyper-focused, sourced, executable plan grounded in this project's real
510
512
  customer signal, prior decisions, and human comments. Execute PLAN.md exactly.
511
513
  ---
@@ -514,18 +516,20 @@ description: >
514
516
 
515
517
  When asked to build/fix anything user-facing:
516
518
 
517
- 1. Run \`scriptonia plan "<the issue>"\` — it writes **PLAN.md** in the repo.
519
+ 1. Run \`npx scriptonia plan "<the issue>"\` — it writes **PLAN.md** in the repo.
518
520
  2. Execute PLAN.md **exactly**. Respect its **Non-goals** — do not add anything
519
521
  outside the acceptance criteria.
520
522
  3. If a constraint is marked **UNRESOLVED**, STOP and surface it to the human.
521
- 4. Your PR description must cite the \`sources\` signal ids from PLAN.md.
523
+ 4. Run \`npx scriptonia verify\` after implementation. Fix every BLOCK before
524
+ merge and preserve the generated regression case.
525
+ 5. Your PR description must cite the \`sources\` signal ids from PLAN.md.
522
526
 
523
527
  If a plan step conflicts with what you find in the code, run
524
- \`scriptonia comment plan/<slug> "<what you found>"\` and re-read the regenerated PLAN.md.
528
+ \`npx scriptonia comment plan/<slug> "<what you found>"\` and re-read the regenerated PLAN.md.
525
529
 
526
- To just look something up without a full plan: \`scriptonia query "<topic>"\`
530
+ To just look something up without a full plan: \`npx scriptonia query "<topic>"\`
527
531
  (JSON: requirements with source_ids, contradictions, edge_cases). Quote a
528
- customer verbatim with \`scriptonia link "<signal_id>"\`.
532
+ customer verbatim with \`npx scriptonia link "<signal_id>"\`.
529
533
 
530
534
  Rule: never invent demand. If nothing in the plan/query supports a step, it
531
535
  doesn't belong in the PR.
@@ -588,7 +592,7 @@ function loadConfig() {
588
592
  }
589
593
  function requireConfig() {
590
594
  const cfg = loadConfig();
591
- if (!cfg) fail("not logged in — run `scriptonia login`");
595
+ if (!cfg) fail("not logged in — run `npx scriptonia login`");
592
596
  return cfg;
593
597
  }
594
598
  function saveConfig(cfg) {
@@ -662,31 +666,37 @@ function usage() {
662
666
  console.log(`scriptonia — issue → executable plan for your coding agent
663
667
 
664
668
  THE LOOP
665
- 1 SIGNAL scriptonia add <files> feed customer reality (text · md · vtt)
666
- 2 PLAN scriptonia plan "<issue>" → PLAN.md: sourced, contradiction-checked
669
+ 1 SIGNAL npx scriptonia add <files> feed customer reality (text · md · vtt)
670
+ 2 PLAN npx scriptonia plan "<issue>" → PLAN.md: sourced, contradiction-checked
667
671
  3 BUILD claude "execute PLAN.md" your agent ships it → PR
668
- 4 REFINE scriptonia comment plan/<slug> "…" a note regenerates the plan
672
+ 4 REFINE npx scriptonia comment plan/<slug> "…" a note regenerates the plan
669
673
 
670
674
  SETUP
671
- scriptonia login sign in (browser) — wires your agent, no keys
672
- scriptonia init [per repo] give this repo its own brain + AGENTS.md
675
+ npx scriptonia login sign in (browser) — wires your agent, no keys
676
+ npx scriptonia init [per repo] give this repo its own brain + AGENTS.md
673
677
 
674
678
  SIGNAL & PLANS
675
- scriptonia add <file...> ingest files (--source, --segment; or cat x | scriptonia add -)
676
- scriptonia plan "<issue>" generate PLAN.md (--out <file>, --refresh <slug>)
677
- scriptonia plans list your plans (and their slugs)
678
- scriptonia comment plan/<slug> "…" refine a plan → regenerates PLAN.md
679
+ npx scriptonia add <file...> ingest files (--source, --segment; or cat x | npx scriptonia add -)
680
+ npx scriptonia plan "<issue>" generate PLAN.md (--out <file>, --refresh <slug>)
681
+ npx scriptonia plans list your plans (and their slugs)
682
+ npx scriptonia comment plan/<slug> "…" refine a plan → regenerates PLAN.md
683
+ npx scriptonia verify check the git diff against PLAN.md
679
684
 
680
685
  INSPECT
681
- scriptonia the loop + what's next
682
- scriptonia contexts what the brain learned from your signal
683
- scriptonia query "<topic>" quick lookup, no full plan (--json)
684
- scriptonia link "<id>" a signal's full text
685
- scriptonia sync write the brain to local files
686
- scriptonia credits plan, balance, reset date
687
-
688
- ACCOUNT scriptonia status | logout | skill | mcp
689
- Plans & comments cost 5 credits · queries 1 · adding signal is free.
686
+ npx scriptonia the loop + what's next
687
+ npx scriptonia contexts what the brain learned from your signal
688
+ npx scriptonia query "<topic>" quick lookup, no full plan (--json)
689
+ npx scriptonia link "<id>" a signal's full text
690
+ npx scriptonia sync write the brain to local files
691
+ npx scriptonia credits plan, balance, reset date
692
+
693
+ VERIFY
694
+ npx scriptonia verify [--plan PLAN.md] [--base origin/main] [--diff patch.txt]
695
+ [--skip-checks] [--out .scriptonia/evals/<case>.yaml]
696
+ Exit 0: PASS/WARN · Exit 1: BLOCK or operational failure
697
+
698
+ ACCOUNT npx scriptonia status | npx scriptonia logout | npx scriptonia skill | npx scriptonia mcp
699
+ Plans, comments & verification cost 5 credits · queries 1 · adding signal is free.
690
700
  `);
691
701
  }
692
702
 
@@ -697,7 +707,7 @@ function usage() {
697
707
  /** Repo-local project when pwd is inside an initialized repo, else the global login. */
698
708
  function activeCreds() {
699
709
  const g = loadConfig();
700
- if (!g) fail("run `scriptonia login` first");
710
+ if (!g) fail("run `npx scriptonia login` first");
701
711
  const proj = findProjectForCwd();
702
712
  if (proj) {
703
713
  // v0.6 brains are flat (REPO.md etc. at the project dir root); older ones
@@ -736,7 +746,7 @@ function findProjectForCwd() {
736
746
  async function init(args) {
737
747
  const flags = parseFlags(args);
738
748
  const g = loadConfig();
739
- if (!g) fail("run `scriptonia login` first, then `scriptonia init` in your repo");
749
+ if (!g) fail("run `npx scriptonia login` first, then `npx scriptonia init` in your repo");
740
750
  const C = colors();
741
751
  const cwd = process.cwd();
742
752
 
@@ -744,8 +754,8 @@ async function init(args) {
744
754
  if (existing && !flags.force) {
745
755
  writeRepoMd(existing.dir, scanRepo(cwd));
746
756
  console.log(`\n ${C.g}✓${C.r} already initialized as ${C.b}${existing.data.name}${C.r} ${C.dim}(${existing.dir})${C.r}`);
747
- console.log(` ${C.dim}re-scan from scratch:${C.r} ${C.g}scriptonia init --force${C.r}`);
748
- console.log(` ${C.dim}next:${C.r} ${C.g}scriptonia add <files>${C.r}\n`);
757
+ console.log(` ${C.dim}re-scan from scratch:${C.r} ${C.g}npx scriptonia init --force${C.r}`);
758
+ console.log(` ${C.dim}next:${C.r} ${C.g}npx scriptonia add <files>${C.r}\n`);
749
759
  return;
750
760
  }
751
761
 
@@ -765,7 +775,7 @@ async function init(args) {
765
775
  headers: { authorization: `Bearer ${g.token}`, "content-type": "application/json" },
766
776
  body: JSON.stringify({ name }),
767
777
  });
768
- if (!res.ok) fail(`could not create project (${res.status}) — is your login valid? try \`scriptonia login\``);
778
+ if (!res.ok) fail(`could not create project (${res.status}) — is your login valid? try \`npx scriptonia login\``);
769
779
  const data = await res.json();
770
780
  projectId = data.projectId; token = data.token;
771
781
  }
@@ -800,15 +810,15 @@ async function init(args) {
800
810
  console.log(` ${C.g}✓${C.r} Scanned repo (${scan.stack})`);
801
811
  console.log(` ${C.g}✓${C.r} Wrote agent instructions ${C.dim}→ ./AGENTS.md${C.r}`);
802
812
  console.log(`\n ${C.b}Feed it customer signal, then turn an issue into a plan:${C.r}\n`);
803
- console.log(` ${C.g}scriptonia add${C.r} feedback.txt call.vtt ${C.dim}(or: scriptonia add "Issue #42: …")${C.r}`);
804
- console.log(` ${C.g}scriptonia plan${C.r} "the issue you want to build"\n`);
813
+ console.log(` ${C.g}npx scriptonia add${C.r} feedback.txt call.vtt ${C.dim}(or: npx scriptonia add "Issue #42: …")${C.r}`);
814
+ console.log(` ${C.g}npx scriptonia plan${C.r} "the issue you want to build"\n`);
805
815
  }
806
816
 
807
817
  // ── sync ──────────────────────────────────────────────────────
808
818
  async function sync() {
809
819
  const cfg = activeCreds();
810
820
  const C = colors();
811
- if (!cfg.inited) fail("run `scriptonia init` in this repo first");
821
+ if (!cfg.inited) fail("run `npx scriptonia init` in this repo first");
812
822
  const res = await fetch(`${cfg.url}/api/brain`, { headers: { authorization: `Bearer ${cfg.token}` } });
813
823
  if (!res.ok) fail(`sync failed (${res.status})`);
814
824
  const brain = await res.json();
@@ -849,7 +859,7 @@ async function plan(args) {
849
859
  }
850
860
 
851
861
  const issue = args.filter((a) => !a.startsWith("--")).join(" ").trim();
852
- if (!issue) fail('usage: scriptonia plan "<the issue>" [--out PLAN.md] [--refresh <slug>]');
862
+ if (!issue) fail('usage: npx scriptonia plan "<the issue>" [--out PLAN.md] [--refresh <slug>]');
853
863
 
854
864
  const repoSummary = cfg.inited && fs.existsSync(path.join(cfg.brainDir, "REPO.md"))
855
865
  ? fs.readFileSync(path.join(cfg.brainDir, "REPO.md"), "utf8").slice(0, 6000)
@@ -867,6 +877,12 @@ async function plan(args) {
867
877
  writePlan(cfg, data, flags);
868
878
  }
869
879
 
880
+ // ── verify — PLAN.md + git diff → separate CI gates ──────────────────
881
+ async function verify(args) {
882
+ const result = await runVerification(args, { cfg: activeCreds(), cwd: process.cwd() });
883
+ process.exitCode = result.exitCode;
884
+ }
885
+
870
886
  function writePlan(cfg, data, flags) {
871
887
  const C = colors();
872
888
  const out = flags.out ?? "PLAN.md";
@@ -882,7 +898,7 @@ function writePlan(cfg, data, flags) {
882
898
  console.log(`\n ${C.g}✓${C.r} Plan ${C.b}${data.slug}${C.r} ${C.dim}v${data.version}${C.r} → ${C.v}${out}${C.r}`);
883
899
  console.log(` ${C.dim}${(data.sources ?? []).length} cited signal(s)${data.contradictions ? `, ${data.contradictions} unresolved contradiction(s)` : ""}${C.r}`);
884
900
  console.log(`\n ${C.b}Hand it to your agent:${C.r} ${C.g}claude "execute PLAN.md"${C.r} ${C.dim}(or codex · opencode · hermes)${C.r}`);
885
- console.log(` ${C.dim}refine:${C.r} ${C.g}scriptonia comment plan/${data.slug} "…"${C.r} ${C.dim}(rewrites PLAN.md)${C.r}`);
901
+ console.log(` ${C.dim}refine:${C.r} ${C.g}npx scriptonia comment plan/${data.slug} "…"${C.r} ${C.dim}(rewrites PLAN.md)${C.r}`);
886
902
  if (data.credits) {
887
903
  const cr = data.credits;
888
904
  const low = cr.cap > 0 && cr.cap - cr.used <= Math.max(2, Math.round(cr.cap * 0.2));
@@ -973,7 +989,7 @@ function writeProjectMd(dir, scan, pitch) {
973
989
  "",
974
990
  `# ${scan.name}`,
975
991
  "",
976
- pitch ? `> ${pitch}` : (scan.pkgDescription ? `> ${scan.pkgDescription}` : "> (describe this product with `scriptonia init --force`)"),
992
+ pitch ? `> ${pitch}` : (scan.pkgDescription ? `> ${scan.pkgDescription}` : "> (describe this product with `npx scriptonia init --force`)"),
977
993
  "",
978
994
  `**Stack:** ${scan.stack}`,
979
995
  scan.deps.length ? `**Key deps:** ${scan.deps.join(", ")}` : "",
@@ -982,7 +998,7 @@ function writeProjectMd(dir, scan, pitch) {
982
998
  scan.readmeHead ? "## From the README\n\n" + scan.readmeHead : "",
983
999
  "",
984
1000
  "---",
985
- "*The product brain. Signal receipts in `signals/`, constraints in `rules/`, enriched contexts in `contexts/`, live synthesis in `context.md`, live plan in `plan.md`. Generated by `scriptonia init`.*",
1001
+ "*The product brain. Signal receipts in `signals/`, constraints in `rules/`, enriched contexts in `contexts/`, live synthesis in `context.md`, live plan in `plan.md`. Generated by `npx scriptonia init`.*",
986
1002
  ].filter((l) => l !== "" && l !== null).join("\n");
987
1003
  fs.writeFileSync(path.join(dir, "PROJECT.md"), md);
988
1004
  }
@@ -1009,25 +1025,27 @@ function writeAgentsBrainMd(dir, meta, decisions) {
1009
1025
  "",
1010
1026
  "## 1. Identity",
1011
1027
  "",
1012
- meta.pitch ? `${meta.pitch}` : "(run `scriptonia init --force` to describe this product)",
1028
+ meta.pitch ? `${meta.pitch}` : "(run `npx scriptonia init --force` to describe this product)",
1013
1029
  meta.stack ? `\nStack: ${meta.stack}` : "",
1014
1030
  "",
1015
1031
  "## 2. Workflow — non-negotiable",
1016
1032
  "",
1017
- '1. Before building any user-facing change, run `scriptonia plan "<the issue>"`.',
1033
+ '1. Before building any user-facing change, run `npx scriptonia plan "<the issue>"`.',
1018
1034
  " It writes PLAN.md, grounded in real customer signal and checked against",
1019
1035
  " the rules below.",
1020
1036
  "2. Execute PLAN.md **exactly**. Its **Non-goals** section is a fence — do not",
1021
1037
  " build past it.",
1022
1038
  "3. Any constraint marked **⚠ UNRESOLVED** is an approval gate: STOP and get a",
1023
- ' human comment (`scriptonia comment plan/<slug> "…"`) before writing code.',
1039
+ ' human comment (`npx scriptonia comment plan/<slug> "…"`) before writing code.',
1024
1040
  "4. Cite PLAN.md's `sources` signal ids in your PR description.",
1041
+ "5. After implementation, run `npx scriptonia verify`; fix every BLOCK",
1042
+ " before merge and preserve the generated `.scriptonia/evals/*.yaml` case.",
1025
1043
  "",
1026
1044
  "## 3. Non-negotiable constraints (from the decisions corpus)",
1027
1045
  "",
1028
1046
  ];
1029
1047
  if (decisions.length === 0) {
1030
- L.push("_None synced yet — run `scriptonia sync` after decisions exist. The server_", "_still enforces them during `plan` regardless._");
1048
+ L.push("_None synced yet — run `npx scriptonia sync` after decisions exist. The server_", "_still enforces them during `plan` regardless._");
1031
1049
  } else {
1032
1050
  for (const d of decisions) {
1033
1051
  L.push(`### ${d.title}${d.ref ? ` (${d.ref})` : ""}`, "", d.body, "");
@@ -1058,7 +1076,7 @@ function renderRule(d) {
1058
1076
  "check: |",
1059
1077
  ...String(d.body).split("\n").map((l) => ` ${l}`),
1060
1078
  "enforcement: |",
1061
- " Server-side during `scriptonia plan`: any issue conflicting with this rule",
1079
+ " Server-side during `npx scriptonia plan`: any issue conflicting with this rule",
1062
1080
  " is marked ⚠ UNRESOLVED in PLAN.md until a human comment approves the",
1063
1081
  " override (recorded in comments/plan/<slug>/).",
1064
1082
  "---",
@@ -1098,7 +1116,7 @@ function renderContextLive(issue, q, project, signalId) {
1098
1116
  if (cons.length) {
1099
1117
  L.push("## ⚠ CONTRADICTION GATE — UNRESOLVED");
1100
1118
  for (const c of cons) L.push(`- ${c.detail} (${c.decision_ref})`);
1101
- L.push("", "**STATUS: ⚠ UNRESOLVED.** A human must approve the override before any plan", "built on this context is executed. `scriptonia plan` will carry this gate.", "");
1119
+ L.push("", "**STATUS: ⚠ UNRESOLVED.** A human must approve the override before any plan", "built on this context is executed. `npx scriptonia plan` will carry this gate.", "");
1102
1120
  } else {
1103
1121
  L.push("## Contradiction gate", "", "✓ No conflicts with prior decisions on record.", "");
1104
1122
  }
@@ -1113,7 +1131,7 @@ function renderContextLive(issue, q, project, signalId) {
1113
1131
  for (const s of q.sources ?? []) {
1114
1132
  L.push(`- \`sig_${s.signal_id.slice(0, 8)}\` ${s.source}${s.ref ? ` — ${s.ref}` : ""} _(similarity ${s.similarity})_`);
1115
1133
  }
1116
- L.push("", "---", `*Next: \`scriptonia plan "${issue.slice(0, 60)}${issue.length > 60 ? "…" : ""}"\` → PLAN.md.*`);
1134
+ L.push("", "---", `*Next: \`npx scriptonia plan "${issue.slice(0, 60)}${issue.length > 60 ? "…" : ""}"\` → PLAN.md.*`);
1117
1135
  return L.join("\n");
1118
1136
  }
1119
1137
 
@@ -1142,7 +1160,7 @@ function buildRepoMd(scan) {
1142
1160
  scan.tree,
1143
1161
  "```",
1144
1162
  "",
1145
- "*When you name files in a plan, use paths from this tree. Refresh with `scriptonia init --force`.*",
1163
+ "*When you name files in a plan, use paths from this tree. Refresh with `npx scriptonia init --force`.*",
1146
1164
  ].filter(Boolean).join("\n");
1147
1165
  }
1148
1166
 
@@ -1164,13 +1182,15 @@ function injectAgentsMd(cwd) {
1164
1182
  This repo has a product brain (customer signal + prior decisions). Before building
1165
1183
  any user-facing change:
1166
1184
 
1167
- - \`scriptonia plan "<the issue>"\` → writes **PLAN.md** here. Execute PLAN.md
1185
+ - \`npx scriptonia plan "<the issue>"\` → writes **PLAN.md** here. Execute PLAN.md
1168
1186
  exactly; respect its **Non-goals**; cite its \`sources\` ids in your PR.
1169
- - \`scriptonia query "<topic>"\` → JSON: what customers asked for, contradictions,
1187
+ - \`npx scriptonia query "<topic>"\` → JSON: what customers asked for, contradictions,
1170
1188
  edge cases.
1171
1189
  - If a plan constraint is **UNRESOLVED**, stop and ask the human. If a step
1172
- conflicts with the code, \`scriptonia comment plan/<slug> "<what you found>"\`
1190
+ conflicts with the code, \`npx scriptonia comment plan/<slug> "<what you found>"\`
1173
1191
  and re-read PLAN.md.
1192
+ - After implementation, run \`npx scriptonia verify\`. Fix every BLOCK before
1193
+ merge; keep the generated \`.scriptonia/evals/*.yaml\` regression case.
1174
1194
  ${END}`;
1175
1195
 
1176
1196
  let existing = "";