scriptonia 0.5.0 → 0.6.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/bin/scriptonia.mjs +266 -35
- package/package.json +1 -1
package/bin/scriptonia.mjs
CHANGED
|
@@ -155,10 +155,11 @@ function nextStep(s, plansN, recentPlan, repo, C) {
|
|
|
155
155
|
async function add(args) {
|
|
156
156
|
const cfg = activeCreds();
|
|
157
157
|
const flags = parseFlags(args);
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
158
|
+
const items = args.filter((a) => !a.startsWith("--"));
|
|
159
|
+
if (items.length === 0) {
|
|
160
160
|
fail(
|
|
161
|
-
'usage: scriptonia add <file...>
|
|
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' +
|
|
162
163
|
" flags: --source slack|ticket|sales_call|voice_note|founder_note|email|notion|github\n" +
|
|
163
164
|
" --segment enterprise|mid_market|smb|free\n" +
|
|
164
165
|
" pipe: cat thread.txt | scriptonia add -",
|
|
@@ -166,21 +167,30 @@ async function add(args) {
|
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
const C = colors();
|
|
170
|
+
// A single non-file argument with whitespace is inline signal text (an issue,
|
|
171
|
+
// a pasted message). A bare word that isn't a file stays a "not found" error
|
|
172
|
+
// so typo'd filenames don't get silently ingested as one-word signals.
|
|
173
|
+
const inline = items.length === 1 && items[0] !== "-" && !fs.existsSync(items[0]) && /\s/.test(items[0]);
|
|
169
174
|
let created = 0, dup = 0, failed = 0;
|
|
175
|
+
let inlineSignalId = null;
|
|
170
176
|
|
|
171
|
-
for (const
|
|
177
|
+
for (const item of inline ? [items[0]] : items) {
|
|
172
178
|
let body, ref, srcHint;
|
|
173
|
-
if (
|
|
179
|
+
if (inline) {
|
|
180
|
+
body = item;
|
|
181
|
+
ref = null;
|
|
182
|
+
srcHint = flags.source ?? "ticket";
|
|
183
|
+
} else if (item === "-") {
|
|
174
184
|
body = fs.readFileSync(0, "utf8"); // stdin
|
|
175
185
|
ref = null;
|
|
176
186
|
srcHint = "founder_note";
|
|
177
187
|
} else {
|
|
178
|
-
if (!fs.existsSync(
|
|
179
|
-
body = fs.readFileSync(
|
|
180
|
-
ref = `upload/${path.basename(
|
|
181
|
-
srcHint = detectSource(
|
|
188
|
+
if (!fs.existsSync(item)) { console.log(` ${C.w}skip${C.r} ${item} (not found)`); failed++; continue; }
|
|
189
|
+
body = fs.readFileSync(item, "utf8");
|
|
190
|
+
ref = `upload/${path.basename(item)}`;
|
|
191
|
+
srcHint = detectSource(item);
|
|
182
192
|
}
|
|
183
|
-
if (!body.trim()) { console.log(` ${C.w}skip${C.r} ${
|
|
193
|
+
if (!body.trim()) { console.log(` ${C.w}skip${C.r} ${item} (empty)`); continue; }
|
|
184
194
|
|
|
185
195
|
const res = await fetch(`${cfg.url}/api/ingest`, {
|
|
186
196
|
method: "POST",
|
|
@@ -188,19 +198,60 @@ async function add(args) {
|
|
|
188
198
|
body: JSON.stringify({
|
|
189
199
|
source: flags.source ?? srcHint,
|
|
190
200
|
body,
|
|
191
|
-
|
|
201
|
+
// omit when absent — the API's schema rejects an explicit null
|
|
202
|
+
externalRef: ref ?? undefined,
|
|
192
203
|
segment: flags.segment,
|
|
193
204
|
}),
|
|
194
205
|
});
|
|
195
206
|
const data = await res.json().catch(() => ({}));
|
|
196
|
-
if (!res.ok) { console.log(` ${C.w}fail${C.r} ${
|
|
197
|
-
|
|
198
|
-
|
|
207
|
+
if (!res.ok) { console.log(` ${C.w}fail${C.r} ${inline ? "inline signal" : item} (${res.status})`); failed++; continue; }
|
|
208
|
+
const label = inline ? `"${body.slice(0, 56)}${body.length > 56 ? "…" : ""}"` : item;
|
|
209
|
+
if (data.status === "duplicate") { console.log(` ${C.dim}dup ${C.r} ${label}`); dup++; }
|
|
210
|
+
else { console.log(` ${C.g}✓${C.r} ${label} ${C.dim}→ ${flags.source ?? srcHint} · sig_${(data.signalId ?? "").slice(0, 8)}${C.r}`); created++; }
|
|
211
|
+
if (inline) inlineSignalId = data.signalId ?? null;
|
|
212
|
+
|
|
213
|
+
// Local receipt so an agent reading the brain sees what signal exists.
|
|
214
|
+
if (cfg.inited && data.signalId) {
|
|
215
|
+
writeSignalReceipt(cfg.dir, {
|
|
216
|
+
id: data.signalId,
|
|
217
|
+
source: flags.source ?? srcHint,
|
|
218
|
+
externalRef: ref,
|
|
219
|
+
segment: flags.segment ?? null,
|
|
220
|
+
preview: body.slice(0, 240),
|
|
221
|
+
status: data.status === "duplicate" ? "duplicate" : "queued_for_embedding",
|
|
222
|
+
});
|
|
223
|
+
}
|
|
199
224
|
}
|
|
200
225
|
|
|
226
|
+
if (cfg.inited) appendLog(cfg.dir, "add", `${created} added, ${dup} dup, ${failed} failed${inline ? " (inline)" : ""}`);
|
|
201
227
|
console.log(`\n ${created} added${dup ? `, ${dup} already there` : ""}${failed ? `, ${failed} failed` : ""}.`);
|
|
228
|
+
|
|
229
|
+
// Inline signal = an issue. Analyze it against the brain now (the existing
|
|
230
|
+
// embedded signal + decisions) and materialize context.md.
|
|
231
|
+
if (inline && (created > 0 || dup > 0)) {
|
|
232
|
+
console.log(` ${C.dim}Analyzing against the brain — retrieved signal · prior decisions… (~10s)${C.r}`);
|
|
233
|
+
try {
|
|
234
|
+
const qres = await fetch(`${cfg.url}/api/query`, {
|
|
235
|
+
method: "POST",
|
|
236
|
+
headers: { authorization: `Bearer ${cfg.token}`, "content-type": "application/json" },
|
|
237
|
+
body: JSON.stringify({ query: items[0], window: "all" }),
|
|
238
|
+
});
|
|
239
|
+
const q = await qres.json();
|
|
240
|
+
if (qres.ok) {
|
|
241
|
+
const md = renderContextLive(items[0], q, cfg.project, inlineSignalId);
|
|
242
|
+
if (cfg.inited) fs.writeFileSync(path.join(cfg.dir, "context.md"), md);
|
|
243
|
+
const unresolved = (q.contradictions ?? []).length;
|
|
244
|
+
console.log(`\n ${C.g}✓${C.r} context synthesized${cfg.inited ? ` → ${C.dim}${path.join(cfg.dir, "context.md")}${C.r}` : ""}`);
|
|
245
|
+
console.log(` ${C.dim}${(q.sources ?? []).length} signal(s) cited${unresolved ? ` · ${C.r}${C.w}⚠ ${unresolved} contradiction(s) with prior decisions — gate is UNRESOLVED${C.r}` : ""}${C.r}`);
|
|
246
|
+
if (cfg.inited) appendLog(cfg.dir, "add", `context.md synthesized (${(q.sources ?? []).length} cited, ${unresolved} contradictions)`);
|
|
247
|
+
}
|
|
248
|
+
} 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`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
202
253
|
if (created > 0) {
|
|
203
|
-
console.log(` ${C.dim}The brain is enriching them now.
|
|
254
|
+
console.log(` ${C.dim}The brain is enriching them now. Then:${C.r} ${C.g}scriptonia plan "<issue>"${C.r}\n`);
|
|
204
255
|
}
|
|
205
256
|
}
|
|
206
257
|
|
|
@@ -290,6 +341,16 @@ async function comment(args) {
|
|
|
290
341
|
const data = await res.json().catch(() => ({}));
|
|
291
342
|
if (res.status === 404) fail(`no plan "${slug}" — generate one first: scriptonia plan "<issue>"`);
|
|
292
343
|
if (!res.ok) fail(`comment failed (${res.status}): ${JSON.stringify(data).slice(0, 200)}`);
|
|
344
|
+
// Approval-chain receipt: which human note produced which plan version.
|
|
345
|
+
if (cfg.inited) {
|
|
346
|
+
const cdir = path.join(cfg.dir, "comments", "plan", slug);
|
|
347
|
+
fs.mkdirSync(cdir, { recursive: true });
|
|
348
|
+
fs.writeFileSync(
|
|
349
|
+
path.join(cdir, `v${data.version}.json`),
|
|
350
|
+
JSON.stringify({ author: flags.as ?? cfg.email, body: text, applied_in_version: data.version, at: new Date().toISOString() }, null, 2),
|
|
351
|
+
);
|
|
352
|
+
appendLog(cfg.dir, "comment", `plan/${slug} — regenerated to v${data.version}`);
|
|
353
|
+
}
|
|
293
354
|
writePlan(cfg, data, flags);
|
|
294
355
|
return;
|
|
295
356
|
}
|
|
@@ -575,10 +636,14 @@ function activeCreds() {
|
|
|
575
636
|
if (!g) fail("run `scriptonia login` first");
|
|
576
637
|
const proj = findProjectForCwd();
|
|
577
638
|
if (proj) {
|
|
639
|
+
// v0.6 brains are flat (REPO.md etc. at the project dir root); older ones
|
|
640
|
+
// used a brain/ subfolder — keep reading those until re-init.
|
|
641
|
+
const legacy = !fs.existsSync(path.join(proj.dir, "REPO.md")) && fs.existsSync(path.join(proj.dir, "brain"));
|
|
578
642
|
return {
|
|
579
643
|
url: g.url, email: g.email,
|
|
580
644
|
token: proj.data.token, project: proj.data.name, projectId: proj.data.projectId,
|
|
581
|
-
dir: proj.dir, brainDir: path.join(proj.dir, "brain")
|
|
645
|
+
dir: proj.dir, brainDir: legacy ? path.join(proj.dir, "brain") : proj.dir,
|
|
646
|
+
plansDir: path.join(proj.dir, "plans"),
|
|
582
647
|
repoPath: proj.data.repoPath, inited: true,
|
|
583
648
|
};
|
|
584
649
|
}
|
|
@@ -643,8 +708,16 @@ async function init(args) {
|
|
|
643
708
|
|
|
644
709
|
const pitch = flags.about ?? (await ask(` In one or two sentences — what is ${name} and who is it for?\n > `));
|
|
645
710
|
|
|
646
|
-
|
|
647
|
-
|
|
711
|
+
// Brain structure (v0.6, flat): everything an agent needs to be grounded,
|
|
712
|
+
// as plain files. cache/ + embeddings/ intentionally absent — vectors live
|
|
713
|
+
// server-side (pgvector); writing fake index files here would be theater.
|
|
714
|
+
for (const sub of ["signals", "rules", "contexts", "plans", "comments/plan", "logs"]) {
|
|
715
|
+
fs.mkdirSync(path.join(dir, sub), { recursive: true });
|
|
716
|
+
}
|
|
717
|
+
// Migrate a legacy brain/ subfolder layout if present.
|
|
718
|
+
const legacyBrain = path.join(dir, "brain");
|
|
719
|
+
if (fs.existsSync(legacyBrain)) fs.rmSync(legacyBrain, { recursive: true, force: true });
|
|
720
|
+
|
|
648
721
|
fs.writeFileSync(
|
|
649
722
|
path.join(dir, "project.json"),
|
|
650
723
|
JSON.stringify({ projectId, token, name, slug, repoPath, pitch: pitch || null, createdAt: new Date().toISOString() }, null, 2),
|
|
@@ -652,14 +725,18 @@ async function init(args) {
|
|
|
652
725
|
);
|
|
653
726
|
writeProjectMd(dir, scan, pitch);
|
|
654
727
|
writeRepoMd(dir, scan);
|
|
728
|
+
writeAgentsBrainMd(dir, { name, pitch, stack: scan.stack }, []);
|
|
729
|
+
fs.writeFileSync(path.join(dir, ".gitignore"), "logs/\nproject.json\n");
|
|
655
730
|
injectAgentsMd(cwd);
|
|
656
731
|
installSkill();
|
|
732
|
+
appendLog(dir, "init", `Initialized brain for ${name} (${repoPath}) — stack: ${scan.stack}`);
|
|
657
733
|
|
|
658
734
|
console.log(`\n ${C.g}✓${C.r} Brain created for ${C.b}${name}${C.r} ${C.dim}→ ${dir}${C.r}`);
|
|
659
|
-
console.log(`
|
|
735
|
+
console.log(` ${C.dim}AGENTS.md · PROJECT.md · REPO.md · signals/ · rules/ · contexts/ · plans/ · comments/ · logs/${C.r}`);
|
|
736
|
+
console.log(` ${C.g}✓${C.r} Scanned repo (${scan.stack})`);
|
|
660
737
|
console.log(` ${C.g}✓${C.r} Wrote agent instructions ${C.dim}→ ./AGENTS.md${C.r}`);
|
|
661
738
|
console.log(`\n ${C.b}Feed it customer signal, then turn an issue into a plan:${C.r}\n`);
|
|
662
|
-
console.log(` ${C.g}scriptonia add${C.r} feedback.txt call.vtt
|
|
739
|
+
console.log(` ${C.g}scriptonia add${C.r} feedback.txt call.vtt ${C.dim}(or: scriptonia add "Issue #42: …")${C.r}`);
|
|
663
740
|
console.log(` ${C.g}scriptonia plan${C.r} "the issue you want to build"\n`);
|
|
664
741
|
}
|
|
665
742
|
|
|
@@ -676,9 +753,22 @@ async function sync() {
|
|
|
676
753
|
fs.mkdirSync(cdir, { recursive: true });
|
|
677
754
|
for (const f of fs.readdirSync(cdir)) if (f.endsWith(".md")) fs.rmSync(path.join(cdir, f));
|
|
678
755
|
for (const c of brain.contexts) fs.writeFileSync(path.join(cdir, `${c.slug}.md`), renderContextFile(c));
|
|
679
|
-
fs.writeFileSync(path.join(cfg.brainDir, "DECISIONS.md"), renderDecisions(brain.decisions));
|
|
680
756
|
|
|
681
|
-
|
|
757
|
+
// Decisions → machine-checkable rules/. The server enforces these during
|
|
758
|
+
// `plan` (conflicts come back ⚠ UNRESOLVED); the local copies ground agents.
|
|
759
|
+
const rdir = path.join(cfg.brainDir, "rules");
|
|
760
|
+
fs.mkdirSync(rdir, { recursive: true });
|
|
761
|
+
for (const f of fs.readdirSync(rdir)) if (f.endsWith(".rule")) fs.rmSync(path.join(rdir, f));
|
|
762
|
+
for (const d of brain.decisions) {
|
|
763
|
+
fs.writeFileSync(path.join(rdir, `${slugify(d.title)}.rule`), renderRule(d));
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Refresh the brain's master contract with the live constraint set.
|
|
767
|
+
const pj = readProjectJson(cfg.dir);
|
|
768
|
+
writeAgentsBrainMd(cfg.brainDir, { name: cfg.project, pitch: pj?.pitch ?? "", stack: null }, brain.decisions);
|
|
769
|
+
|
|
770
|
+
appendLog(cfg.dir, "sync", `${brain.contexts.length} contexts, ${brain.decisions.length} decisions → rules/`);
|
|
771
|
+
console.log(` ${C.g}✓${C.r} synced ${brain.contexts.length} context(s) → contexts/, ${brain.decisions.length} decision(s) → rules/ ${C.dim}(${cfg.brainDir})${C.r}`);
|
|
682
772
|
}
|
|
683
773
|
|
|
684
774
|
// ── plan (the money command) ──────────────────────────────────
|
|
@@ -719,6 +809,10 @@ function writePlan(cfg, data, flags) {
|
|
|
719
809
|
if (cfg.inited) {
|
|
720
810
|
fs.mkdirSync(cfg.plansDir, { recursive: true });
|
|
721
811
|
fs.writeFileSync(path.join(cfg.plansDir, `${data.slug}.md`), data.markdown);
|
|
812
|
+
// Live copy in the brain, so an agent grounded on the brain dir sees the
|
|
813
|
+
// current plan without knowing the slug.
|
|
814
|
+
fs.writeFileSync(path.join(cfg.brainDir, "plan.md"), data.markdown);
|
|
815
|
+
appendLog(cfg.dir, "plan", `${data.slug} v${data.version} — ${(data.sources ?? []).length} sources, ${data.contradictions ?? 0} unresolved`);
|
|
722
816
|
}
|
|
723
817
|
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}`);
|
|
724
818
|
console.log(` ${C.dim}${(data.sources ?? []).length} cited signal(s)${data.contradictions ? `, ${data.contradictions} unresolved contradiction(s)` : ""}${C.r}`);
|
|
@@ -799,26 +893,170 @@ function gitRemoteName(cwd) {
|
|
|
799
893
|
// ── brain file writers ────────────────────────────────────────
|
|
800
894
|
function writeProjectMd(dir, scan, pitch) {
|
|
801
895
|
const md = [
|
|
896
|
+
"---",
|
|
897
|
+
`project_name: ${JSON.stringify(scan.name)}`,
|
|
898
|
+
`stack: ${JSON.stringify(scan.stack)}`,
|
|
899
|
+
scan.scripts.length ? `scripts: [${scan.scripts.join(", ")}]` : null,
|
|
900
|
+
`initialized: ${new Date().toISOString().slice(0, 10)}`,
|
|
901
|
+
"---",
|
|
902
|
+
"",
|
|
802
903
|
`# ${scan.name}`,
|
|
803
904
|
"",
|
|
804
905
|
pitch ? `> ${pitch}` : (scan.pkgDescription ? `> ${scan.pkgDescription}` : "> (describe this product with `scriptonia init --force`)"),
|
|
805
906
|
"",
|
|
806
907
|
`**Stack:** ${scan.stack}`,
|
|
807
|
-
scan.scripts.length ? `**Scripts:** ${scan.scripts.join(", ")}` : "",
|
|
808
908
|
scan.deps.length ? `**Key deps:** ${scan.deps.join(", ")}` : "",
|
|
809
909
|
scan.entryPoints.length ? `**Entry points:** ${scan.entryPoints.join(", ")}` : "",
|
|
810
910
|
"",
|
|
811
911
|
scan.readmeHead ? "## From the README\n\n" + scan.readmeHead : "",
|
|
812
912
|
"",
|
|
813
913
|
"---",
|
|
814
|
-
"*
|
|
815
|
-
].filter((l) => l !== "").join("\n");
|
|
816
|
-
fs.writeFileSync(path.join(dir, "
|
|
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`.*",
|
|
915
|
+
].filter((l) => l !== "" && l !== null).join("\n");
|
|
916
|
+
fs.writeFileSync(path.join(dir, "PROJECT.md"), md);
|
|
817
917
|
}
|
|
818
918
|
|
|
819
919
|
function writeRepoMd(dir, scan) {
|
|
820
|
-
|
|
821
|
-
fs.
|
|
920
|
+
// v0.6 flat layout; keep writing into brain/ for pre-0.6 projects.
|
|
921
|
+
const target = fs.existsSync(path.join(dir, "brain")) && !fs.existsSync(path.join(dir, "REPO.md"))
|
|
922
|
+
? path.join(dir, "brain")
|
|
923
|
+
: dir;
|
|
924
|
+
fs.mkdirSync(target, { recursive: true });
|
|
925
|
+
fs.writeFileSync(path.join(target, "REPO.md"), buildRepoMd(scan));
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* AGENTS.md — the brain's master contract. What an agent must respect when
|
|
930
|
+
* acting on this project: identity, the Scriptonia workflow, and the
|
|
931
|
+
* non-negotiable constraints (mirrored from the decisions corpus; the server
|
|
932
|
+
* enforces them during `plan`, this copy grounds agents working offline).
|
|
933
|
+
*/
|
|
934
|
+
function writeAgentsBrainMd(dir, meta, decisions) {
|
|
935
|
+
const L = [
|
|
936
|
+
`# AGENTS.md — ${meta.name} project brain`,
|
|
937
|
+
`# Generated by: scriptonia (refreshed on \`sync\`)`,
|
|
938
|
+
"",
|
|
939
|
+
"## 1. Identity",
|
|
940
|
+
"",
|
|
941
|
+
meta.pitch ? `${meta.pitch}` : "(run `scriptonia init --force` to describe this product)",
|
|
942
|
+
meta.stack ? `\nStack: ${meta.stack}` : "",
|
|
943
|
+
"",
|
|
944
|
+
"## 2. Workflow — non-negotiable",
|
|
945
|
+
"",
|
|
946
|
+
'1. Before building any user-facing change, run `scriptonia plan "<the issue>"`.',
|
|
947
|
+
" It writes PLAN.md, grounded in real customer signal and checked against",
|
|
948
|
+
" the rules below.",
|
|
949
|
+
"2. Execute PLAN.md **exactly**. Its **Non-goals** section is a fence — do not",
|
|
950
|
+
" build past it.",
|
|
951
|
+
"3. Any constraint marked **⚠ UNRESOLVED** is an approval gate: STOP and get a",
|
|
952
|
+
' human comment (`scriptonia comment plan/<slug> "…"`) before writing code.',
|
|
953
|
+
"4. Cite PLAN.md's `sources` signal ids in your PR description.",
|
|
954
|
+
"",
|
|
955
|
+
"## 3. Non-negotiable constraints (from the decisions corpus)",
|
|
956
|
+
"",
|
|
957
|
+
];
|
|
958
|
+
if (decisions.length === 0) {
|
|
959
|
+
L.push("_None synced yet — run `scriptonia sync` after decisions exist. The server_", "_still enforces them during `plan` regardless._");
|
|
960
|
+
} else {
|
|
961
|
+
for (const d of decisions) {
|
|
962
|
+
L.push(`### ${d.title}${d.ref ? ` (${d.ref})` : ""}`, "", d.body, "");
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
L.push(
|
|
966
|
+
"",
|
|
967
|
+
"## 4. Approval gates",
|
|
968
|
+
"",
|
|
969
|
+
"- Overriding any constraint above requires an explicit human comment on the",
|
|
970
|
+
" plan. The comment is recorded in `comments/plan/<slug>/` and flips the",
|
|
971
|
+
" plan's gate to RESOLVED on regeneration.",
|
|
972
|
+
"",
|
|
973
|
+
);
|
|
974
|
+
fs.writeFileSync(path.join(dir, "AGENTS.md"), L.join("\n"));
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/** Decision → machine-checkable rule file. */
|
|
978
|
+
function renderRule(d) {
|
|
979
|
+
const id = slugify(d.title);
|
|
980
|
+
return [
|
|
981
|
+
"---",
|
|
982
|
+
`rule_id: ${JSON.stringify(id)}`,
|
|
983
|
+
`name: ${JSON.stringify(d.title)}`,
|
|
984
|
+
"severity: BLOCKING",
|
|
985
|
+
d.ref ? `source: ${JSON.stringify(d.ref)}` : null,
|
|
986
|
+
d.decidedAt ? `decided_at: ${JSON.stringify(String(d.decidedAt).slice(0, 10))}` : null,
|
|
987
|
+
"check: |",
|
|
988
|
+
...String(d.body).split("\n").map((l) => ` ${l}`),
|
|
989
|
+
"enforcement: |",
|
|
990
|
+
" Server-side during `scriptonia plan`: any issue conflicting with this rule",
|
|
991
|
+
" is marked ⚠ UNRESOLVED in PLAN.md until a human comment approves the",
|
|
992
|
+
" override (recorded in comments/plan/<slug>/).",
|
|
993
|
+
"---",
|
|
994
|
+
].filter(Boolean).join("\n") + "\n";
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/** Per-signal local receipt: signals/sig_<8>.json */
|
|
998
|
+
function writeSignalReceipt(dir, sig) {
|
|
999
|
+
const sdir = path.join(dir, "signals");
|
|
1000
|
+
fs.mkdirSync(sdir, { recursive: true });
|
|
1001
|
+
fs.writeFileSync(
|
|
1002
|
+
path.join(sdir, `sig_${sig.id.slice(0, 8)}.json`),
|
|
1003
|
+
JSON.stringify({ ...sig, added_at: new Date().toISOString() }, null, 2),
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/** context.md — live synthesis of the current issue against the brain. */
|
|
1008
|
+
function renderContextLive(issue, q, project, signalId) {
|
|
1009
|
+
const cite = (ids) => (ids?.length ? ` \`[${ids.map((i) => i.slice(0, 8)).join(", ")}]\`` : "");
|
|
1010
|
+
const L = [
|
|
1011
|
+
`# Context — ${issue.slice(0, 90)}${issue.length > 90 ? "…" : ""}`,
|
|
1012
|
+
"",
|
|
1013
|
+
`> Synthesized ${new Date().toISOString().slice(0, 10)} against the **${project}** brain · ${(q.sources ?? []).length} signal(s) retrieved${signalId ? ` · this issue ingested as \`sig_${signalId.slice(0, 8)}\`` : ""}`,
|
|
1014
|
+
"",
|
|
1015
|
+
"## TL;DR",
|
|
1016
|
+
q.tldr ?? "(no synthesis)",
|
|
1017
|
+
"",
|
|
1018
|
+
"## What customers actually asked for",
|
|
1019
|
+
];
|
|
1020
|
+
for (const r of q.requirements ?? []) {
|
|
1021
|
+
L.push(`- **${r.title}** _[${r.strength}]_ — ${r.detail}${cite(r.source_ids)}`);
|
|
1022
|
+
}
|
|
1023
|
+
if (!(q.requirements ?? []).length) L.push("- (no supporting signal retrieved — treat scope with caution)");
|
|
1024
|
+
L.push("");
|
|
1025
|
+
|
|
1026
|
+
const cons = q.contradictions ?? [];
|
|
1027
|
+
if (cons.length) {
|
|
1028
|
+
L.push("## ⚠ CONTRADICTION GATE — UNRESOLVED");
|
|
1029
|
+
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.", "");
|
|
1031
|
+
} else {
|
|
1032
|
+
L.push("## Contradiction gate", "", "✓ No conflicts with prior decisions on record.", "");
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if ((q.edge_cases ?? []).length) {
|
|
1036
|
+
L.push("## Edge cases — fold into any plan");
|
|
1037
|
+
for (const e of q.edge_cases) L.push(`- ${e}`);
|
|
1038
|
+
L.push("");
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
L.push("## Cited signals");
|
|
1042
|
+
for (const s of q.sources ?? []) {
|
|
1043
|
+
L.push(`- \`sig_${s.signal_id.slice(0, 8)}\` ${s.source}${s.ref ? ` — ${s.ref}` : ""} _(similarity ${s.similarity})_`);
|
|
1044
|
+
}
|
|
1045
|
+
L.push("", "---", `*Next: \`scriptonia plan "${issue.slice(0, 60)}${issue.length > 60 ? "…" : ""}"\` → PLAN.md.*`);
|
|
1046
|
+
return L.join("\n");
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function readProjectJson(dir) {
|
|
1050
|
+
try { return JSON.parse(fs.readFileSync(path.join(dir, "project.json"), "utf8")); } catch { return null; }
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/** Append to the brain's audit trail: logs/<name>.log */
|
|
1054
|
+
function appendLog(dir, name, msg) {
|
|
1055
|
+
try {
|
|
1056
|
+
const ldir = path.join(dir, "logs");
|
|
1057
|
+
fs.mkdirSync(ldir, { recursive: true });
|
|
1058
|
+
fs.appendFileSync(path.join(ldir, `${name}.log`), `[${new Date().toISOString()}] INFO scriptonia.${name} — ${msg}\n`);
|
|
1059
|
+
} catch { /* audit trail is best-effort */ }
|
|
822
1060
|
}
|
|
823
1061
|
|
|
824
1062
|
function buildRepoMd(scan) {
|
|
@@ -844,13 +1082,6 @@ function renderContextFile(c) {
|
|
|
844
1082
|
return L.join("\n");
|
|
845
1083
|
}
|
|
846
1084
|
|
|
847
|
-
function renderDecisions(decs) {
|
|
848
|
-
if (!decs?.length) return "# Decisions\n\n(none on record)\n";
|
|
849
|
-
const L = ["# Decisions", "", "*The contradiction corpus — plans are checked against these.*", ""];
|
|
850
|
-
for (const d of decs) L.push(`## ${d.title}${d.ref ? ` (${d.ref})` : ""}`, "", d.body, "");
|
|
851
|
-
return L.join("\n");
|
|
852
|
-
}
|
|
853
|
-
|
|
854
1085
|
// ── AGENTS.md injection (every agent reads this) ──────────────
|
|
855
1086
|
function injectAgentsMd(cwd) {
|
|
856
1087
|
const file = path.join(cwd, "AGENTS.md");
|