scriptonia 0.6.0 → 0.8.0
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 +17 -12
- package/bin/scriptonia.mjs +178 -87
- package/bin/verify.mjs +266 -0
- package/package.json +5 -2
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,21 @@ 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
|
|
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` | Check the git diff, unresolved constraints, repo checks, and test coverage. Writes `.scriptonia/evals/<plan>.yaml`; exits 1 on BLOCK. |
|
|
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
|
+
verify verify [--base <ref>] PLAN.md + git diff → PASS / WARN / BLOCK
|
|
48
50
|
inspect scriptonia the loop + what's next
|
|
49
51
|
contexts | query "<topic>" | link "<id>" | sync
|
|
50
52
|
account status | logout | skill | mcp
|
|
@@ -52,10 +54,13 @@ account status | logout | skill | mcp
|
|
|
52
54
|
|
|
53
55
|
## One brain per repo (for people juggling projects)
|
|
54
56
|
|
|
55
|
-
`scriptonia init` in a repo gives it its own brain at
|
|
57
|
+
`npx scriptonia init` in a repo gives it its own brain at
|
|
56
58
|
`~/.scriptonia/projects/<slug>/`, scans the code into a repo map (so plan steps
|
|
57
|
-
name real files), and writes an `AGENTS.md` section.
|
|
58
|
-
|
|
59
|
+
name real files), and writes an `AGENTS.md` section. Customer signal you submit
|
|
60
|
+
is stored and embedded on Scriptonia's servers; it is not written into the
|
|
61
|
+
repository. The local brain is a human-readable working surface. Initialization
|
|
62
|
+
does not upload the full codebase. When you explicitly run `verify`, the selected
|
|
63
|
+
PLAN.md and git diff are sent for analysis; the YAML regression case stays local.
|
|
59
64
|
|
|
60
65
|
Skip `init` if you have one product — `login → add → plan` uses your default
|
|
61
66
|
brain and scans whatever repo you run `plan` in.
|
|
@@ -71,4 +76,4 @@ Exposes `get_context`, `link_back`, `flag_contradiction` from your stored login.
|
|
|
71
76
|
## Self-hosting
|
|
72
77
|
|
|
73
78
|
Defaults to `https://scriptonia.dev`. Point elsewhere with
|
|
74
|
-
`SCRIPTONIA_URL=https://your.host scriptonia login` (or `--url`). Node 18+.
|
|
79
|
+
`SCRIPTONIA_URL=https://your.host npx scriptonia login` (or `--url`). Node 18+.
|
package/bin/scriptonia.mjs
CHANGED
|
@@ -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,9 +31,9 @@ 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
|
-
status, logout, skill: reinstallSkill, mcp,
|
|
36
|
+
status, credits, logout, skill: reinstallSkill, mcp,
|
|
36
37
|
flow, guide: flow,
|
|
37
38
|
help: async () => usage(),
|
|
38
39
|
};
|
|
@@ -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,26 +128,33 @@ 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 : ""}`);
|
|
134
135
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
const cr = o?.credits;
|
|
137
|
+
const creditStr = cr && cr.metered !== false ? ` · ${cr.plan} ${cr.used}/${cr.cap} credits` : "";
|
|
138
|
+
console.log(` ${C.dim}THE LOOP${C.r} ${C.dim}${s.total} signals · ${plansN} plans${creditStr}${C.r}`);
|
|
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>"`);
|
|
138
141
|
row("3", "BUILD", "your agent executes PLAN.md → PR", `claude "execute PLAN.md"`);
|
|
139
|
-
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`);
|
|
140
144
|
|
|
141
|
-
console.log(`\n ${C.b}NEXT${C.r} ${nextStep(s, plansN, recentPlan, repo, C)}
|
|
145
|
+
console.log(`\n ${C.b}NEXT${C.r} ${nextStep(s, plansN, recentPlan, repo, C)}`);
|
|
146
|
+
if (cr && cr.metered !== false && cr.available <= 0) {
|
|
147
|
+
console.log(` ${C.w}Out of credits${C.r} ${C.dim}— ${cr.topup}${C.r}`);
|
|
148
|
+
}
|
|
149
|
+
console.log("");
|
|
142
150
|
}
|
|
143
151
|
|
|
144
152
|
function nextStep(s, plansN, recentPlan, repo, C) {
|
|
145
153
|
if (s.total === 0)
|
|
146
|
-
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}`;
|
|
147
155
|
if (plansN === 0)
|
|
148
|
-
return `${C.g}scriptonia plan "the thing you're about to build"${C.r} ${C.dim}— writes PLAN.md${C.r}`;
|
|
149
|
-
return `${C.g}claude "execute PLAN.md"${C.r} ${C.dim}—
|
|
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}`;
|
|
150
158
|
}
|
|
151
159
|
|
|
152
160
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -158,11 +166,11 @@ async function add(args) {
|
|
|
158
166
|
const items = args.filter((a) => !a.startsWith("--"));
|
|
159
167
|
if (items.length === 0) {
|
|
160
168
|
fail(
|
|
161
|
-
'usage: scriptonia add <file...> e.g. scriptonia add slack.txt notes.md call.vtt\n' +
|
|
162
|
-
' 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' +
|
|
163
171
|
" flags: --source slack|ticket|sales_call|voice_note|founder_note|email|notion|github\n" +
|
|
164
172
|
" --segment enterprise|mid_market|smb|free\n" +
|
|
165
|
-
" pipe: cat thread.txt | scriptonia add -",
|
|
173
|
+
" pipe: cat thread.txt | npx scriptonia add -",
|
|
166
174
|
);
|
|
167
175
|
}
|
|
168
176
|
|
|
@@ -246,12 +254,12 @@ async function add(args) {
|
|
|
246
254
|
if (cfg.inited) appendLog(cfg.dir, "add", `context.md synthesized (${(q.sources ?? []).length} cited, ${unresolved} contradictions)`);
|
|
247
255
|
}
|
|
248
256
|
} catch { /* analysis is best-effort; the signal is already ingested */ }
|
|
249
|
-
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`);
|
|
250
258
|
return;
|
|
251
259
|
}
|
|
252
260
|
|
|
253
261
|
if (created > 0) {
|
|
254
|
-
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`);
|
|
255
263
|
}
|
|
256
264
|
}
|
|
257
265
|
|
|
@@ -274,7 +282,7 @@ async function contexts() {
|
|
|
274
282
|
|
|
275
283
|
if (o.contexts_count === 0) {
|
|
276
284
|
console.log(`\n ${C.dim}No contexts yet.${C.r}`);
|
|
277
|
-
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`);
|
|
278
286
|
else console.log(` The brain is still indexing ${o.signals.total} signals — check back in a minute.\n`);
|
|
279
287
|
return;
|
|
280
288
|
}
|
|
@@ -285,7 +293,7 @@ async function contexts() {
|
|
|
285
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}`);
|
|
286
294
|
console.log(` ${C.v}${cfg.url}/contexts/${c.slug}${C.r}`);
|
|
287
295
|
}
|
|
288
|
-
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`);
|
|
289
297
|
}
|
|
290
298
|
|
|
291
299
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -299,14 +307,14 @@ async function plans() {
|
|
|
299
307
|
const o = await res.json();
|
|
300
308
|
const list = o.plans ?? [];
|
|
301
309
|
if (list.length === 0) {
|
|
302
|
-
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`);
|
|
303
311
|
return;
|
|
304
312
|
}
|
|
305
313
|
console.log(`\n ${C.b}${list.length} plan${list.length > 1 ? "s" : ""}${C.r} ${C.dim}· ${o.project}${C.r}\n`);
|
|
306
314
|
for (const p of list) {
|
|
307
315
|
console.log(` ${C.b}${p.slug}${C.r} ${C.dim}v${p.version}${C.r}`);
|
|
308
316
|
console.log(` ${C.dim}${p.issue}${C.r}`);
|
|
309
|
-
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}`);
|
|
310
318
|
}
|
|
311
319
|
console.log("");
|
|
312
320
|
}
|
|
@@ -323,8 +331,8 @@ async function comment(args) {
|
|
|
323
331
|
const text = positional.slice(1).join(" ").trim();
|
|
324
332
|
if (!target || !text) {
|
|
325
333
|
fail(
|
|
326
|
-
'usage: scriptonia comment <slug> "<your note>" comment on a context\n' +
|
|
327
|
-
' 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' +
|
|
328
336
|
' --as "Name" attribute the comment (defaults to you)',
|
|
329
337
|
);
|
|
330
338
|
}
|
|
@@ -339,7 +347,8 @@ async function comment(args) {
|
|
|
339
347
|
body: JSON.stringify({ slug, body: text, author: flags.as ?? cfg.email }),
|
|
340
348
|
});
|
|
341
349
|
const data = await res.json().catch(() => ({}));
|
|
342
|
-
if (res.status ===
|
|
350
|
+
if (res.status === 402) failCredits(data);
|
|
351
|
+
if (res.status === 404) fail(`no plan "${slug}" — generate one first: npx scriptonia plan "<issue>"`);
|
|
343
352
|
if (!res.ok) fail(`comment failed (${res.status}): ${JSON.stringify(data).slice(0, 200)}`);
|
|
344
353
|
// Approval-chain receipt: which human note produced which plan version.
|
|
345
354
|
if (cfg.inited) {
|
|
@@ -362,11 +371,12 @@ async function comment(args) {
|
|
|
362
371
|
body: JSON.stringify({ slug: target, body: text, author: flags.as ?? cfg.email }),
|
|
363
372
|
});
|
|
364
373
|
const data = await res.json().catch(() => ({}));
|
|
365
|
-
if (res.status ===
|
|
374
|
+
if (res.status === 402) failCredits(data);
|
|
375
|
+
if (res.status === 404) fail(`no context "${target}" — see \`npx scriptonia contexts\``);
|
|
366
376
|
if (!res.ok) fail(`comment failed (${res.status}): ${JSON.stringify(data)}`);
|
|
367
377
|
|
|
368
378
|
console.log(`\n ${C.g}✓${C.r} comment posted on ${C.b}${target}${C.r} ${C.dim}(v${data.from_version})${C.r}`);
|
|
369
|
-
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`);
|
|
370
380
|
}
|
|
371
381
|
|
|
372
382
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -376,7 +386,7 @@ async function query(args) {
|
|
|
376
386
|
const cfg = activeCreds();
|
|
377
387
|
const flags = parseFlags(args);
|
|
378
388
|
const q = args.filter((a) => !a.startsWith("--")).join(" ").trim();
|
|
379
|
-
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]');
|
|
380
390
|
|
|
381
391
|
const res = await fetch(`${cfg.url}/api/query`, {
|
|
382
392
|
method: "POST",
|
|
@@ -384,6 +394,7 @@ async function query(args) {
|
|
|
384
394
|
body: JSON.stringify({ query: q, window: flags.window, segment: flags.segment }),
|
|
385
395
|
});
|
|
386
396
|
const data = await res.json();
|
|
397
|
+
if (res.status === 402) failCredits(data);
|
|
387
398
|
if (!res.ok) fail(`query failed (${res.status}): ${JSON.stringify(data)}`);
|
|
388
399
|
|
|
389
400
|
const wantJson = flags.json !== undefined || !process.stdout.isTTY;
|
|
@@ -411,13 +422,66 @@ async function link(args) {
|
|
|
411
422
|
// ─────────────────────────────────────────────────────────────
|
|
412
423
|
async function status() {
|
|
413
424
|
const cfg = loadConfig();
|
|
414
|
-
if (!cfg) { console.log("not logged in — run `scriptonia login`"); return; }
|
|
425
|
+
if (!cfg) { console.log("not logged in — run `npx scriptonia login`"); return; }
|
|
426
|
+
const active = activeCreds();
|
|
427
|
+
const C = colors();
|
|
415
428
|
console.log(`signed in as ${cfg.email}`);
|
|
416
|
-
console.log(`project ${
|
|
429
|
+
console.log(`project ${active.project}`);
|
|
430
|
+
const c = await fetchCredits(active);
|
|
431
|
+
if (c && c.metered !== false) {
|
|
432
|
+
const bar = creditBar(c.used, c.cap, C);
|
|
433
|
+
console.log(`plan ${C.b}${c.plan}${C.r}`);
|
|
434
|
+
console.log(`credits ${bar} ${c.used}/${c.cap} used · resets ${c.resets_at}`);
|
|
435
|
+
if (c.available <= 0) console.log(` ${C.w}out of credits${C.r} — ${c.topup}`);
|
|
436
|
+
else if (c.available <= Math.max(2, Math.round(c.cap * 0.2))) console.log(` ${C.dim}running low — ${c.topup}${C.r}`);
|
|
437
|
+
}
|
|
417
438
|
console.log(`endpoint ${cfg.url}`);
|
|
418
439
|
console.log(`config ${CONFIG_PATH}`);
|
|
419
440
|
}
|
|
420
441
|
|
|
442
|
+
// ── credits ───────────────────────────────────────────────────
|
|
443
|
+
async function credits() {
|
|
444
|
+
const cfg = activeCreds();
|
|
445
|
+
const C = colors();
|
|
446
|
+
const c = await fetchCredits(cfg);
|
|
447
|
+
if (!c) fail("could not load credits");
|
|
448
|
+
if (c.metered === false) { console.log(`\n ${C.dim}This project isn't metered.${C.r}\n`); return; }
|
|
449
|
+
console.log(`\n ${C.b}${c.plan.toUpperCase()}${C.r} · ${cfg.project}`);
|
|
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}`);
|
|
451
|
+
console.log(`\n ${C.dim}Plans, comments & verification cost 5 · queries 1 · adding signal is free.${C.r}`);
|
|
452
|
+
if (c.available <= 0) console.log(` ${C.w}Out of credits.${C.r} ${c.topup}`);
|
|
453
|
+
else console.log(` ${C.dim}Need more? ${c.topup}${C.r}`);
|
|
454
|
+
console.log("");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function fetchCredits(cfg) {
|
|
458
|
+
try {
|
|
459
|
+
const res = await fetch(`${cfg.url}/api/credits`, { headers: { authorization: `Bearer ${cfg.token}` } });
|
|
460
|
+
if (!res.ok) return null;
|
|
461
|
+
return await res.json();
|
|
462
|
+
} catch { return null; }
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function creditBar(used, cap, C) {
|
|
466
|
+
const width = 16;
|
|
467
|
+
const frac = cap > 0 ? Math.min(1, used / cap) : 0;
|
|
468
|
+
const filled = Math.round(frac * width);
|
|
469
|
+
const color = frac >= 1 ? C.w : frac >= 0.8 ? C.w : C.g;
|
|
470
|
+
return `${color}${"█".repeat(filled)}${C.dim}${"░".repeat(width - filled)}${C.r}`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** Render an HTTP 402 credits_exhausted body and exit. Shared by plan/query/comment. */
|
|
474
|
+
function failCredits(data) {
|
|
475
|
+
const C = colors();
|
|
476
|
+
const resets = data.resets_at ? ` (resets ${data.resets_at})` : "";
|
|
477
|
+
console.error(`\n ${C.w}✕ Out of credits${C.r} — ${data.plan}: ${data.used}/${data.cap} used this month${resets}.\n`);
|
|
478
|
+
console.error(` Your signal is safe and still being collected. Plans & queries resume`);
|
|
479
|
+
console.error(` when credits do.\n`);
|
|
480
|
+
console.error(` ${C.b}Top up:${C.r} ${data.topup}`);
|
|
481
|
+
console.error(` ${C.dim}(adding signal, sync, contexts, plans list — all still free)${C.r}\n`);
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
|
|
421
485
|
async function logout() {
|
|
422
486
|
if (fs.existsSync(CONFIG_PATH)) fs.rmSync(CONFIG_PATH);
|
|
423
487
|
console.log("logged out.");
|
|
@@ -443,7 +507,7 @@ function skillMarkdown() {
|
|
|
443
507
|
name: scriptonia
|
|
444
508
|
description: >
|
|
445
509
|
BEFORE building any user-facing change — a feature, a bug fix, a refactor with
|
|
446
|
-
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
|
|
447
511
|
hyper-focused, sourced, executable plan grounded in this project's real
|
|
448
512
|
customer signal, prior decisions, and human comments. Execute PLAN.md exactly.
|
|
449
513
|
---
|
|
@@ -452,18 +516,20 @@ description: >
|
|
|
452
516
|
|
|
453
517
|
When asked to build/fix anything user-facing:
|
|
454
518
|
|
|
455
|
-
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.
|
|
456
520
|
2. Execute PLAN.md **exactly**. Respect its **Non-goals** — do not add anything
|
|
457
521
|
outside the acceptance criteria.
|
|
458
522
|
3. If a constraint is marked **UNRESOLVED**, STOP and surface it to the human.
|
|
459
|
-
4.
|
|
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.
|
|
460
526
|
|
|
461
527
|
If a plan step conflicts with what you find in the code, run
|
|
462
|
-
\`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.
|
|
463
529
|
|
|
464
|
-
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>"\`
|
|
465
531
|
(JSON: requirements with source_ids, contradictions, edge_cases). Quote a
|
|
466
|
-
customer verbatim with \`scriptonia link "<signal_id>"\`.
|
|
532
|
+
customer verbatim with \`npx scriptonia link "<signal_id>"\`.
|
|
467
533
|
|
|
468
534
|
Rule: never invent demand. If nothing in the plan/query supports a step, it
|
|
469
535
|
doesn't belong in the PR.
|
|
@@ -526,7 +592,7 @@ function loadConfig() {
|
|
|
526
592
|
}
|
|
527
593
|
function requireConfig() {
|
|
528
594
|
const cfg = loadConfig();
|
|
529
|
-
if (!cfg) fail("not logged in — run `scriptonia login`");
|
|
595
|
+
if (!cfg) fail("not logged in — run `npx scriptonia login`");
|
|
530
596
|
return cfg;
|
|
531
597
|
}
|
|
532
598
|
function saveConfig(cfg) {
|
|
@@ -600,29 +666,37 @@ function usage() {
|
|
|
600
666
|
console.log(`scriptonia — issue → executable plan for your coding agent
|
|
601
667
|
|
|
602
668
|
THE LOOP
|
|
603
|
-
1 SIGNAL scriptonia add <files> feed customer reality (text · md · vtt)
|
|
604
|
-
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
|
|
605
671
|
3 BUILD claude "execute PLAN.md" your agent ships it → PR
|
|
606
|
-
4 REFINE scriptonia comment plan/<slug> "…" a note regenerates the plan
|
|
672
|
+
4 REFINE npx scriptonia comment plan/<slug> "…" a note regenerates the plan
|
|
607
673
|
|
|
608
674
|
SETUP
|
|
609
|
-
scriptonia login sign in (browser) — wires your agent, no keys
|
|
610
|
-
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
|
|
611
677
|
|
|
612
678
|
SIGNAL & PLANS
|
|
613
|
-
scriptonia add <file...> ingest files (--source, --segment; or cat x | scriptonia add -)
|
|
614
|
-
scriptonia plan "<issue>" generate PLAN.md (--out <file>, --refresh <slug>)
|
|
615
|
-
scriptonia plans list your plans (and their slugs)
|
|
616
|
-
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
|
|
617
684
|
|
|
618
685
|
INSPECT
|
|
619
|
-
scriptonia
|
|
620
|
-
scriptonia contexts what the brain learned from your signal
|
|
621
|
-
scriptonia query "<topic>" quick lookup, no full plan (--json)
|
|
622
|
-
scriptonia link "<id>"
|
|
623
|
-
scriptonia sync write the brain to local files
|
|
624
|
-
|
|
625
|
-
|
|
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.
|
|
626
700
|
`);
|
|
627
701
|
}
|
|
628
702
|
|
|
@@ -633,7 +707,7 @@ function usage() {
|
|
|
633
707
|
/** Repo-local project when pwd is inside an initialized repo, else the global login. */
|
|
634
708
|
function activeCreds() {
|
|
635
709
|
const g = loadConfig();
|
|
636
|
-
if (!g) fail("run `scriptonia login` first");
|
|
710
|
+
if (!g) fail("run `npx scriptonia login` first");
|
|
637
711
|
const proj = findProjectForCwd();
|
|
638
712
|
if (proj) {
|
|
639
713
|
// v0.6 brains are flat (REPO.md etc. at the project dir root); older ones
|
|
@@ -672,7 +746,7 @@ function findProjectForCwd() {
|
|
|
672
746
|
async function init(args) {
|
|
673
747
|
const flags = parseFlags(args);
|
|
674
748
|
const g = loadConfig();
|
|
675
|
-
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");
|
|
676
750
|
const C = colors();
|
|
677
751
|
const cwd = process.cwd();
|
|
678
752
|
|
|
@@ -680,8 +754,8 @@ async function init(args) {
|
|
|
680
754
|
if (existing && !flags.force) {
|
|
681
755
|
writeRepoMd(existing.dir, scanRepo(cwd));
|
|
682
756
|
console.log(`\n ${C.g}✓${C.r} already initialized as ${C.b}${existing.data.name}${C.r} ${C.dim}(${existing.dir})${C.r}`);
|
|
683
|
-
console.log(` ${C.dim}re-scan from scratch:${C.r} ${C.g}scriptonia init --force${C.r}`);
|
|
684
|
-
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`);
|
|
685
759
|
return;
|
|
686
760
|
}
|
|
687
761
|
|
|
@@ -701,7 +775,7 @@ async function init(args) {
|
|
|
701
775
|
headers: { authorization: `Bearer ${g.token}`, "content-type": "application/json" },
|
|
702
776
|
body: JSON.stringify({ name }),
|
|
703
777
|
});
|
|
704
|
-
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\``);
|
|
705
779
|
const data = await res.json();
|
|
706
780
|
projectId = data.projectId; token = data.token;
|
|
707
781
|
}
|
|
@@ -736,15 +810,15 @@ async function init(args) {
|
|
|
736
810
|
console.log(` ${C.g}✓${C.r} Scanned repo (${scan.stack})`);
|
|
737
811
|
console.log(` ${C.g}✓${C.r} Wrote agent instructions ${C.dim}→ ./AGENTS.md${C.r}`);
|
|
738
812
|
console.log(`\n ${C.b}Feed it customer signal, then turn an issue into a plan:${C.r}\n`);
|
|
739
|
-
console.log(` ${C.g}scriptonia add${C.r} feedback.txt call.vtt ${C.dim}(or: scriptonia add "Issue #42: …")${C.r}`);
|
|
740
|
-
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`);
|
|
741
815
|
}
|
|
742
816
|
|
|
743
817
|
// ── sync ──────────────────────────────────────────────────────
|
|
744
818
|
async function sync() {
|
|
745
819
|
const cfg = activeCreds();
|
|
746
820
|
const C = colors();
|
|
747
|
-
if (!cfg.inited) fail("run `scriptonia init` in this repo first");
|
|
821
|
+
if (!cfg.inited) fail("run `npx scriptonia init` in this repo first");
|
|
748
822
|
const res = await fetch(`${cfg.url}/api/brain`, { headers: { authorization: `Bearer ${cfg.token}` } });
|
|
749
823
|
if (!res.ok) fail(`sync failed (${res.status})`);
|
|
750
824
|
const brain = await res.json();
|
|
@@ -785,7 +859,7 @@ async function plan(args) {
|
|
|
785
859
|
}
|
|
786
860
|
|
|
787
861
|
const issue = args.filter((a) => !a.startsWith("--")).join(" ").trim();
|
|
788
|
-
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>]');
|
|
789
863
|
|
|
790
864
|
const repoSummary = cfg.inited && fs.existsSync(path.join(cfg.brainDir, "REPO.md"))
|
|
791
865
|
? fs.readFileSync(path.join(cfg.brainDir, "REPO.md"), "utf8").slice(0, 6000)
|
|
@@ -798,10 +872,17 @@ async function plan(args) {
|
|
|
798
872
|
body: JSON.stringify({ issue, repo_summary: repoSummary }),
|
|
799
873
|
});
|
|
800
874
|
const data = await res.json();
|
|
875
|
+
if (res.status === 402) failCredits(data);
|
|
801
876
|
if (!res.ok) fail(`plan failed (${res.status}): ${JSON.stringify(data).slice(0, 200)}`);
|
|
802
877
|
writePlan(cfg, data, flags);
|
|
803
878
|
}
|
|
804
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
|
+
|
|
805
886
|
function writePlan(cfg, data, flags) {
|
|
806
887
|
const C = colors();
|
|
807
888
|
const out = flags.out ?? "PLAN.md";
|
|
@@ -817,7 +898,13 @@ function writePlan(cfg, data, flags) {
|
|
|
817
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}`);
|
|
818
899
|
console.log(` ${C.dim}${(data.sources ?? []).length} cited signal(s)${data.contradictions ? `, ${data.contradictions} unresolved contradiction(s)` : ""}${C.r}`);
|
|
819
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}`);
|
|
820
|
-
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}`);
|
|
902
|
+
if (data.credits) {
|
|
903
|
+
const cr = data.credits;
|
|
904
|
+
const low = cr.cap > 0 && cr.cap - cr.used <= Math.max(2, Math.round(cr.cap * 0.2));
|
|
905
|
+
console.log(` ${low ? C.w : C.dim}${cr.used}/${cr.cap} credits used this month${low ? " — top up: sathwik07@scriptonia.dev" : ""}${C.r}`);
|
|
906
|
+
}
|
|
907
|
+
console.log("");
|
|
821
908
|
}
|
|
822
909
|
|
|
823
910
|
// ── repo scan ─────────────────────────────────────────────────
|
|
@@ -902,7 +989,7 @@ function writeProjectMd(dir, scan, pitch) {
|
|
|
902
989
|
"",
|
|
903
990
|
`# ${scan.name}`,
|
|
904
991
|
"",
|
|
905
|
-
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`)"),
|
|
906
993
|
"",
|
|
907
994
|
`**Stack:** ${scan.stack}`,
|
|
908
995
|
scan.deps.length ? `**Key deps:** ${scan.deps.join(", ")}` : "",
|
|
@@ -911,7 +998,7 @@ function writeProjectMd(dir, scan, pitch) {
|
|
|
911
998
|
scan.readmeHead ? "## From the README\n\n" + scan.readmeHead : "",
|
|
912
999
|
"",
|
|
913
1000
|
"---",
|
|
914
|
-
"*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`.*",
|
|
915
1002
|
].filter((l) => l !== "" && l !== null).join("\n");
|
|
916
1003
|
fs.writeFileSync(path.join(dir, "PROJECT.md"), md);
|
|
917
1004
|
}
|
|
@@ -938,25 +1025,27 @@ function writeAgentsBrainMd(dir, meta, decisions) {
|
|
|
938
1025
|
"",
|
|
939
1026
|
"## 1. Identity",
|
|
940
1027
|
"",
|
|
941
|
-
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)",
|
|
942
1029
|
meta.stack ? `\nStack: ${meta.stack}` : "",
|
|
943
1030
|
"",
|
|
944
1031
|
"## 2. Workflow — non-negotiable",
|
|
945
1032
|
"",
|
|
946
|
-
'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>"`.',
|
|
947
1034
|
" It writes PLAN.md, grounded in real customer signal and checked against",
|
|
948
1035
|
" the rules below.",
|
|
949
1036
|
"2. Execute PLAN.md **exactly**. Its **Non-goals** section is a fence — do not",
|
|
950
1037
|
" build past it.",
|
|
951
1038
|
"3. Any constraint marked **⚠ UNRESOLVED** is an approval gate: STOP and get a",
|
|
952
|
-
' human comment (`scriptonia comment plan/<slug> "…"`) before writing code.',
|
|
1039
|
+
' human comment (`npx scriptonia comment plan/<slug> "…"`) before writing code.',
|
|
953
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.",
|
|
954
1043
|
"",
|
|
955
1044
|
"## 3. Non-negotiable constraints (from the decisions corpus)",
|
|
956
1045
|
"",
|
|
957
1046
|
];
|
|
958
1047
|
if (decisions.length === 0) {
|
|
959
|
-
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._");
|
|
960
1049
|
} else {
|
|
961
1050
|
for (const d of decisions) {
|
|
962
1051
|
L.push(`### ${d.title}${d.ref ? ` (${d.ref})` : ""}`, "", d.body, "");
|
|
@@ -987,7 +1076,7 @@ function renderRule(d) {
|
|
|
987
1076
|
"check: |",
|
|
988
1077
|
...String(d.body).split("\n").map((l) => ` ${l}`),
|
|
989
1078
|
"enforcement: |",
|
|
990
|
-
" Server-side during `scriptonia plan`: any issue conflicting with this rule",
|
|
1079
|
+
" Server-side during `npx scriptonia plan`: any issue conflicting with this rule",
|
|
991
1080
|
" is marked ⚠ UNRESOLVED in PLAN.md until a human comment approves the",
|
|
992
1081
|
" override (recorded in comments/plan/<slug>/).",
|
|
993
1082
|
"---",
|
|
@@ -1027,7 +1116,7 @@ function renderContextLive(issue, q, project, signalId) {
|
|
|
1027
1116
|
if (cons.length) {
|
|
1028
1117
|
L.push("## ⚠ CONTRADICTION GATE — UNRESOLVED");
|
|
1029
1118
|
for (const c of cons) L.push(`- ${c.detail} (${c.decision_ref})`);
|
|
1030
|
-
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.", "");
|
|
1031
1120
|
} else {
|
|
1032
1121
|
L.push("## Contradiction gate", "", "✓ No conflicts with prior decisions on record.", "");
|
|
1033
1122
|
}
|
|
@@ -1042,7 +1131,7 @@ function renderContextLive(issue, q, project, signalId) {
|
|
|
1042
1131
|
for (const s of q.sources ?? []) {
|
|
1043
1132
|
L.push(`- \`sig_${s.signal_id.slice(0, 8)}\` ${s.source}${s.ref ? ` — ${s.ref}` : ""} _(similarity ${s.similarity})_`);
|
|
1044
1133
|
}
|
|
1045
|
-
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.*`);
|
|
1046
1135
|
return L.join("\n");
|
|
1047
1136
|
}
|
|
1048
1137
|
|
|
@@ -1071,7 +1160,7 @@ function buildRepoMd(scan) {
|
|
|
1071
1160
|
scan.tree,
|
|
1072
1161
|
"```",
|
|
1073
1162
|
"",
|
|
1074
|
-
"*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`.*",
|
|
1075
1164
|
].filter(Boolean).join("\n");
|
|
1076
1165
|
}
|
|
1077
1166
|
|
|
@@ -1093,13 +1182,15 @@ function injectAgentsMd(cwd) {
|
|
|
1093
1182
|
This repo has a product brain (customer signal + prior decisions). Before building
|
|
1094
1183
|
any user-facing change:
|
|
1095
1184
|
|
|
1096
|
-
- \`scriptonia plan "<the issue>"\` → writes **PLAN.md** here. Execute PLAN.md
|
|
1185
|
+
- \`npx scriptonia plan "<the issue>"\` → writes **PLAN.md** here. Execute PLAN.md
|
|
1097
1186
|
exactly; respect its **Non-goals**; cite its \`sources\` ids in your PR.
|
|
1098
|
-
- \`scriptonia query "<topic>"\` → JSON: what customers asked for, contradictions,
|
|
1187
|
+
- \`npx scriptonia query "<topic>"\` → JSON: what customers asked for, contradictions,
|
|
1099
1188
|
edge cases.
|
|
1100
1189
|
- If a plan constraint is **UNRESOLVED**, stop and ask the human. If a step
|
|
1101
|
-
conflicts with the code, \`scriptonia comment plan/<slug> "<what you found>"\`
|
|
1190
|
+
conflicts with the code, \`npx scriptonia comment plan/<slug> "<what you found>"\`
|
|
1102
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.
|
|
1103
1194
|
${END}`;
|
|
1104
1195
|
|
|
1105
1196
|
let existing = "";
|
package/bin/verify.mjs
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const RANK = { PASS: 0, WARN: 1, BLOCK: 2 };
|
|
7
|
+
const CHECK_SCRIPTS = ["test", "build", "lint", "typecheck", "type-check"];
|
|
8
|
+
|
|
9
|
+
export async function runVerification(args, { cfg, cwd = process.cwd(), fetchImpl = fetch } = {}) {
|
|
10
|
+
if (!cfg?.url || !cfg?.token) throw new Error("run `npx scriptonia login` first");
|
|
11
|
+
const flags = parseFlags(args);
|
|
12
|
+
const planPath = path.resolve(cwd, flags.plan ?? "PLAN.md");
|
|
13
|
+
if (!fs.existsSync(planPath)) throw new Error(`PLAN.md not found at ${planPath} (use --plan <file>)`);
|
|
14
|
+
|
|
15
|
+
const plan = fs.readFileSync(planPath, "utf8");
|
|
16
|
+
const meta = parsePlanMetadata(plan, planPath);
|
|
17
|
+
const { diff, files, source } = collectDiff(cwd, flags);
|
|
18
|
+
if (!diff.trim()) throw new Error("no git diff to verify (use --base <ref> or --diff <file>)");
|
|
19
|
+
|
|
20
|
+
const diffForJudge = diff.length > 290_000
|
|
21
|
+
? `${diff.slice(0, 290_000)}\n\n[diff truncated by CLI; verdicts requiring omitted evidence must WARN]`
|
|
22
|
+
: diff;
|
|
23
|
+
const semanticPromise = fetchImpl(`${cfg.url}/api/verify`, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { authorization: `Bearer ${cfg.token}`, "content-type": "application/json" },
|
|
26
|
+
body: JSON.stringify({ plan, diff: diffForJudge }),
|
|
27
|
+
});
|
|
28
|
+
const repositoryChecks = flags["skip-checks"] ? skippedRepositoryGate() : runRepositoryChecks(cwd, files);
|
|
29
|
+
const unresolvedConstraints = buildUnresolvedGate(plan);
|
|
30
|
+
const testCoverage = buildTestCoverageGate(files);
|
|
31
|
+
|
|
32
|
+
const response = await semanticPromise;
|
|
33
|
+
const semantic = await response.json().catch(() => ({}));
|
|
34
|
+
if (!response.ok) throw new Error(`verification service failed (${response.status}): ${JSON.stringify(semantic).slice(0, 240)}`);
|
|
35
|
+
for (const name of ["acceptance_criteria", "non_goals", "product_decisions"]) {
|
|
36
|
+
const result = semantic[name];
|
|
37
|
+
if (!result || !(result.verdict in RANK) || !Array.isArray(result.checks)) {
|
|
38
|
+
throw new Error(`verification service returned an invalid ${name} gate`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const gates = {
|
|
43
|
+
acceptance_criteria: semantic.acceptance_criteria,
|
|
44
|
+
non_goals: semantic.non_goals,
|
|
45
|
+
product_decisions: semantic.product_decisions,
|
|
46
|
+
unresolved_constraints: unresolvedConstraints,
|
|
47
|
+
repository_checks: repositoryChecks,
|
|
48
|
+
test_coverage: testCoverage,
|
|
49
|
+
};
|
|
50
|
+
const verdict = worstVerdict(Object.values(gates).map((gate) => gate.verdict));
|
|
51
|
+
const evalPath = path.resolve(cwd, flags.out ?? path.join(".scriptonia", "evals", `${meta.slug}.yaml`));
|
|
52
|
+
fs.mkdirSync(path.dirname(evalPath), { recursive: true });
|
|
53
|
+
fs.writeFileSync(evalPath, renderEvalYaml({
|
|
54
|
+
id: meta.slug,
|
|
55
|
+
plan: path.relative(cwd, planPath) || path.basename(planPath),
|
|
56
|
+
planVersion: meta.version,
|
|
57
|
+
diffSource: source,
|
|
58
|
+
diffSha256: createHash("sha256").update(diff).digest("hex"),
|
|
59
|
+
verdict,
|
|
60
|
+
gates,
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
printResult({ verdict, gates, evalPath, cwd });
|
|
64
|
+
return { verdict, exitCode: verdict === "BLOCK" ? 1 : 0, evalPath, gates };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function parsePlanMetadata(plan, planPath = "PLAN.md") {
|
|
68
|
+
const frontmatter = plan.match(/^---\s*\n([\s\S]*?)\n---/m)?.[1] ?? "";
|
|
69
|
+
const value = (key) => frontmatter.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]?.trim().replace(/^['"]|['"]$/g, "");
|
|
70
|
+
const slug = value("plan") || slugify(plan.match(/^#\s+(.+)$/m)?.[1] || path.basename(planPath, path.extname(planPath)));
|
|
71
|
+
return { slug: slugify(slug), version: Number(value("version") || 1) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function buildUnresolvedGate(plan) {
|
|
75
|
+
const matches = plan.split("\n").filter((line) => /(?:⚠\s*)?UNRESOLVED/i.test(line));
|
|
76
|
+
const blocked = matches.length > 0;
|
|
77
|
+
return gate(blocked ? "BLOCK" : "PASS", {
|
|
78
|
+
evidence: blocked ? matches.slice(0, 6) : ["No UNRESOLVED marker appears in PLAN.md."],
|
|
79
|
+
plan_criterion: "Every unresolved constraint requires human approval before implementation.",
|
|
80
|
+
citation: decisionCitation(matches.join(" ")),
|
|
81
|
+
reason: blocked ? "PLAN.md still contains an unresolved approval gate." : "No unresolved approval gate remains in the approved plan.",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function buildTestCoverageGate(files) {
|
|
86
|
+
const tests = files.filter(isTestFile);
|
|
87
|
+
const behavior = files.filter((file) => isSourceFile(file) && !isTestFile(file));
|
|
88
|
+
if (tests.length) return gate("PASS", {
|
|
89
|
+
evidence: tests,
|
|
90
|
+
plan_criterion: "Add or update tests for changed product behavior.",
|
|
91
|
+
citation: "PLAN.md test plan",
|
|
92
|
+
reason: "The diff includes test changes that a reviewer can inspect.",
|
|
93
|
+
});
|
|
94
|
+
if (behavior.length) return gate("BLOCK", {
|
|
95
|
+
evidence: behavior.slice(0, 8),
|
|
96
|
+
plan_criterion: "Add or update tests for changed product behavior.",
|
|
97
|
+
citation: "PLAN.md test plan",
|
|
98
|
+
reason: "Product code changed without any test file added or updated.",
|
|
99
|
+
});
|
|
100
|
+
return gate("PASS", {
|
|
101
|
+
evidence: files.length ? files.slice(0, 8) : ["No changed source files."],
|
|
102
|
+
plan_criterion: "Add or update tests for changed product behavior.",
|
|
103
|
+
citation: "PLAN.md test plan",
|
|
104
|
+
reason: "The diff contains no product-code change requiring test coverage.",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function runRepositoryChecks(cwd, files) {
|
|
109
|
+
const checks = [];
|
|
110
|
+
for (const root of relevantPackageRoots(cwd, files)) {
|
|
111
|
+
let pkg;
|
|
112
|
+
try { pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")); } catch { continue; }
|
|
113
|
+
const manager = packageManager(root);
|
|
114
|
+
const seen = new Set();
|
|
115
|
+
for (const name of CHECK_SCRIPTS) {
|
|
116
|
+
if (!pkg.scripts?.[name] || seen.has(pkg.scripts[name])) continue;
|
|
117
|
+
seen.add(pkg.scripts[name]);
|
|
118
|
+
const command = manager === "yarn" ? ["yarn", name] : manager === "pnpm" ? ["pnpm", "run", name] : manager === "bun" ? ["bun", "run", name] : ["npm", "run", name];
|
|
119
|
+
const result = spawnSync(command[0], command.slice(1), { cwd: root, encoding: "utf8", timeout: 300_000, maxBuffer: 2_000_000, env: { ...process.env, CI: "1" } });
|
|
120
|
+
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
|
|
121
|
+
checks.push({ name, command: command.join(" "), root: path.relative(cwd, root) || ".", ok: result.status === 0, output: output.slice(-1200) || `(exit ${result.status ?? "unknown"})` });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!checks.length) return gate("WARN", {
|
|
125
|
+
evidence: ["No test, build, lint, or type-check script was found for the changed packages."],
|
|
126
|
+
plan_criterion: "Run the repository's existing quality checks.",
|
|
127
|
+
citation: "repository package scripts",
|
|
128
|
+
reason: "There were no recognized repository checks to run automatically.",
|
|
129
|
+
});
|
|
130
|
+
const failed = checks.filter((check) => !check.ok);
|
|
131
|
+
return {
|
|
132
|
+
verdict: failed.length ? "BLOCK" : "PASS",
|
|
133
|
+
checks: checks.map((check) => ({
|
|
134
|
+
verdict: check.ok ? "PASS" : "BLOCK",
|
|
135
|
+
evidence: [`${check.root}: ${check.command}`, check.output],
|
|
136
|
+
plan_criterion: "Run the repository's existing quality checks.",
|
|
137
|
+
citation: "repository package scripts",
|
|
138
|
+
reason: check.ok ? `${check.name} completed successfully.` : `${check.name} failed and must be fixed before merge.`,
|
|
139
|
+
})),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function skippedRepositoryGate() {
|
|
144
|
+
return gate("WARN", {
|
|
145
|
+
evidence: ["Repository checks skipped with --skip-checks."],
|
|
146
|
+
plan_criterion: "Run the repository's existing quality checks.",
|
|
147
|
+
citation: "CLI flag",
|
|
148
|
+
reason: "CI should run the repository checks separately before merge.",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function relevantPackageRoots(cwd, files) {
|
|
153
|
+
const candidates = [];
|
|
154
|
+
if (fs.existsSync(path.join(cwd, "package.json"))) candidates.push(cwd);
|
|
155
|
+
for (const entry of fs.readdirSync(cwd, { withFileTypes: true })) {
|
|
156
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && fs.existsSync(path.join(cwd, entry.name, "package.json"))) candidates.push(path.join(cwd, entry.name));
|
|
157
|
+
}
|
|
158
|
+
const relevant = candidates.filter((root) => {
|
|
159
|
+
const prefix = path.relative(cwd, root);
|
|
160
|
+
return !prefix || files.some((file) => file === prefix || file.startsWith(prefix + "/"));
|
|
161
|
+
});
|
|
162
|
+
return relevant.length ? relevant : candidates.length === 1 ? candidates : [];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function packageManager(root) {
|
|
166
|
+
if (fs.existsSync(path.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
167
|
+
if (fs.existsSync(path.join(root, "yarn.lock"))) return "yarn";
|
|
168
|
+
if (fs.existsSync(path.join(root, "bun.lockb")) || fs.existsSync(path.join(root, "bun.lock"))) return "bun";
|
|
169
|
+
return "npm";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function collectDiff(cwd, flags) {
|
|
173
|
+
if (flags.diff) {
|
|
174
|
+
const file = path.resolve(cwd, flags.diff);
|
|
175
|
+
const diff = fs.readFileSync(file, "utf8");
|
|
176
|
+
return { diff, files: diffFiles(diff), source: `file:${path.relative(cwd, file)}` };
|
|
177
|
+
}
|
|
178
|
+
const base = flags.base || (process.env.GITHUB_BASE_REF ? `origin/${process.env.GITHUB_BASE_REF}` : null);
|
|
179
|
+
const range = base ? `${base}...HEAD` : "HEAD";
|
|
180
|
+
const result = spawnSync("git", ["diff", range, "--no-ext-diff", "--unified=40"], { cwd, encoding: "utf8", maxBuffer: 20_000_000 });
|
|
181
|
+
if (result.status !== 0) throw new Error(`could not read git diff for ${range}: ${(result.stderr || "").trim()}`);
|
|
182
|
+
let diff = result.stdout || "";
|
|
183
|
+
if (!base) diff += untrackedDiff(cwd);
|
|
184
|
+
return { diff, files: diffFiles(diff), source: `git:${range}` };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function untrackedDiff(cwd) {
|
|
188
|
+
const status = spawnSync("git", ["status", "--porcelain", "--untracked-files=all"], { cwd, encoding: "utf8" });
|
|
189
|
+
if (status.status !== 0) return "";
|
|
190
|
+
const files = status.stdout.split("\n").filter((line) => line.startsWith("?? ")).map((line) => line.slice(3));
|
|
191
|
+
const chunks = [];
|
|
192
|
+
for (const file of files) {
|
|
193
|
+
const full = path.join(cwd, file);
|
|
194
|
+
try {
|
|
195
|
+
const stat = fs.statSync(full);
|
|
196
|
+
if (!stat.isFile() || stat.size > 500_000) continue;
|
|
197
|
+
const body = fs.readFileSync(full, "utf8");
|
|
198
|
+
chunks.push(`\ndiff --git a/${file} b/${file}\nnew file mode 100644\n--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${body.split("\n").length} @@\n${body.split("\n").map((line) => `+${line}`).join("\n")}\n`);
|
|
199
|
+
} catch { /* ignore unreadable/binary untracked files */ }
|
|
200
|
+
}
|
|
201
|
+
return chunks.join("");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function diffFiles(diff) {
|
|
205
|
+
return [...new Set([...diff.matchAll(/^diff --git a\/(.+?) b\/(.+)$/gm)].map((match) => match[2]))];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isTestFile(file) {
|
|
209
|
+
return /(^|\/)(?:__tests__|tests?|spec)(?:\/|$)|\.(?:test|spec)\.[^.]+$/i.test(file);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function isSourceFile(file) {
|
|
213
|
+
return /\.(?:[cm]?[jt]sx?|py|go|rs|java|rb|php|swift|kt)$/i.test(file) && !/(?:^|\/)(?:scripts?|migrations?|drizzle)(?:\/|$)/i.test(file);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function gate(verdict, check) {
|
|
217
|
+
return { verdict, checks: [{ verdict, ...check }] };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function worstVerdict(verdicts) {
|
|
221
|
+
return verdicts.reduce((worst, verdict) => (RANK[verdict] > RANK[worst] ? verdict : worst), "PASS");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function decisionCitation(text) {
|
|
225
|
+
return text.match(/(?:decision|dec)[-_:/ ]?[a-z0-9_-]+/i)?.[0] || "PLAN.md constraint";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function renderEvalYaml(data) {
|
|
229
|
+
const q = (value) => JSON.stringify(String(value ?? ""));
|
|
230
|
+
const lines = ["version: 1", `id: ${q(data.id)}`, `plan: ${q(data.plan)}`, `plan_version: ${data.planVersion}`, `diff_source: ${q(data.diffSource)}`, `diff_sha256: ${q(data.diffSha256)}`, `generated_at: ${q(new Date().toISOString())}`, `verdict: ${data.verdict}`, "gates:"];
|
|
231
|
+
for (const [name, gateResult] of Object.entries(data.gates)) {
|
|
232
|
+
lines.push(` ${name}:`, ` verdict: ${gateResult.verdict}`, " checks:");
|
|
233
|
+
for (const check of gateResult.checks ?? []) {
|
|
234
|
+
lines.push(` - verdict: ${check.verdict}`, ` plan_criterion: ${q(check.plan_criterion)}`, ` citation: ${q(check.citation)}`, ` reason: ${q(check.reason)}`, " evidence:");
|
|
235
|
+
for (const evidence of check.evidence ?? []) lines.push(` - ${q(String(evidence).slice(0, 1600))}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return lines.join("\n") + "\n";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function printResult({ verdict, gates, evalPath, cwd }) {
|
|
242
|
+
const icon = verdict === "PASS" ? "✓" : verdict === "WARN" ? "!" : "✕";
|
|
243
|
+
console.log(`\n ${icon} SCRIPTONIA VERIFY · ${verdict}\n`);
|
|
244
|
+
for (const [name, result] of Object.entries(gates)) {
|
|
245
|
+
console.log(` ${result.verdict.padEnd(5)} ${name.replaceAll("_", " ")}`);
|
|
246
|
+
for (const check of (result.checks ?? []).filter((item) => item.verdict !== "PASS").slice(0, 3)) console.log(` ${check.reason}`);
|
|
247
|
+
}
|
|
248
|
+
console.log(`\n case ${path.relative(cwd, evalPath)}`);
|
|
249
|
+
console.log(` exit ${verdict === "BLOCK" ? 1 : 0}\n`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function parseFlags(args) {
|
|
253
|
+
const out = {};
|
|
254
|
+
for (let i = 0; i < args.length; i++) {
|
|
255
|
+
if (!args[i].startsWith("--")) continue;
|
|
256
|
+
const [name, inline] = args[i].slice(2).split(/=(.*)/s);
|
|
257
|
+
if (inline !== undefined) out[name] = inline;
|
|
258
|
+
else if (args[i + 1] && !args[i + 1].startsWith("--")) out[name] = args[++i];
|
|
259
|
+
else out[name] = true;
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function slugify(value) {
|
|
265
|
+
return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "").slice(0, 80) || "plan";
|
|
266
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scriptonia",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Turn customer signal into
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Turn customer signal into plans and merge-blocking verification for AI-written PRs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"scriptonia": "bin/scriptonia.mjs"
|
|
8
8
|
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test"
|
|
11
|
+
},
|
|
9
12
|
"files": [
|
|
10
13
|
"bin",
|
|
11
14
|
"README.md"
|