@kodr/aiwrangler 0.1.1 → 0.1.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,33 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## 0.1.2 - 2026-10-20
9
+
10
+ ### Added
11
+
12
+ - OpenCode V2 plugin entrypoint. `@kodr/aiwrangler/plugin` now default-exports
13
+ a V2 plugin (id `aiwrangler.nudge`) alongside the unchanged V1 named export
14
+ `WranglerWatchPlugin`: add `@kodr/aiwrangler` to the `plugins` array in
15
+ `opencode.json`. On session completion (`session.execution.succeeded`, with
16
+ V1's `session.idle` kept as a deduped fallback) it runs the deterministic
17
+ gates against the dirty diff and nudges the session to fix blocking
18
+ findings (fingerprint-deduped, capped at `gates.session.maxNudges`, one
19
+ nudge per completion even with multiple opencode locations loaded
20
+ (single-actor lease on plugin storage). Session directories come from the
21
+ event's location, and diagnostics go to `<repo>/.aiwrangler/plugin.log`.
22
+ The V2 half activates only when `@opencode/plugin` resolves; V1 installs
23
+ keep working unchanged.
24
+ - `runDeterministic`, `formatFindings`, and `diffFingerprint` are now exported
25
+ from the package root (previously internal, forcing external consumers to
26
+ import from `src/` paths).
27
+ - `verify:plugin` (part of `npm test`) asserts the built dual entrypoint:
28
+ V1 named export, V2 `id`/`setup`, and the `server()` bridge.
29
+
30
+ ### Changed
31
+
32
+ - `@opencode/plugin` (>=2.1.1) is an optional peer dependency, like
33
+ `@opencode-ai/plugin`: neither host SDK is required at install time.
34
+
8
35
  ## 0.1.1 - 2026-10-19
9
36
 
10
37
  ### Added
package/README.md CHANGED
@@ -222,27 +222,46 @@ npm link
222
222
 
223
223
  ### opencode (native)
224
224
 
225
- Make the package resolvable from the opencode config dir (the `npm link`
226
- above covers it), then add two small files:
225
+ **OpenCode 2.x**: add the plugin package to `opencode.json` (global
226
+ `~/.config/opencode/opencode.json`, or the project's own):
227
227
 
228
- `~/.config/opencode/tools/wrangler.ts`:
228
+ ```jsonc
229
+ {
230
+ "$schema": "https://opencode.ai/config.json",
231
+ "plugins": [{ "package": "@kodr/aiwrangler" }]
232
+ }
233
+ ```
234
+
235
+ The plugin is inert in repos without an `aiwrangler.config.json` (or
236
+ `AIWRANGLER_CONFIG`), and when `gates.session.nudge` is off. On session
237
+ completion it runs the deterministic gates against the dirty diff and
238
+ injects a fix-nudge into the session (fingerprint-deduped, capped at
239
+ `gates.session.maxNudges`, exactly one nudge per completion even when
240
+ several opencode locations are loaded. Its diagnostics land in
241
+ `<repo>/.aiwrangler/plugin.log`.
242
+
243
+ **OpenCode 1.x (legacy)**: make the package resolvable from the opencode
244
+ config dir (the `npm link` above covers it), then add two small files.
245
+ `~/.config/opencode/plugins/wrangler-watch.ts` re-exports the named export:
229
246
 
230
247
  ```ts
231
- export { wrangler_config, wrangler_plan, wrangler_check, wrangler_ack, wrangler_override, wrangler_loop_reset } from "@kodr/aiwrangler/tools"
248
+ export { WranglerWatchPlugin } from "@kodr/aiwrangler/plugin"
232
249
  ```
233
250
 
234
- `~/.config/opencode/plugins/wrangler-watch.ts`:
251
+ `~/.config/opencode/tools/wrangler.ts` re-exports the native tools:
235
252
 
236
253
  ```ts
237
- export { WranglerWatchPlugin } from "@kodr/aiwrangler/plugin"
254
+ export { wrangler_config, wrangler_plan, wrangler_check, wrangler_ack, wrangler_override, wrangler_loop_reset } from "@kodr/aiwrangler/tools"
238
255
  ```
239
256
 
240
- Restart opencode. The tools appear in every session, and the plugin nudges
241
- the agent to run the gates at the right moments. `@opencode-ai/plugin` is an
242
- optional peer dependency: opencode provides it, and the MCP server and the
243
- `aiwrangler` CLI do not load it. Prefer the MCP server
257
+ Restart opencode. The MCP server and the plugin compose and do not overlap:
258
+ MCP gives the tools on demand (`wrangler_config`, `wrangler_check`,
259
+ acknowledgements), and the plugin adds the automatic nudge loop at session
260
+ completion. `@opencode-ai/plugin` and `@opencode/plugin` are both optional
261
+ peer dependencies: the host provides the one it runs, and the MCP server
262
+ and the `aiwrangler` CLI load neither. Prefer the MCP server
244
263
  inside opencode for approvals: it can show an elicitation form in the
245
- editor. The native tools above have no MCP session, so a write falls back
264
+ editor. The 1.x native tools have no MCP session, so a write falls back
246
265
  to a desktop dialog. Neither path treats the agent's arguments as a yes.
247
266
 
248
267
  ### Any MCP client (Cursor, Claude Code, Windsurf, ...)
@@ -410,16 +429,19 @@ AIWRANGLER_MODEL=jev-1.13.0
410
429
  | `diffs/<hash>.txt` | capped snapshot of the exact diff each check judged |
411
430
  | `cache/` | content-addressed semantic answers (replay without re-billing) |
412
431
  | `runtime-overrides.json` | active runtime overrides, if any (auto-expire on PASS) |
432
+ | `watch-state.json` | session-nudge loop state: last diff fingerprint, nudge count, green announcement |
433
+ | `plugin.log` | tail-truncated diagnostics from the opencode nudge plugin |
413
434
 
414
435
  ## Development
415
436
 
416
437
  ```
417
438
  npm run build # library, owner CLI, and MCP server bundles
418
439
  npm run typecheck
419
- npm test # verify:p0–p4, verify:mcp, and verify:cli (what CI gates on)
440
+ npm test # verify:p0–p4, verify:mcp, verify:cli, and verify:plugin (what CI gates on)
420
441
  npm run verify:p0 # config, corpus parsing, hash stability
421
442
  npm run verify:mcp # MCP handshake; mock client accepts and declines elicitation
422
443
  npm run verify:cli # TTY confirmation, --yes refusal, injected desktop dialog, diff-hash binding
444
+ npm run verify:plugin # built dual entrypoint: V1 named export + V2 default export
423
445
  ```
424
446
 
425
447
  `verify:p1` through `verify:p4` cover rule detection, the plan gate, the
@@ -1,13 +1,14 @@
1
1
  import {
2
2
  effectiveTruth,
3
3
  formatReport,
4
+ loadCorpus,
4
5
  runCheck,
5
6
  runPlanGate,
6
7
  sectionsOfKind
7
- } from "./chunk-2CIV4KGU.js";
8
+ } from "./chunk-RZWCNEZS.js";
8
9
  import {
9
10
  Jev
10
- } from "./chunk-EDVWHCQV.js";
11
+ } from "./chunk-HT3QDS3C.js";
11
12
  import {
12
13
  Ledger,
13
14
  activeThresholds,
@@ -17,10 +18,9 @@ import {
17
18
  findConfigFile,
18
19
  flattenGates,
19
20
  loadConfig,
20
- loadCorpus,
21
21
  readRuntimeOverridesFile,
22
22
  writeRuntimeOverridesFile
23
- } from "./chunk-EAQASAXL.js";
23
+ } from "./chunk-F3YFQ4NW.js";
24
24
 
25
25
  // src/osdialog.ts
26
26
  import { spawn } from "child_process";
@@ -519,4 +519,4 @@ export {
519
519
  requestHumanOverride,
520
520
  loopResetGate
521
521
  };
522
- //# sourceMappingURL=chunk-NQFO47CV.js.map
522
+ //# sourceMappingURL=chunk-4D4NYPL3.js.map
@@ -1,13 +1,3 @@
1
- // src/types.ts
2
- var PRECEDENCE = {
3
- ruling: 0,
4
- paper: 1,
5
- door: 1,
6
- spec: 2,
7
- doc: 3,
8
- nonRuling: 4
9
- };
10
-
11
1
  // src/jsonc.ts
12
2
  function stripJsonc(text) {
13
3
  let out = "";
@@ -322,374 +312,12 @@ function shortHash(text) {
322
312
  return sha256Hex(text).slice(0, 12);
323
313
  }
324
314
 
325
- // src/groundtruth/sectionize.ts
326
- function splitMarkdown(md) {
327
- const lines = md.split("\n");
328
- const sections = [];
329
- let current = null;
330
- for (let i = 0; i < lines.length; i++) {
331
- const line = lines[i] ?? "";
332
- const m = line.match(/^(#{1,6})\s+(.*)$/);
333
- if (m) {
334
- if (current) sections.push(current);
335
- current = { level: m[1].length, title: m[2].trim(), text: "", line: i + 1 };
336
- } else if (current) {
337
- current.text += line + "\n";
338
- }
339
- }
340
- if (current) sections.push(current);
341
- return sections.map((s) => ({ ...s, text: s.text.trim() }));
342
- }
343
- function slug(title) {
344
- return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
345
- }
346
-
347
- // src/groundtruth/parsers.ts
348
- function section(partial) {
349
- return { precedence: PRECEDENCE.doc, ...partial };
350
- }
351
- var ROMAN_RE = /^[IVX]+(?:-[A-Z])?$/;
352
- function parseSpec(md, source) {
353
- const out = [];
354
- for (const sec of splitMarkdown(md)) {
355
- const m = sec.title.match(/^(\d+)\.\s+(.*)$/);
356
- if (!m) continue;
357
- const title = m[2];
358
- const ref = title.match(/\(([^)]+)\)\s*$/);
359
- const paperRef = ref && ROMAN_RE.test(ref[1]) ? ref[1] : void 0;
360
- out.push(
361
- section({
362
- id: `SPEC-${m[1]}`,
363
- kind: "spec",
364
- title,
365
- text: sec.text,
366
- source,
367
- status: "info",
368
- ...paperRef ? { paperRef } : {},
369
- precedence: PRECEDENCE.spec
370
- })
371
- );
372
- }
373
- return out;
374
- }
375
- function parseRules(md, source) {
376
- const lines = md.split("\n");
377
- const out = [];
378
- let current = null;
379
- const flush = () => {
380
- if (!current) return;
381
- out.push(
382
- section({
383
- id: `RULE-${current.n}`,
384
- kind: "rule",
385
- title: current.lines[0].trim(),
386
- text: current.lines.join("\n").trim(),
387
- source,
388
- status: "info",
389
- precedence: PRECEDENCE.spec
390
- })
391
- );
392
- current = null;
393
- };
394
- for (const line of lines) {
395
- const m = line.match(/^(\d+)\.\s+(.*)$/);
396
- if (m) {
397
- flush();
398
- current = { n: m[1], lines: [m[2]] };
399
- continue;
400
- }
401
- if (current && (/^\s{2,}/.test(line) || line.trim() === "")) {
402
- if (line.trim() === "" && current.lines[current.lines.length - 1] === "") continue;
403
- current.lines.push(line.trim() === "" ? "" : line.trim());
404
- continue;
405
- }
406
- flush();
407
- }
408
- flush();
409
- return out;
410
- }
411
- function parseDivergences(md, source) {
412
- const lines = md.split("\n");
413
- const out = [];
414
- let topic = "";
415
- let current = null;
416
- const flush = () => {
417
- if (!current) return;
418
- const body = current.lines.join("\n").trim();
419
- const marker = body.indexOf("**RULED");
420
- const ruled = marker >= 0;
421
- const ruling = ruled ? body.slice(marker).replace(/\*\*/g, "").replace(/\s+/g, " ").trim().slice(0, 800) : void 0;
422
- out.push(
423
- section({
424
- id: `DIV-D-${current.id}`,
425
- kind: "divergence",
426
- title: `${current.topic ? current.topic + ": " : ""}${current.lines[0].trim()}`,
427
- text: body,
428
- source,
429
- status: ruled ? "ruled" : "open",
430
- ...ruled && ruling ? { ruling } : {},
431
- precedence: ruled ? PRECEDENCE.ruling : PRECEDENCE.nonRuling
432
- })
433
- );
434
- current = null;
435
- };
436
- for (const line of lines) {
437
- const h = line.match(/^##\s+(.*)$/);
438
- if (h) {
439
- flush();
440
- topic = h[1].trim();
441
- continue;
442
- }
443
- const m = line.match(/^D-(\d+)\s+(.*)$/);
444
- if (m) {
445
- flush();
446
- current = { id: m[1], lines: [m[2]], topic };
447
- continue;
448
- }
449
- if (current) current.lines.push(line);
450
- }
451
- flush();
452
- return out;
453
- }
454
- function parseQuestions(md, source) {
455
- const lines = md.split("\n");
456
- const out = [];
457
- let heading = "";
458
- let current = null;
459
- const flush = () => {
460
- if (!current) return;
461
- const body = current.lines.join("\n").trim();
462
- const marker = body.indexOf("**ANSWERED");
463
- const answered = marker >= 0;
464
- const ruling = answered ? body.slice(marker).replace(/\*\*/g, "").replace(/\s+/g, " ").trim().slice(0, 800) : void 0;
465
- out.push(
466
- section({
467
- id: `Q-${current.n}`,
468
- kind: "question",
469
- title: current.lines[0].trim(),
470
- text: body,
471
- source,
472
- status: answered ? "answered" : "open",
473
- ...answered && ruling ? { ruling } : {},
474
- precedence: answered ? PRECEDENCE.ruling : PRECEDENCE.nonRuling
475
- })
476
- );
477
- current = null;
478
- };
479
- for (const line of lines) {
480
- const h = line.match(/^##\s+(.*)$/);
481
- if (h) {
482
- flush();
483
- heading = h[1].trim();
484
- continue;
485
- }
486
- const m = line.match(/^(\d+)\.\s+(.*)$/);
487
- if (m) {
488
- flush();
489
- current = { n: m[1], lines: [m[2]], heading };
490
- continue;
491
- }
492
- if (current) current.lines.push(line);
493
- }
494
- flush();
495
- return out;
496
- }
497
- function parsePlan(md, source) {
498
- const out = [];
499
- for (const sec of splitMarkdown(md)) {
500
- if (!/one-way door|launch gate/i.test(sec.title)) continue;
501
- out.push(
502
- section({
503
- id: `DOOR-${slug(sec.title) || out.length + 1}`,
504
- kind: "door",
505
- title: sec.title,
506
- text: sec.text,
507
- source,
508
- status: "info",
509
- precedence: PRECEDENCE.door
510
- })
511
- );
512
- }
513
- return out;
514
- }
515
- function parseDoc(md, source, label) {
516
- return splitMarkdown(md).filter((s) => s.text.length > 0).map(
517
- (sec, i) => section({
518
- id: `DOC-${label}-${String(i + 1).padStart(2, "0")}`,
519
- kind: "doc",
520
- title: sec.title || "(intro)",
521
- text: sec.text,
522
- source,
523
- status: "info",
524
- precedence: PRECEDENCE.doc
525
- })
526
- );
527
- }
528
- var PAPER_HEADING_RE = /^([IVX]+(?:-[A-Z])?)?\.?\s{1,4}(.+)$/;
529
- var ROMAN_ONLY_RE = /^[IVX]+$/;
530
- function mergeSpacedCaps(title) {
531
- const tokens = title.split(/\s+/).filter(Boolean);
532
- const merged = [];
533
- let i = 0;
534
- while (i < tokens.length) {
535
- const tok = tokens[i];
536
- const next = tokens[i + 1];
537
- if (next !== void 0 && /^-?[A-Z]$/.test(tok) && /^[A-Z]/.test(next) && next.length > 1) {
538
- merged.push((tok.startsWith("-") ? tok : "") + next);
539
- i += 2;
540
- continue;
541
- }
542
- if (tok.length === 1 && /^[A-Z]$/.test(tok) && next !== void 0 && /^[A-Z]/.test(next)) {
543
- merged.push(tok + next);
544
- i += 2;
545
- continue;
546
- }
547
- merged.push(tok);
548
- i++;
549
- }
550
- return merged.join(" ").replace(/ -/g, "-").replace(/\s+,/g, ",");
551
- }
552
- function capsRatio(s) {
553
- const letters = s.replace(/[^A-Za-z]/g, "");
554
- if (letters.length === 0) return 0;
555
- const upper = letters.replace(/[^A-Z]/g, "");
556
- return upper.length / letters.length;
557
- }
558
- function parsePaper(text, source) {
559
- const lines = text.split("\n").map((l) => l.replace(/^\f/, ""));
560
- const byId = /* @__PURE__ */ new Map();
561
- let current = null;
562
- const looksLikeCapsFragment = (line) => {
563
- const t = line.trim();
564
- return t.length >= 3 && t.length <= 40 && !/[.:;]$/.test(t) && !/\.\s*\.\s*\./.test(t) && capsRatio(t) >= 0.6;
565
- };
566
- const flush = () => {
567
- if (!current) return;
568
- const body = current.lines.join("\n").trim();
569
- const existing = byId.get(current.id);
570
- if (existing) {
571
- existing.text = (existing.text + "\n" + body).trim();
572
- } else {
573
- byId.set(
574
- current.id,
575
- section({
576
- id: `PAPER-${current.id}`,
577
- kind: "paper",
578
- title: current.title,
579
- text: body,
580
- source,
581
- status: "info",
582
- precedence: PRECEDENCE.paper
583
- })
584
- );
585
- }
586
- current = null;
587
- };
588
- for (let li = 0; li < lines.length; li++) {
589
- const line = lines[li] ?? "";
590
- if (/\.\s*\.\s*\./.test(line)) continue;
591
- const m = line.match(PAPER_HEADING_RE);
592
- if (m && m[1] && !ROMAN_ONLY_RE.test(m[2] ?? "") && !/^[ivx]+$/.test(m[1])) {
593
- let title = mergeSpacedCaps(m[2] ?? "");
594
- let consumed = 1;
595
- while (li + consumed < lines.length && looksLikeCapsFragment(lines[li + consumed] ?? "") && title.split(/\s+/).length < 10) {
596
- title = title + " " + mergeSpacedCaps((lines[li + consumed] ?? "").trim());
597
- consumed++;
598
- }
599
- const letters = title.replace(/[^A-Za-z]/g, "");
600
- if (letters.length >= 4 && capsRatio(title) >= 0.7) {
601
- flush();
602
- current = { id: m[1], title: title.replace(/\s{2,}/g, " ").trim(), lines: [] };
603
- li += consumed - 1;
604
- continue;
605
- }
606
- }
607
- if (current) current.lines.push(line);
608
- }
609
- flush();
610
- return [...byId.values()];
611
- }
612
-
613
- // src/groundtruth/whitepaper.ts
315
+ // src/git/diff.ts
614
316
  import { spawnSync } from "child_process";
615
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync2 } from "fs";
317
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
616
318
  import { join as join3 } from "path";
617
- function ensurePaperText(config, root) {
618
- const pdf = expandPath(config.corpus.whitepaper, root);
619
- const cacheDir = expandPath(config.cache.dir, root);
620
- const cached = join3(cacheDir, "whitepaper.txt");
621
- const pdfMtime = existsSync3(pdf) ? statSync(pdf).mtimeMs : 0;
622
- if (existsSync3(cached) && statSync(cached).mtimeMs >= pdfMtime) {
623
- return { text: readFileSync3(cached, "utf8"), extracted: false };
624
- }
625
- if (!existsSync3(pdf)) return { text: null, extracted: false };
626
- const res = spawnSync("pdftotext", [pdf, "-"], { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
627
- if (res.error || res.status !== 0) {
628
- const reason = res.error ? res.error.message : `pdftotext exit ${res.status}`;
629
- throw new Error(`whitepaper extraction failed (${reason}); is poppler-utils installed?`);
630
- }
631
- const text = res.stdout;
632
- if (text.split(/\s+/).length < 1e3) throw new Error("whitepaper extraction produced suspiciously little text; refusing to cache");
633
- mkdirSync2(cacheDir, { recursive: true });
634
- writeFileSync2(cached, text, "utf8");
635
- return { text, extracted: true };
636
- }
637
-
638
- // src/groundtruth/corpus.ts
639
- import { readFileSync as readFileSync4 } from "fs";
640
- function slugFromPath(rel) {
641
- return rel.toLowerCase().replace(/\.[a-z0-9]+$/i, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
642
- }
643
- function readOr(path, fallback) {
644
- try {
645
- return { text: readFileSync4(path, "utf8"), sections: [] };
646
- } catch {
647
- return { text: null, sections: fallback };
648
- }
649
- }
650
- function loadCorpus(config, root) {
651
- const sections = [];
652
- const sources = [];
653
- const addFile = (path, parse) => {
654
- const resolved = expandPath(path, root);
655
- const { text } = readOr(resolved, []);
656
- if (text === null) return;
657
- sources.push({ path: resolved, hash: sha256Hex(text) });
658
- sections.push(...parse(text, resolved));
659
- };
660
- addFile(config.corpus.spec, parseSpec);
661
- addFile(config.corpus.rules, parseRules);
662
- addFile(config.corpus.divergences, parseDivergences);
663
- addFile(config.corpus.questions, parseQuestions);
664
- addFile(config.corpus.plan, parsePlan);
665
- for (const doc of config.corpus.docs) {
666
- const resolved = expandPath(doc, root);
667
- let label = doc.split("/").pop()?.replace(/\.[a-z]+$/i, "") ?? "doc";
668
- const rel = resolved.startsWith(root + "/") ? resolved.slice(root.length + 1) : null;
669
- if (rel && rel.includes("/")) label = slugFromPath(rel);
670
- addFile(doc, (md, source) => parseDoc(md, source, label));
671
- }
672
- const paper = ensurePaperText(config, root);
673
- if (paper.text !== null) {
674
- const paperPath = expandPath(config.corpus.whitepaper, root);
675
- sources.push({ path: paperPath, hash: sha256Hex(paper.text) });
676
- sections.push(...parsePaper(paper.text, paperPath));
677
- }
678
- const hash = sha256Hex(JSON.stringify({ sources: sources.map((s) => [s.path, s.hash]).sort((a, b) => (a[0] ?? "").localeCompare(b[0] ?? "")), ids: sections.map((s) => s.id).sort() }));
679
- return { sections, sources, hash, paperText: paper.text ?? "" };
680
- }
681
- function loadCorpusFrom(startDir) {
682
- const loaded = loadConfig(startDir);
683
- const corpus = loadCorpus(loaded.config, loaded.root);
684
- return { loaded, corpus };
685
- }
686
-
687
- // src/git/diff.ts
688
- import { spawnSync as spawnSync2 } from "child_process";
689
- import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
690
- import { join as join4 } from "path";
691
319
  function git(repoDir, args, timeoutMs = 3e4) {
692
- const res = spawnSync2("git", ["-C", repoDir, ...args], { encoding: "utf8", timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 });
320
+ const res = spawnSync("git", ["-C", repoDir, ...args], { encoding: "utf8", timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 });
693
321
  if (res.error) return { code: null, stdout: "", stderr: String(res.error) };
694
322
  return { code: res.status, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
695
323
  }
@@ -757,8 +385,8 @@ function repoDirs(config, root) {
757
385
  const superPath = expandPath(config.repos.super, root);
758
386
  const dirs = [{ repo: "(super)", dir: superPath }];
759
387
  for (const member of config.repos.members) {
760
- const dir = join4(superPath, member);
761
- if (existsSync4(join4(dir, ".git"))) dirs.push({ repo: member, dir });
388
+ const dir = join3(superPath, member);
389
+ if (existsSync3(join3(dir, ".git"))) dirs.push({ repo: member, dir });
762
390
  }
763
391
  return dirs;
764
392
  }
@@ -808,7 +436,7 @@ function dirtySet(config, root) {
808
436
  seen.add(path);
809
437
  if (untracked.includes(path)) {
810
438
  try {
811
- const content = readFileSync5(join4(dir, path), "utf8");
439
+ const content = readFileSync3(join3(dir, path), "utf8");
812
440
  const lines = content.split("\n");
813
441
  files.push({
814
442
  path,
@@ -1102,7 +730,7 @@ function formatFindings(findings) {
1102
730
  }
1103
731
 
1104
732
  // src/loop.ts
1105
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
733
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
1106
734
  import { dirname as dirname3 } from "path";
1107
735
  function diffFingerprint(diffText) {
1108
736
  return sha256Hex(diffText);
@@ -1113,9 +741,9 @@ var LoopTracker = class {
1113
741
  constructor(statePath) {
1114
742
  this.path = statePath;
1115
743
  this.state = { cycles: [] };
1116
- if (existsSync5(statePath)) {
744
+ if (existsSync4(statePath)) {
1117
745
  try {
1118
- this.state = JSON.parse(readFileSync6(statePath, "utf8"));
746
+ this.state = JSON.parse(readFileSync4(statePath, "utf8"));
1119
747
  } catch {
1120
748
  this.state = { cycles: [] };
1121
749
  }
@@ -1126,13 +754,13 @@ var LoopTracker = class {
1126
754
  }
1127
755
  record(cycle) {
1128
756
  this.state.cycles = [...this.state.cycles.slice(-9), cycle];
1129
- mkdirSync3(dirname3(this.path), { recursive: true });
1130
- writeFileSync3(this.path, JSON.stringify(this.state), "utf8");
757
+ mkdirSync2(dirname3(this.path), { recursive: true });
758
+ writeFileSync2(this.path, JSON.stringify(this.state), "utf8");
1131
759
  }
1132
760
  reset() {
1133
761
  this.state = { cycles: [] };
1134
- if (existsSync5(this.path)) mkdirSync3(dirname3(this.path), { recursive: true });
1135
- writeFileSync3(this.path, JSON.stringify(this.state), "utf8");
762
+ if (existsSync4(this.path)) mkdirSync2(dirname3(this.path), { recursive: true });
763
+ writeFileSync2(this.path, JSON.stringify(this.state), "utf8");
1136
764
  }
1137
765
  evaluate(diffHash, reasons, maxCycles) {
1138
766
  const last = this.state.cycles[this.state.cycles.length - 1];
@@ -1173,7 +801,7 @@ function reasonsOverlap(a, b) {
1173
801
  }
1174
802
 
1175
803
  // src/ledger.ts
1176
- import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync7 } from "fs";
804
+ import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5 } from "fs";
1177
805
  import { dirname as dirname4 } from "path";
1178
806
  var Ledger = class {
1179
807
  path;
@@ -1184,12 +812,12 @@ var Ledger = class {
1184
812
  return this.path;
1185
813
  }
1186
814
  append(entry) {
1187
- mkdirSync4(dirname4(this.path), { recursive: true });
815
+ mkdirSync3(dirname4(this.path), { recursive: true });
1188
816
  appendFileSync(this.path, JSON.stringify(entry) + "\n", "utf8");
1189
817
  }
1190
818
  read() {
1191
- if (!existsSync6(this.path)) return [];
1192
- return readFileSync7(this.path, "utf8").split("\n").filter(Boolean).map((line) => {
819
+ if (!existsSync5(this.path)) return [];
820
+ return readFileSync5(this.path, "utf8").split("\n").filter(Boolean).map((line) => {
1193
821
  try {
1194
822
  return JSON.parse(line);
1195
823
  } catch {
@@ -1210,7 +838,6 @@ var Ledger = class {
1210
838
  };
1211
839
 
1212
840
  export {
1213
- PRECEDENCE,
1214
841
  stripJsonc,
1215
842
  parseJsonc,
1216
843
  RuntimeOverridesSchema,
@@ -1228,18 +855,6 @@ export {
1228
855
  activeThresholds,
1229
856
  sha256Hex,
1230
857
  shortHash,
1231
- splitMarkdown,
1232
- slug,
1233
- parseSpec,
1234
- parseRules,
1235
- parseDivergences,
1236
- parseQuestions,
1237
- parsePlan,
1238
- parseDoc,
1239
- parsePaper,
1240
- ensurePaperText,
1241
- loadCorpus,
1242
- loadCorpusFrom,
1243
858
  parseCargoJson,
1244
859
  checkRepoDiff,
1245
860
  checkCommitMessage,
@@ -1249,4 +864,4 @@ export {
1249
864
  LoopTracker,
1250
865
  Ledger
1251
866
  };
1252
- //# sourceMappingURL=chunk-EAQASAXL.js.map
867
+ //# sourceMappingURL=chunk-F3YFQ4NW.js.map