scriptonia 0.8.0 → 0.9.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +43 -13
  3. package/assets/grammar-manifest.json +32 -0
  4. package/assets/grammars/tree-sitter-bash.wasm +0 -0
  5. package/assets/grammars/tree-sitter-c.wasm +0 -0
  6. package/assets/grammars/tree-sitter-cpp.wasm +0 -0
  7. package/assets/grammars/tree-sitter-dart.wasm +0 -0
  8. package/assets/grammars/tree-sitter-go.wasm +0 -0
  9. package/assets/grammars/tree-sitter-java.wasm +0 -0
  10. package/assets/grammars/tree-sitter-javascript.wasm +0 -0
  11. package/assets/grammars/tree-sitter-kotlin.wasm +0 -0
  12. package/assets/grammars/tree-sitter-objc.wasm +0 -0
  13. package/assets/grammars/tree-sitter-python.wasm +0 -0
  14. package/assets/grammars/tree-sitter-rust.wasm +0 -0
  15. package/assets/grammars/tree-sitter-swift.wasm +0 -0
  16. package/assets/grammars/tree-sitter-tsx.wasm +0 -0
  17. package/assets/grammars/tree-sitter-typescript.wasm +0 -0
  18. package/assets/licenses/tree-sitter-wasms-LICENSE +24 -0
  19. package/assets/licenses/web-tree-sitter-LICENSE +21 -0
  20. package/assets/queries/manifest.json +12 -0
  21. package/assets/queries/v1/dart.scm +7 -0
  22. package/assets/queries/v1/go.scm +5 -0
  23. package/assets/queries/v1/javascript.scm +15 -0
  24. package/assets/queries/v1/python.scm +5 -0
  25. package/assets/queries/v1/typescript.scm +17 -0
  26. package/assets/tree-sitter.wasm +0 -0
  27. package/bin/scriptonia.mjs +243 -76
  28. package/bin/verify.mjs +40 -6
  29. package/dist/brain-ast-worker.js +62 -0
  30. package/dist/brain-ast-worker.js.map +1 -0
  31. package/dist/chunk-6TLQP3LV.js +4048 -0
  32. package/dist/chunk-6TLQP3LV.js.map +1 -0
  33. package/dist/chunk-ZLKAW77A.js +16613 -0
  34. package/dist/chunk-ZLKAW77A.js.map +1 -0
  35. package/dist/index.js +2431 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/legacy-brain-bridge.js +156 -0
  38. package/dist/legacy-brain-bridge.js.map +1 -0
  39. package/package.json +78 -5
@@ -25,6 +25,12 @@ const DEFAULT_URL = process.env.SCRIPTONIA_URL || "https://scriptonia.dev";
25
25
  const CONFIG_DIR = path.join(os.homedir(), ".scriptonia");
26
26
  const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
27
27
  const PROJECTS_DIR = path.join(CONFIG_DIR, "projects");
28
+ let brainBridgePromise;
29
+
30
+ function loadBrainBridge() {
31
+ brainBridgePromise ??= import(new URL("../dist/legacy-brain-bridge.js", import.meta.url).href);
32
+ return brainBridgePromise;
33
+ }
28
34
 
29
35
  const [, , cmd, ...rest] = process.argv;
30
36
 
@@ -91,7 +97,7 @@ async function login(args) {
91
97
 
92
98
  const C = colors();
93
99
  console.log(`\n ${C.g}✓${C.r} Signed in as ${result.email} ${C.dim}(project "${result.project}", with sample data to try)${C.r}`);
94
- console.log(` ${C.g}✓${C.r} Agent wired — skill + AGENTS.md know about \`npx scriptonia plan\``);
100
+ console.log(` ${C.g}✓${C.r} Claude skill installed run \`npx scriptonia init\` inside each repository to add AGENTS.md`);
95
101
  console.log(`\n ${C.b}The loop — feed signal, turn an issue into a plan your agent runs:${C.r}\n`);
96
102
  console.log(` ${C.g}npx scriptonia add${C.r} feedback.txt call.vtt ${C.dim}feed customer signal${C.r}`);
97
103
  console.log(` ${C.g}npx scriptonia plan${C.r} "the thing to build" ${C.dim}→ PLAN.md (sourced, checked)${C.r}`);
@@ -341,15 +347,34 @@ async function comment(args) {
341
347
  if (target.startsWith("plan/")) {
342
348
  const slug = target.slice(5);
343
349
  console.log(`\n ${C.dim}Folding your note into "${slug}" and regenerating…${C.r}`);
350
+ // A regeneration receives both a freshly retrieved pack and the complete
351
+ // snapshot-bound census inventory. Without both, create-file absence
352
+ // cannot be proved and the server fails closed.
353
+ let evidence;
354
+ try {
355
+ evidence = await freshPackForExistingPlan(cfg, slug);
356
+ } catch (error) {
357
+ fail(error instanceof Error ? error.message : String(error));
358
+ }
359
+ if (!evidence) {
360
+ fail("Cannot prove the repository census for this plan. Restore its local PLAN.md issue metadata and rebuild the Deep Brain before regenerating.");
361
+ }
344
362
  const res = await fetch(`${cfg.url}/api/plan/comment`, {
345
363
  method: "POST",
346
364
  headers: { authorization: `Bearer ${cfg.token}`, "content-type": "application/json" },
347
- body: JSON.stringify({ slug, body: text, author: flags.as ?? cfg.email }),
365
+ body: JSON.stringify({
366
+ slug,
367
+ body: text,
368
+ author: flags.as ?? cfg.email,
369
+ ...(evidence ? {
370
+ context_pack: evidence.contextPack,
371
+ repository_inventory: evidence.repositoryInventory,
372
+ } : {}),
373
+ }),
348
374
  });
349
- const data = await res.json().catch(() => ({}));
350
- if (res.status === 402) failCredits(data);
375
+ const data = await readResponseJson(res);
351
376
  if (res.status === 404) fail(`no plan "${slug}" — generate one first: npx scriptonia plan "<issue>"`);
352
- if (!res.ok) fail(`comment failed (${res.status}): ${JSON.stringify(data).slice(0, 200)}`);
377
+ if (!res.ok) failPlanResponse("comment", res, data);
353
378
  // Approval-chain receipt: which human note produced which plan version.
354
379
  if (cfg.inited) {
355
380
  const cdir = path.join(cfg.dir, "comments", "plan", slug);
@@ -360,7 +385,7 @@ async function comment(args) {
360
385
  );
361
386
  appendLog(cfg.dir, "comment", `plan/${slug} — regenerated to v${data.version}`);
362
387
  }
363
- writePlan(cfg, data, flags);
388
+ await writePlan(cfg, data, flags, "commented");
364
389
  return;
365
390
  }
366
391
 
@@ -482,6 +507,71 @@ function failCredits(data) {
482
507
  process.exit(1);
483
508
  }
484
509
 
510
+ async function readResponseJson(response) {
511
+ const text = await response.text();
512
+ if (!text) return {};
513
+ try { return JSON.parse(text); } catch { return { error: "invalid_json_response", message: text.slice(0, 2000) }; }
514
+ }
515
+
516
+ function issueText(value) {
517
+ if (typeof value === "string") return value;
518
+ if (!value || typeof value !== "object") return String(value);
519
+ const pathText = Array.isArray(value.path) && value.path.length ? `${value.path.join(".")}: ` : "";
520
+ return `${pathText}${typeof value.message === "string" ? value.message : JSON.stringify(value)}`;
521
+ }
522
+
523
+ function planWarnings(data) {
524
+ const values = [
525
+ ...(Array.isArray(data?.warnings) ? data.warnings : []),
526
+ ...(Array.isArray(data?.quality?.warnings) ? data.quality.warnings : []),
527
+ ].map(issueText).filter(Boolean);
528
+ return [...new Set(values)];
529
+ }
530
+
531
+ function failPlanResponse(operation, response, data) {
532
+ if (response.status === 402) failCredits(data);
533
+ const error = typeof data?.error === "string" ? data.error : `${operation}_failed`;
534
+ const message = typeof data?.message === "string" ? data.message : "";
535
+ const issues = Array.isArray(data?.issues) ? data.issues.map(issueText) : [];
536
+ const warnings = planWarnings(data);
537
+ const details = [message, ...issues.map((item) => `- ${item}`), ...warnings.map((item) => `- warning: ${item}`)].filter(Boolean);
538
+ if (response.status === 409) {
539
+ fail(`\nPlan blocked (409 · ${error}).${details.length ? `\n${details.join("\n")}` : ""}\nRun \`npx scriptonia brain status\`; rebuild if needed, then retry.`);
540
+ }
541
+ if (response.status === 422) {
542
+ fail(`\nPlan quality rejected (422 · ${error}).${details.length ? `\n${details.join("\n")}` : ""}\nNo PLAN.md was overwritten.`);
543
+ }
544
+ fail(`${operation} failed (${response.status} · ${error})${details.length ? `:\n${details.join("\n")}` : ""}`);
545
+ }
546
+
547
+ function issueFromPlanMarkdown(markdown) {
548
+ const raw = markdown.match(/^issue:\s*(.+)$/m)?.[1]?.trim();
549
+ if (!raw) return undefined;
550
+ try {
551
+ const parsed = JSON.parse(raw);
552
+ return typeof parsed === "string" && parsed.trim() ? parsed.trim() : undefined;
553
+ } catch {
554
+ return raw.replace(/^['"]|['"]$/g, "").trim() || undefined;
555
+ }
556
+ }
557
+
558
+ async function freshPackForExistingPlan(cfg, slug) {
559
+ if (!cfg.inited) return undefined;
560
+ const candidates = [path.join(cfg.plansDir, `${slug}.md`), path.resolve(process.cwd(), "PLAN.md")];
561
+ let issue;
562
+ for (const filename of candidates) {
563
+ try {
564
+ const markdown = fs.readFileSync(filename, "utf8");
565
+ if (filename.endsWith("PLAN.md") && !new RegExp(`^plan:\\s*${slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m").test(markdown)) continue;
566
+ issue = issueFromPlanMarkdown(markdown);
567
+ if (issue) break;
568
+ } catch { /* server pack remains the safe fallback */ }
569
+ }
570
+ if (!issue) return undefined;
571
+ const { buildFreshPlanEvidence } = await loadBrainBridge();
572
+ return buildFreshPlanEvidence(cfg.repoPath, issue);
573
+ }
574
+
485
575
  async function logout() {
486
576
  if (fs.existsSync(CONFIG_PATH)) fs.rmSync(CONFIG_PATH);
487
577
  console.log("logged out.");
@@ -752,9 +842,11 @@ async function init(args) {
752
842
 
753
843
  const existing = findProjectForCwd();
754
844
  if (existing && !flags.force) {
755
- writeRepoMd(existing.dir, scanRepo(cwd));
845
+ injectAgentsMd(cwd);
846
+ injectBrainGitignore(cwd);
756
847
  console.log(`\n ${C.g}✓${C.r} already initialized as ${C.b}${existing.data.name}${C.r} ${C.dim}(${existing.dir})${C.r}`);
757
848
  console.log(` ${C.dim}re-scan from scratch:${C.r} ${C.g}npx scriptonia init --force${C.r}`);
849
+ await buildFastBrainForInit(existing.data.repoPath ?? cwd, existing.dir, C);
758
850
  console.log(` ${C.dim}next:${C.r} ${C.g}npx scriptonia add <files>${C.r}\n`);
759
851
  return;
760
852
  }
@@ -788,9 +880,8 @@ async function init(args) {
788
880
  for (const sub of ["signals", "rules", "contexts", "plans", "comments/plan", "logs"]) {
789
881
  fs.mkdirSync(path.join(dir, sub), { recursive: true });
790
882
  }
791
- // Migrate a legacy brain/ subfolder layout if present.
792
- const legacyBrain = path.join(dir, "brain");
793
- if (fs.existsSync(legacyBrain)) fs.rmSync(legacyBrain, { recursive: true, force: true });
883
+ // Legacy plain-file brains remain readable. Init never deletes repository
884
+ // memory or the last READY generation while replacing a project binding.
794
885
 
795
886
  fs.writeFileSync(
796
887
  path.join(dir, "project.json"),
@@ -798,22 +889,68 @@ async function init(args) {
798
889
  { mode: 0o600 },
799
890
  );
800
891
  writeProjectMd(dir, scan, pitch);
801
- writeRepoMd(dir, scan);
802
892
  writeAgentsBrainMd(dir, { name, pitch, stack: scan.stack }, []);
803
893
  fs.writeFileSync(path.join(dir, ".gitignore"), "logs/\nproject.json\n");
804
894
  injectAgentsMd(cwd);
895
+ injectBrainGitignore(cwd);
805
896
  installSkill();
806
897
  appendLog(dir, "init", `Initialized brain for ${name} (${repoPath}) — stack: ${scan.stack}`);
807
898
 
808
899
  console.log(`\n ${C.g}✓${C.r} Brain created for ${C.b}${name}${C.r} ${C.dim}→ ${dir}${C.r}`);
809
- console.log(` ${C.dim}AGENTS.md · PROJECT.md · REPO.md · signals/ · rules/ · contexts/ · plans/ · comments/ · logs/${C.r}`);
900
+ console.log(` ${C.dim}AGENTS.md · PROJECT.md · signals/ · rules/ · contexts/ · plans/ · comments/ · logs/${C.r}`);
810
901
  console.log(` ${C.g}✓${C.r} Scanned repo (${scan.stack})`);
811
902
  console.log(` ${C.g}✓${C.r} Wrote agent instructions ${C.dim}→ ./AGENTS.md${C.r}`);
903
+ await buildFastBrainForInit(cwd, dir, C);
812
904
  console.log(`\n ${C.b}Feed it customer signal, then turn an issue into a plan:${C.r}\n`);
813
905
  console.log(` ${C.g}npx scriptonia add${C.r} feedback.txt call.vtt ${C.dim}(or: npx scriptonia add "Issue #42: …")${C.r}`);
814
906
  console.log(` ${C.g}npx scriptonia plan${C.r} "the issue you want to build"\n`);
815
907
  }
816
908
 
909
+ async function buildFastBrainForInit(repoRoot, projectDir, C) {
910
+ console.log(` ${C.dim}Building the local fast brain…${C.r}`);
911
+ try {
912
+ const { buildFastBrainAfterInit } = await loadBrainBridge();
913
+ const result = await buildFastBrainAfterInit(repoRoot);
914
+ const projection = mirrorDeepRepoProjection(repoRoot, projectDir);
915
+ appendLog(projectDir, "brain", `READY generation ${result.generation} (${result.buildId}) after init`);
916
+ console.log(` ${C.g}✓${C.r} Local brain READY ${C.dim}generation ${result.generation}${C.r}`);
917
+ console.log(` ${C.g}✓${C.r} Full repository map ${C.dim}→ ${projection}${C.r}`);
918
+ } catch (error) {
919
+ const message = error instanceof Error ? error.message : String(error);
920
+ appendLog(projectDir, "brain", `Fast build failed after binding persisted — ${message}`);
921
+ console.error(` ${C.w}⚠${C.r} Fast brain build could not finish: ${message}`);
922
+ console.error(` ${C.dim}Your login and project binding are saved; any prior READY generation is unchanged.${C.r}`);
923
+ }
924
+ console.log(` ${C.b}Next:${C.r} ${C.g}npx scriptonia brain build --deep${C.r} ${C.dim}— full repository understanding (5–7 minutes)${C.r}`);
925
+ }
926
+
927
+ /**
928
+ * The legacy project binding still exposes REPO.md to agents working outside
929
+ * the repository. Its contents must be the activated Deep Brain projection,
930
+ * never a second, shallow filesystem scan with different facts.
931
+ */
932
+ function mirrorDeepRepoProjection(repoRoot, projectDir) {
933
+ const source = path.join(realPath(repoRoot), ".scriptonia", "REPO.md");
934
+ if (!fs.existsSync(source)) throw new Error("READY generation did not produce .scriptonia/REPO.md");
935
+ const markdown = fs.readFileSync(source, "utf8");
936
+ if (!markdown.startsWith("# Repository Brain") || markdown.includes("Tree (depth 3)") || /Stack:\s*unknown/i.test(markdown)) {
937
+ throw new Error("READY generation produced an invalid repository projection");
938
+ }
939
+
940
+ // Pre-v0.6 project bindings used brain/; keep them readable without
941
+ // manufacturing a different repository map.
942
+ const targetDir = fs.existsSync(path.join(projectDir, "brain")) && !fs.existsSync(path.join(projectDir, "REPO.md"))
943
+ ? path.join(projectDir, "brain")
944
+ : projectDir;
945
+ fs.mkdirSync(targetDir, { recursive: true });
946
+ const target = path.join(targetDir, "REPO.md");
947
+ const temporary = `${target}.tmp-${process.pid}`;
948
+ fs.writeFileSync(temporary, markdown, { mode: 0o600 });
949
+ fs.chmodSync(temporary, 0o600);
950
+ fs.renameSync(temporary, target);
951
+ return target;
952
+ }
953
+
817
954
  // ── sync ──────────────────────────────────────────────────────
818
955
  async function sync() {
819
956
  const cfg = activeCreds();
@@ -853,28 +990,36 @@ async function plan(args) {
853
990
 
854
991
  if (flags.refresh) {
855
992
  const res = await fetch(`${cfg.url}/api/plan?slug=${encodeURIComponent(flags.refresh)}`, { headers: { authorization: `Bearer ${cfg.token}` } });
856
- const data = await res.json();
857
- if (!res.ok) fail(`refresh failed (${res.status}): ${JSON.stringify(data)}`);
858
- return writePlan(cfg, data, flags);
993
+ const data = await readResponseJson(res);
994
+ if (!res.ok) failPlanResponse("refresh", res, data);
995
+ return writePlan(cfg, data, flags, "refreshed");
859
996
  }
860
997
 
861
998
  const issue = args.filter((a) => !a.startsWith("--")).join(" ").trim();
862
999
  if (!issue) fail('usage: npx scriptonia plan "<the issue>" [--out PLAN.md] [--refresh <slug>]');
863
1000
 
864
- const repoSummary = cfg.inited && fs.existsSync(path.join(cfg.brainDir, "REPO.md"))
865
- ? fs.readFileSync(path.join(cfg.brainDir, "REPO.md"), "utf8").slice(0, 6000)
866
- : buildRepoMd(scanRepo(process.cwd())).slice(0, 6000);
1001
+ if (!cfg.inited) fail("run `npx scriptonia init` in this repository first — plans require a fresh local Deep Brain context pack");
1002
+ let evidence;
1003
+ try {
1004
+ const { buildFreshPlanEvidence } = await loadBrainBridge();
1005
+ evidence = buildFreshPlanEvidence(cfg.repoPath, issue);
1006
+ } catch (error) {
1007
+ fail(error instanceof Error ? error.message : String(error));
1008
+ }
867
1009
 
868
- console.log(`\n ${C.dim}Planning "${issue}" — retrieving signal, checking decisions, drafting… (Opus, ~15s)${C.r}`);
1010
+ console.log(`\n ${C.dim}Planning "${issue}" — using fresh local brain evidence, retrieving signal, checking decisions, drafting…${C.r}`);
869
1011
  const res = await fetch(`${cfg.url}/api/plan`, {
870
1012
  method: "POST",
871
1013
  headers: { authorization: `Bearer ${cfg.token}`, "content-type": "application/json" },
872
- body: JSON.stringify({ issue, repo_summary: repoSummary }),
1014
+ body: JSON.stringify({
1015
+ issue,
1016
+ context_pack: evidence.contextPack,
1017
+ repository_inventory: evidence.repositoryInventory,
1018
+ }),
873
1019
  });
874
- const data = await res.json();
875
- if (res.status === 402) failCredits(data);
876
- if (!res.ok) fail(`plan failed (${res.status}): ${JSON.stringify(data).slice(0, 200)}`);
877
- writePlan(cfg, data, flags);
1020
+ const data = await readResponseJson(res);
1021
+ if (!res.ok) failPlanResponse("plan", res, data);
1022
+ await writePlan(cfg, data, flags, "fresh");
878
1023
  }
879
1024
 
880
1025
  // ── verify — PLAN.md + git diff → separate CI gates ──────────────────
@@ -883,7 +1028,7 @@ async function verify(args) {
883
1028
  process.exitCode = result.exitCode;
884
1029
  }
885
1030
 
886
- function writePlan(cfg, data, flags) {
1031
+ async function writePlan(cfg, data, flags, flow) {
887
1032
  const C = colors();
888
1033
  const out = flags.out ?? "PLAN.md";
889
1034
  fs.writeFileSync(path.resolve(process.cwd(), out), data.markdown);
@@ -895,8 +1040,25 @@ function writePlan(cfg, data, flags) {
895
1040
  fs.writeFileSync(path.join(cfg.brainDir, "plan.md"), data.markdown);
896
1041
  appendLog(cfg.dir, "plan", `${data.slug} v${data.version} — ${(data.sources ?? []).length} sources, ${data.contradictions ?? 0} unresolved`);
897
1042
  }
1043
+ if (cfg.inited && cfg.repoPath) {
1044
+ try {
1045
+ const { recordPlanQualityEvidence } = await loadBrainBridge();
1046
+ recordPlanQualityEvidence(cfg.repoPath, {
1047
+ flow,
1048
+ slug: data.slug,
1049
+ version: data.version,
1050
+ quality: data.quality,
1051
+ brainGeneration: data.brain_generation,
1052
+ contextPackHash: data.context_pack_hash,
1053
+ });
1054
+ } catch (error) {
1055
+ const message = error instanceof Error ? error.message : String(error);
1056
+ console.error(` ${C.dim}Plan was written, but build-scoped quality evidence could not be recorded: ${message}${C.r}`);
1057
+ }
1058
+ }
898
1059
  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}`);
899
1060
  console.log(` ${C.dim}${(data.sources ?? []).length} cited signal(s)${data.contradictions ? `, ${data.contradictions} unresolved contradiction(s)` : ""}${C.r}`);
1061
+ for (const warning of planWarnings(data)) console.log(` ${C.w}⚠ ${warning}${C.r}`);
900
1062
  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}`);
901
1063
  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
1064
  if (data.credits) {
@@ -914,7 +1076,6 @@ function scanRepo(cwd) {
914
1076
  let pkg = null;
915
1077
  try { pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8")); } catch {}
916
1078
  const stack = detectStack(cwd, pkg);
917
- const tree = buildTree(cwd, 3);
918
1079
  const name =
919
1080
  (pkg && pkg.name && String(pkg.name).replace(/^@[^/]+\//, "")) ||
920
1081
  gitRemoteName(cwd) ||
@@ -922,11 +1083,16 @@ function scanRepo(cwd) {
922
1083
  const scripts = pkg && pkg.scripts ? Object.keys(pkg.scripts) : [];
923
1084
  const deps = pkg ? Object.keys({ ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }).slice(0, 20) : [];
924
1085
  const entryPoints = findEntryPoints(cwd);
925
- return { name, readmeHead, stack, tree, scripts, deps, entryPoints, pkgDescription: pkg?.description || "" };
1086
+ return { name, readmeHead, stack, scripts, deps, entryPoints, pkgDescription: pkg?.description || "" };
926
1087
  }
927
1088
 
928
1089
  function detectStack(cwd, pkg) {
929
1090
  const has = (f) => fs.existsSync(path.join(cwd, f));
1091
+ let pubspec = "";
1092
+ if (has("pubspec.yaml")) {
1093
+ try { pubspec = fs.readFileSync(path.join(cwd, "pubspec.yaml"), "utf8"); } catch {}
1094
+ if (/(^|\n)\s*flutter\s*:|sdk\s*:\s*flutter\b|flutter_test\s*:/m.test(pubspec)) return "Flutter/Dart";
1095
+ }
930
1096
  if (pkg) {
931
1097
  const d = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
932
1098
  if (d.next) return "Next.js / TypeScript";
@@ -936,6 +1102,7 @@ function detectStack(cwd, pkg) {
936
1102
  }
937
1103
  if (has("go.mod")) return "Go";
938
1104
  if (has("Cargo.toml")) return "Rust";
1105
+ if (pubspec) return "Dart";
939
1106
  if (has("requirements.txt") || has("pyproject.toml")) return "Python";
940
1107
  if (has("Gemfile")) return "Ruby";
941
1108
  if (has("pom.xml") || has("build.gradle")) return "Java/JVM";
@@ -951,24 +1118,6 @@ function findEntryPoints(cwd) {
951
1118
  return cands.filter((c) => fs.existsSync(path.join(cwd, c)));
952
1119
  }
953
1120
 
954
- function buildTree(cwd, maxDepth) {
955
- const IGNORE = new Set(["node_modules", ".git", ".next", "dist", "build", ".turbo", "coverage", ".vercel", "out", "__pycache__", ".venv", "target", "vendor"]);
956
- const lines = [];
957
- const walk = (d, depth, prefix) => {
958
- if (depth > maxDepth) return;
959
- let entries;
960
- try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
961
- entries = entries.filter((e) => !e.name.startsWith(".") || e.name === ".env.example").filter((e) => !IGNORE.has(e.name));
962
- entries.sort((a, b) => (a.isDirectory() === b.isDirectory() ? a.name.localeCompare(b.name) : a.isDirectory() ? -1 : 1));
963
- for (const e of entries.slice(0, 40)) {
964
- lines.push(`${prefix}${e.name}${e.isDirectory() ? "/" : ""}`);
965
- if (e.isDirectory()) walk(path.join(d, e.name), depth + 1, prefix + " ");
966
- }
967
- };
968
- walk(cwd, 1, "");
969
- return lines.slice(0, 200).join("\n");
970
- }
971
-
972
1121
  function gitRemoteName(cwd) {
973
1122
  try {
974
1123
  const url = execSync("git remote get-url origin", { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
@@ -1003,15 +1152,6 @@ function writeProjectMd(dir, scan, pitch) {
1003
1152
  fs.writeFileSync(path.join(dir, "PROJECT.md"), md);
1004
1153
  }
1005
1154
 
1006
- function writeRepoMd(dir, scan) {
1007
- // v0.6 flat layout; keep writing into brain/ for pre-0.6 projects.
1008
- const target = fs.existsSync(path.join(dir, "brain")) && !fs.existsSync(path.join(dir, "REPO.md"))
1009
- ? path.join(dir, "brain")
1010
- : dir;
1011
- fs.mkdirSync(target, { recursive: true });
1012
- fs.writeFileSync(path.join(target, "REPO.md"), buildRepoMd(scan));
1013
- }
1014
-
1015
1155
  /**
1016
1156
  * AGENTS.md — the brain's master contract. What an agent must respect when
1017
1157
  * acting on this project: identity, the Scriptonia workflow, and the
@@ -1030,17 +1170,24 @@ function writeAgentsBrainMd(dir, meta, decisions) {
1030
1170
  "",
1031
1171
  "## 2. Workflow — non-negotiable",
1032
1172
  "",
1033
- '1. Before building any user-facing change, run `npx scriptonia plan "<the issue>"`.',
1173
+ "1. Check repository evidence with `npx scriptonia brain status`. If it is",
1174
+ " missing or stale, run `npx scriptonia brain build --fast --offline`.",
1175
+ '2. Before building any user-facing change, run `npx scriptonia plan "<the issue>"`.',
1034
1176
  " It writes PLAN.md, grounded in real customer signal and checked against",
1035
- " the rules below.",
1036
- "2. Execute PLAN.md **exactly**. Its **Non-goals** section is a fence do not",
1177
+ " the rules below and the active local Deep Brain generation.",
1178
+ "3. Use `npx scriptonia why <file>` to inspect the exact facts, dependents,",
1179
+ " and tests behind a planned file before editing it.",
1180
+ "4. Execute PLAN.md **exactly**. Its **Non-goals** section is a fence — do not",
1037
1181
  " build past it.",
1038
- "3. Any constraint marked **⚠ UNRESOLVED** is an approval gate: STOP and get a",
1182
+ "5. Any constraint marked **⚠ UNRESOLVED** is an approval gate: STOP and get a",
1039
1183
  ' human comment (`npx scriptonia comment plan/<slug> "…"`) before writing code.',
1040
- "4. Cite PLAN.md's `sources` signal ids in your PR description.",
1041
- "5. After implementation, run `npx scriptonia verify`; fix every BLOCK",
1184
+ "6. Cite PLAN.md's `sources` signal ids in your PR description.",
1185
+ "7. After implementation, run `npx scriptonia verify`; fix every BLOCK",
1042
1186
  " before merge and preserve the generated `.scriptonia/evals/*.yaml` case.",
1043
1187
  "",
1188
+ "After setup, run `npx scriptonia brain build --deep` for the enriched",
1189
+ "repository model (allow 5–7 minutes).",
1190
+ "",
1044
1191
  "## 3. Non-negotiable constraints (from the decisions corpus)",
1045
1192
  "",
1046
1193
  ];
@@ -1148,22 +1295,6 @@ function appendLog(dir, name, msg) {
1148
1295
  } catch { /* audit trail is best-effort */ }
1149
1296
  }
1150
1297
 
1151
- function buildRepoMd(scan) {
1152
- return [
1153
- `# Repo map — ${scan.name}`,
1154
- "",
1155
- `**Stack:** ${scan.stack}`,
1156
- scan.entryPoints.length ? `**Entry points:** ${scan.entryPoints.join(", ")}` : "",
1157
- "",
1158
- "## Tree (depth 3)",
1159
- "```",
1160
- scan.tree,
1161
- "```",
1162
- "",
1163
- "*When you name files in a plan, use paths from this tree. Refresh with `npx scriptonia init --force`.*",
1164
- ].filter(Boolean).join("\n");
1165
- }
1166
-
1167
1298
  function renderContextFile(c) {
1168
1299
  const L = [`# ${c.title}`, "", `> **${c.type}** · ${c.segment} · strength ${c.strength}/10 · v${c.version}`, "", c.summary, ""];
1169
1300
  if (c.sources?.length) { L.push("## Sources"); for (const s of c.sources) L.push(`- \`${s.id.slice(0, 8)}\` ${s.source}${s.ref ? ` — ${s.ref}` : ""}`); L.push(""); }
@@ -1182,8 +1313,12 @@ function injectAgentsMd(cwd) {
1182
1313
  This repo has a product brain (customer signal + prior decisions). Before building
1183
1314
  any user-facing change:
1184
1315
 
1316
+ - \`npx scriptonia brain status\` → confirms the local Deep Brain is READY and fresh.
1317
+ If missing or stale, run \`npx scriptonia brain build --fast --offline\`.
1185
1318
  - \`npx scriptonia plan "<the issue>"\` → writes **PLAN.md** here. Execute PLAN.md
1186
1319
  exactly; respect its **Non-goals**; cite its \`sources\` ids in your PR.
1320
+ - \`npx scriptonia why <file>\` → shows the facts, dependents, and related tests
1321
+ behind a planned file before you edit it.
1187
1322
  - \`npx scriptonia query "<topic>"\` → JSON: what customers asked for, contradictions,
1188
1323
  edge cases.
1189
1324
  - If a plan constraint is **UNRESOLVED**, stop and ask the human. If a step
@@ -1191,6 +1326,8 @@ any user-facing change:
1191
1326
  and re-read PLAN.md.
1192
1327
  - After implementation, run \`npx scriptonia verify\`. Fix every BLOCK before
1193
1328
  merge; keep the generated \`.scriptonia/evals/*.yaml\` regression case.
1329
+ - After setup, run \`npx scriptonia brain build --deep\` for the enriched repository
1330
+ model (allow 5–7 minutes).
1194
1331
  ${END}`;
1195
1332
 
1196
1333
  let existing = "";
@@ -1205,6 +1342,36 @@ ${END}`;
1205
1342
  }
1206
1343
  }
1207
1344
 
1345
+ // The database, object store, and regenerated projections are local artifacts.
1346
+ // Keep policies/evals outside these rules so teams can commit their shared
1347
+ // product constraints and regression cases.
1348
+ function injectBrainGitignore(cwd) {
1349
+ const file = path.join(cwd, ".gitignore");
1350
+ const required = [
1351
+ ".scriptonia/brain.db*",
1352
+ ".scriptonia/artifacts/",
1353
+ ".scriptonia/objects/",
1354
+ ".scriptonia/staging/",
1355
+ ".scriptonia/REPO.md",
1356
+ ".scriptonia/GRAPH.md",
1357
+ ".scriptonia/ARCHITECTURE.md",
1358
+ ".scriptonia/PATTERNS.md",
1359
+ ".scriptonia/DECISION_CANDIDATES.md",
1360
+ ".scriptonia/brain-coverage.json",
1361
+ ".scriptonia/result*.json",
1362
+ ".scriptonia/result*.sarif",
1363
+ ];
1364
+ let existing = "";
1365
+ try { existing = fs.readFileSync(file, "utf8"); } catch {}
1366
+
1367
+ const present = new Set(existing.split(/\r?\n/).map((line) => line.trim()));
1368
+ const missing = required.filter((line) => !present.has(line));
1369
+ if (!missing.length) return;
1370
+
1371
+ const prefix = existing.length && !existing.endsWith("\n") ? `${existing}\n` : existing;
1372
+ fs.writeFileSync(file, `${prefix}${missing.join("\n")}\n`);
1373
+ }
1374
+
1208
1375
  // ── misc ──────────────────────────────────────────────────────
1209
1376
  function slugify(s) {
1210
1377
  return String(s).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "").slice(0, 60) || "project";
package/bin/verify.mjs CHANGED
@@ -27,7 +27,7 @@ export async function runVerification(args, { cfg, cwd = process.cwd(), fetchImp
27
27
  });
28
28
  const repositoryChecks = flags["skip-checks"] ? skippedRepositoryGate() : runRepositoryChecks(cwd, files);
29
29
  const unresolvedConstraints = buildUnresolvedGate(plan);
30
- const testCoverage = buildTestCoverageGate(files);
30
+ const testCoverage = buildTestCoverageGate(files, cwd);
31
31
 
32
32
  const response = await semanticPromise;
33
33
  const semantic = await response.json().catch(() => ({}));
@@ -82,9 +82,9 @@ export function buildUnresolvedGate(plan) {
82
82
  });
83
83
  }
84
84
 
85
- export function buildTestCoverageGate(files) {
85
+ export function buildTestCoverageGate(files, cwd) {
86
86
  const tests = files.filter(isTestFile);
87
- const behavior = files.filter((file) => isSourceFile(file) && !isTestFile(file));
87
+ const behavior = files.filter((file) => isSourceFile(file, cwd));
88
88
  if (tests.length) return gate("PASS", {
89
89
  evidence: tests,
90
90
  plan_criterion: "Add or update tests for changed product behavior.",
@@ -205,12 +205,46 @@ function diffFiles(diff) {
205
205
  return [...new Set([...diff.matchAll(/^diff --git a\/(.+?) b\/(.+)$/gm)].map((match) => match[2]))];
206
206
  }
207
207
 
208
+ const SOURCE_EXTENSIONS = new Set([
209
+ ".c", ".h", ".cc", ".cp", ".cpp", ".cxx", ".hh", ".hpp", ".hxx", ".inc", ".m", ".mm", ".cs",
210
+ ".css", ".scss", ".sass", ".less", ".dart", ".ex", ".exs", ".go", ".graphql", ".gql", ".html", ".htm",
211
+ ".java", ".js", ".jsx", ".mjs", ".cjs", ".json", ".jsonc", ".kt", ".kts", ".lua", ".pl", ".pm", ".php",
212
+ ".proto", ".py", ".pyi", ".rb", ".rs", ".scala", ".sh", ".bash", ".zsh", ".fish", ".sql", ".bzl",
213
+ ".bazel", ".star", ".swift", ".toml", ".ts", ".tsx", ".mts", ".cts", ".vue", ".xml", ".plist", ".yaml", ".yml",
214
+ ]);
215
+ const STARLARK_BASENAMES = new Set(["BUILD", "BUILD.bazel", "WORKSPACE", "WORKSPACE.bzlmod", "MODULE.bazel"]);
216
+ const CONFIG_BASENAMES = new Set([
217
+ "package.json", "package-lock.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", "yarn.lock", "pubspec.yaml", "pubspec.lock",
218
+ "Cargo.toml", "go.mod", "go.sum", "pom.xml", "composer.json", "composer.lock", "pyproject.toml", "requirements.txt",
219
+ "tsconfig.json", "analysis_options.yaml", "CMakeLists.txt", "Makefile", "Dockerfile", "CODEOWNERS",
220
+ ]);
221
+ const TEST_EXTENSION = "(?:c|cc|cp|cpp|cxx|h|hh|hpp|hxx|inc|m|mm|cs|dart|ex|exs|go|java|js|jsx|mjs|cjs|kt|kts|lua|pl|pm|php|proto|py|pyi|rb|rs|scala|sh|bash|zsh|fish|bzl|bazel|star|swift|ts|tsx|mts|cts|vue)";
222
+ const TEST_BASENAME = new RegExp(`(?:^|/)(?:test_[^/]+|[^/]+_(?:test|spec)|[^/]+\\.(?:test|spec))\\.${TEST_EXTENSION}$`, "i");
223
+
208
224
  function isTestFile(file) {
209
- return /(^|\/)(?:__tests__|tests?|spec)(?:\/|$)|\.(?:test|spec)\.[^.]+$/i.test(file);
225
+ const normalized = file.replaceAll("\\", "/");
226
+ return /(^|\/)(?:__tests__|tests?|spec|integration_test)(?:\/|$)/i.test(normalized) ||
227
+ TEST_BASENAME.test(normalized) ||
228
+ /(?:^|\/)[^/]+(?:Test|Tests|Spec)\.(?:cs|java|kt|kts|scala|swift)$/.test(normalized);
210
229
  }
211
230
 
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);
231
+ function isSourceFile(file, cwd) {
232
+ const normalized = file.replaceAll("\\", "/");
233
+ if (isTestFile(normalized) || /(?:^|\/)(?:third_party|vendor|external|docs?|adr|changelogs?)(?:\/|$)/i.test(normalized)) return false;
234
+ const base = path.posix.basename(normalized);
235
+ if (/(?:^|\/)generated(?:\/|$)/i.test(normalized) || /\.(?:g|freezed)\.dart$|\.pb\.go$|\.generated\.[^.]+$|\.min\.(?:js|css)$/i.test(base)) return false;
236
+ if (STARLARK_BASENAMES.has(base)) return true;
237
+ if (CONFIG_BASENAMES.has(base) || /\.(?:lock|config|conf|ini|properties)$/i.test(base) ||
238
+ /(?:^|\/)\.github\/workflows\/[^/]+\.ya?ml$/i.test(normalized) ||
239
+ /^(?:Dockerfile(?:\..+)?|requirements(?:[-_.][^/]*)?\.txt|tsconfig(?:\.[^/]+)?\.json|(?:docker-)?compose(?:\.[^/]+)?\.ya?ml)$/i.test(base)) return false;
240
+ if (SOURCE_EXTENSIONS.has(path.posix.extname(base).toLowerCase())) return true;
241
+ if (!cwd) return false;
242
+ try {
243
+ const firstLine = fs.readFileSync(path.join(cwd, normalized), "utf8").split(/\r?\n/, 1)[0] ?? "";
244
+ return /^#!.*\b(?:python3?|bash|sh|zsh|fish|node|ruby)\b/.test(firstLine);
245
+ } catch {
246
+ return false;
247
+ }
214
248
  }
215
249
 
216
250
  function gate(verdict, check) {
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AST_WORKER_PROTOCOL_VERSION,
4
+ isAstWorkerRequest,
5
+ parseSource
6
+ } from "./chunk-6TLQP3LV.js";
7
+
8
+ // ../packages/brain-ast/src/worker-runtime.ts
9
+ import { isMainThread, parentPort, threadId } from "worker_threads";
10
+ function serializeError(error) {
11
+ if (!(error instanceof Error)) return { name: "Error", message: String(error) };
12
+ const code = "code" in error && typeof error.code === "string" ? error.code : void 0;
13
+ return {
14
+ name: error.name,
15
+ message: error.message,
16
+ ...error.stack ? { stack: error.stack } : {},
17
+ ...code ? { code } : {}
18
+ };
19
+ }
20
+ function startAstWorkerThread() {
21
+ if (isMainThread || !parentPort) {
22
+ throw new Error("the Deep Brain AST worker entry must run inside a worker thread");
23
+ }
24
+ const port = parentPort;
25
+ port.on("message", async (request) => {
26
+ if (!isAstWorkerRequest(request)) {
27
+ const response = {
28
+ protocolVersion: AST_WORKER_PROTOCOL_VERSION,
29
+ type: "error",
30
+ jobId: request && typeof request === "object" && "jobId" in request && typeof request.jobId === "number" ? request.jobId : 0,
31
+ threadId,
32
+ error: { name: "AstWorkerProtocolError", message: "received an invalid AST worker request" }
33
+ };
34
+ port.postMessage(response);
35
+ return;
36
+ }
37
+ try {
38
+ const result = await parseSource(request.input);
39
+ const response = {
40
+ protocolVersion: AST_WORKER_PROTOCOL_VERSION,
41
+ type: "result",
42
+ jobId: request.jobId,
43
+ threadId,
44
+ result
45
+ };
46
+ port.postMessage(response);
47
+ } catch (error) {
48
+ const response = {
49
+ protocolVersion: AST_WORKER_PROTOCOL_VERSION,
50
+ type: "error",
51
+ jobId: request.jobId,
52
+ threadId,
53
+ error: serializeError(error)
54
+ };
55
+ port.postMessage(response);
56
+ }
57
+ });
58
+ }
59
+
60
+ // src/brain-ast-worker.ts
61
+ startAstWorkerThread();
62
+ //# sourceMappingURL=brain-ast-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../packages/brain-ast/src/worker-runtime.ts","../src/brain-ast-worker.ts"],"sourcesContent":["import { isMainThread, parentPort, threadId } from \"node:worker_threads\";\nimport { parseSource } from \"./parse-source\";\nimport {\n AST_WORKER_PROTOCOL_VERSION,\n isAstWorkerRequest,\n type AstWorkerResponse,\n type SerializedWorkerError,\n} from \"./worker-protocol\";\n\nfunction serializeError(error: unknown): SerializedWorkerError {\n if (!(error instanceof Error)) return { name: \"Error\", message: String(error) };\n const code = \"code\" in error && typeof error.code === \"string\" ? error.code : undefined;\n return {\n name: error.name,\n message: error.message,\n ...(error.stack ? { stack: error.stack } : {}),\n ...(code ? { code } : {}),\n };\n}\n\n/** Starts the dedicated AST worker message loop. This is intentionally not run\n * when the module is imported so the CLI can bundle it as a separate entry. */\nexport function startAstWorkerThread(): void {\n if (isMainThread || !parentPort) {\n throw new Error(\"the Deep Brain AST worker entry must run inside a worker thread\");\n }\n const port = parentPort;\n port.on(\"message\", async (request: unknown) => {\n if (!isAstWorkerRequest(request)) {\n const response: AstWorkerResponse = {\n protocolVersion: AST_WORKER_PROTOCOL_VERSION,\n type: \"error\",\n jobId: request && typeof request === \"object\" && \"jobId\" in request && typeof request.jobId === \"number\" ? request.jobId : 0,\n threadId,\n error: { name: \"AstWorkerProtocolError\", message: \"received an invalid AST worker request\" },\n };\n port.postMessage(response);\n return;\n }\n try {\n const result = await parseSource(request.input);\n const response: AstWorkerResponse = {\n protocolVersion: AST_WORKER_PROTOCOL_VERSION,\n type: \"result\",\n jobId: request.jobId,\n threadId,\n result,\n };\n port.postMessage(response);\n } catch (error) {\n const response: AstWorkerResponse = {\n protocolVersion: AST_WORKER_PROTOCOL_VERSION,\n type: \"error\",\n jobId: request.jobId,\n threadId,\n error: serializeError(error),\n };\n port.postMessage(response);\n }\n });\n}\n","import { startAstWorkerThread } from \"@scriptonia/brain-ast/worker\";\n\nstartAstWorkerThread();\n"],"mappings":";;;;;;;;AAAA,SAAS,cAAc,YAAY,gBAAgB;AASnD,SAAS,eAAe,OAAuC;AAC7D,MAAI,EAAE,iBAAiB,OAAQ,QAAO,EAAE,MAAM,SAAS,SAAS,OAAO,KAAK,EAAE;AAC9E,QAAM,OAAO,UAAU,SAAS,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO;AAC9E,SAAO;AAAA,IACL,MAAM,MAAM;AAAA,IACZ,SAAS,MAAM;AAAA,IACf,GAAI,MAAM,QAAQ,EAAE,OAAO,MAAM,MAAM,IAAI,CAAC;AAAA,IAC5C,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,EACzB;AACF;AAIO,SAAS,uBAA6B;AAC3C,MAAI,gBAAgB,CAAC,YAAY;AAC/B,UAAM,IAAI,MAAM,iEAAiE;AAAA,EACnF;AACA,QAAM,OAAO;AACb,OAAK,GAAG,WAAW,OAAO,YAAqB;AAC7C,QAAI,CAAC,mBAAmB,OAAO,GAAG;AAChC,YAAM,WAA8B;AAAA,QAClC,iBAAiB;AAAA,QACjB,MAAM;AAAA,QACN,OAAO,WAAW,OAAO,YAAY,YAAY,WAAW,WAAW,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ;AAAA,QAC3H;AAAA,QACA,OAAO,EAAE,MAAM,0BAA0B,SAAS,yCAAyC;AAAA,MAC7F;AACA,WAAK,YAAY,QAAQ;AACzB;AAAA,IACF;AACA,QAAI;AACF,YAAM,SAAS,MAAM,YAAY,QAAQ,KAAK;AAC9C,YAAM,WAA8B;AAAA,QAClC,iBAAiB;AAAA,QACjB,MAAM;AAAA,QACN,OAAO,QAAQ;AAAA,QACf;AAAA,QACA;AAAA,MACF;AACA,WAAK,YAAY,QAAQ;AAAA,IAC3B,SAAS,OAAO;AACd,YAAM,WAA8B;AAAA,QAClC,iBAAiB;AAAA,QACjB,MAAM;AAAA,QACN,OAAO,QAAQ;AAAA,QACf;AAAA,QACA,OAAO,eAAe,KAAK;AAAA,MAC7B;AACA,WAAK,YAAY,QAAQ;AAAA,IAC3B;AAAA,EACF,CAAC;AACH;;;AC1DA,qBAAqB;","names":[]}