@salaros/ai-harness 0.3.4 → 0.3.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salaros/ai-harness",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Installs and updates the agent harness from its upstream repository: hooks, skills, agents and the documentation chain, merged into an existing repository without touching its own work.",
5
5
  "bin": {
6
6
  "ai-harness": "scripts/update-harness.js"
@@ -340,8 +340,9 @@ function policies(templateDir) {
340
340
  // the docs site is the case, useful to some projects and dead weight in the rest.
341
341
  // `wants` answers whether the run asked for an optional part, so the table's meaning does not depend
342
342
  // on the process's own argv and a test can ask what a repo would get either way.
343
+ const rowFor = (rows, file) => rows.find(r => r.path.endsWith("/") ? file.startsWith(r.path) : file === r.path);
343
344
  function policyFor(rows, file, wants) {
344
- const row = rows.find(r => r.path.endsWith("/") ? file.startsWith(r.path) : file === r.path);
345
+ const row = rowFor(rows, file);
345
346
  if (!row) return "merge"; // anything the upstream ships and nobody classified is harness
346
347
  if (!row.policy.startsWith("optional:")) return row.policy;
347
348
  return wants(row.policy.slice("optional:".length)) ? "seed" : "template";
@@ -402,12 +403,31 @@ function threeWay(base, ours, theirs) {
402
403
  fs.writeFileSync(f("base"), base);
403
404
  fs.writeFileSync(f("ours"), ours);
404
405
  fs.writeFileSync(f("theirs"), theirs);
405
- const r = lib.run("git", ["merge-file", "-L", "yours", "-L", "upstream (base)", "-L", "upstream (new)",
406
+ const r = lib.run("git", ["merge-file", "--diff3", "-L", "yours", "-L", "upstream (base)", "-L", "upstream (new)",
406
407
  f("ours"), f("base"), f("theirs")]);
407
- return { text: fs.readFileSync(f("ours"), "utf8"), conflicts: r.status > 0, failed: r.status < 0 };
408
+ if (r.status < 0) return { text: "", conflicts: false, failed: true };
409
+ return settleDropped(fs.readFileSync(f("ours"), "utf8"));
408
410
  } finally { fs.rmSync(dir, { recursive: true, force: true }); }
409
411
  }
410
412
 
413
+ // The conflicts of a --diff3 merge, each settled or written the way this script always has: the
414
+ // project's side and the upstream's, no base. A conflict whose project side shares no line with the
415
+ // base is a section the project dropped or replaced with its own, as a project does with the harness's
416
+ // part of .gitignore, and it stays dropped: the upstream's edit is to text the project no longer has.
417
+ // A conflict with an empty base is both sides adding at one place, and stays a conflict.
418
+ function settleDropped(text) {
419
+ const HUNK = /^<{7} yours\n([\s\S]*?)^\|{7} upstream \(base\)\n([\s\S]*?)^={7}\n([\s\S]*?)^>{7} upstream \(new\)\n/gm;
420
+ const lines = s => s.split("\n").map(l => l.trim()).filter(Boolean);
421
+ let conflicts = 0;
422
+ const out = text.replace(HUNK, (all, ours, base, theirs) => {
423
+ const was = new Set(lines(base));
424
+ if (was.size && !lines(ours).some(l => was.has(l))) return ours;
425
+ conflicts++;
426
+ return `<<<<<<< yours\n${ours}=======\n${theirs}>>>>>>> upstream (new)\n`;
427
+ });
428
+ return { text: out, conflicts: conflicts > 0, failed: false };
429
+ }
430
+
411
431
  // Git checks a repo out with the platform's line endings, so a Windows working copy holds CRLF where
412
432
  // the upstream stores LF. Compared raw, every line of every file reads as changed: a copy nobody
413
433
  // touched reports as edited, and a real edit is buried in a whole-file conflict nobody can read. So
@@ -609,7 +629,12 @@ function plan({ upstream, target, rows, head, ref, previous, options, stamp = {}
609
629
  // does not have names something that was never there.
610
630
  if (policy === "skip") { line(exists ? "yours" : "absent", exists ? "skipped" : null); continue; }
611
631
  if (policy === "seed") {
632
+ // A seed file the recorded commit already shipped was laid down then, so its absence is
633
+ // the project deleting it, and it stays deleted. An optional part is the exception: its
634
+ // flag is the project asking for it now, whatever an earlier run left out.
635
+ const asked = (rowFor(rows, file) || { policy: "" }).policy.startsWith("optional:");
612
636
  if (exists) line("yours", "kept");
637
+ else if (!asked && base !== null && upstream.blob(base, file) !== null) line("deleted here", null);
613
638
  else line("created", "seeded", { write: theirs });
614
639
  continue;
615
640
  }
@@ -632,15 +657,20 @@ function plan({ upstream, target, rows, head, ref, previous, options, stamp = {}
632
657
 
633
658
  add({ phase: "skeletons a project starts with" });
634
659
  const hasIntent = target.exists(projectFacts.INTENT);
660
+ // The receipt lists the skeletons its run knew, so one missing on an update is one the project
661
+ // deleted, and it stays deleted; a skeleton added since still arrives. A receipt from before the
662
+ // list is taken to know them all, which every install since the skeletons began has laid down.
663
+ const known = new Set(previous && base !== null ? previous.skeletons || Object.keys(SKELETONS) : []);
635
664
  for (const [file, lines] of Object.entries(SKELETONS)) {
636
665
  if (target.exists(file)) add({ file, policy: "seed", mode: "100644", outcome: "yours", bucket: null });
666
+ else if (known.has(file)) add({ file, policy: "seed", mode: "100644", outcome: "deleted here", bucket: null });
637
667
  else add({ file, policy: "seed", mode: "100644", outcome: "created", bucket: "seeded", write: skeletonLines(file, lines, hasIntent).join("\n") });
638
668
  }
639
669
 
640
670
  add({ phase: "skills, merged by name" });
641
671
  entries.push(...planSkills(upstream, target, head, skills));
642
672
 
643
- add({ file: LOCK, silent: true, write: JSON.stringify({ template: TEMPLATE, ref, commit: head, ...stamp }, null, 2) + "\n" });
673
+ add({ file: LOCK, silent: true, write: JSON.stringify({ template: TEMPLATE, ref, commit: head, ...stamp, skeletons: Object.keys(SKELETONS) }, null, 2) + "\n" });
644
674
  return { entries, notices, base };
645
675
  }
646
676
 
@@ -891,7 +921,7 @@ function main(args) {
891
921
  // The plan and the decisions under it, so the suite can put a case in and read the answer out rather
892
922
  // than building a git checkout to reach one branch. apply() is here for its dry run, which prints and
893
923
  // writes nothing; main() writes to somebody's repository and is reached through the command line.
894
- module.exports = { installerStamp, upToDate, mergeRows, unknownArgs, mistypedArgs, usage, parseOptions, policyFor, plan, apply, decideText, decideBinary, lineCounts, overlap, NEAREST, skeletonLines };
924
+ module.exports = { installerStamp, upToDate, mergeRows, settleDropped, unknownArgs, mistypedArgs, usage, parseOptions, policyFor, plan, apply, decideText, decideBinary, lineCounts, overlap, NEAREST, skeletonLines };
895
925
 
896
926
  if (require.main === module) {
897
927
  try { process.exitCode = main(process.argv.slice(2)); }