@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 +1 -1
- package/scripts/update-harness.js +35 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salaros/ai-harness",
|
|
3
|
-
"version": "0.3.
|
|
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
|
|
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:
|
|
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)); }
|