@salaros/ai-harness 0.3.3 → 0.3.4
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 +49 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salaros/ai-harness",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
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"
|
|
@@ -468,9 +468,12 @@ function decideText({ policy, raw, theirs, hasBase, adopt }, { baseText, recover
|
|
|
468
468
|
let from = hasBase ? baseText() : null;
|
|
469
469
|
if (from === null && policy === "reconcile") from = recoverBase(ours);
|
|
470
470
|
if (from === null && policy === "reconcile") from = "";
|
|
471
|
+
// A union table merges by row whether or not there is a base, and even an untouched copy goes
|
|
472
|
+
// through that merge: it may hold a row the upstream dropped and the project still needs.
|
|
473
|
+
if (from === null && policy === "union") from = "";
|
|
471
474
|
if (from === null) return keep;
|
|
472
475
|
|
|
473
|
-
if (ours === from) return { outcome: "written", bucket: "written", text: asFound(theirs, crlf) };
|
|
476
|
+
if (ours === from && policy !== "union") return { outcome: "written", bucket: "written", text: asFound(theirs, crlf) };
|
|
474
477
|
const merged = merge(from, ours, theirs);
|
|
475
478
|
if (merged.failed) return { outcome: "yours, merge failed", bucket: "kept", text: null };
|
|
476
479
|
const result = asFound(merged.text, crlf);
|
|
@@ -482,6 +485,31 @@ function decideText({ policy, raw, theirs, hasBase, adopt }, { baseText, recover
|
|
|
482
485
|
return { outcome: "merged", bucket: "merged", text: result };
|
|
483
486
|
}
|
|
484
487
|
|
|
488
|
+
// A union table, merged row by row rather than line by line: a row is keyed by its first
|
|
489
|
+
// tab-separated column, and the table is a set of them, so there is nothing to conflict over.
|
|
490
|
+
// The upstream's comments and order come first. Each of its rows is the project's where only the
|
|
491
|
+
// project changed it, or where both did, and the upstream's otherwise; a row the project deleted
|
|
492
|
+
// stays deleted. Every row of the project's the upstream lacks follows, whether the project added it
|
|
493
|
+
// or the upstream dropped it: the licence of a skill the upstream stopped shipping is still needed
|
|
494
|
+
// here, because the skills merge keeps the skill. All three texts are LF; `base` is null without a
|
|
495
|
+
// receipt, and then the project's copy of a row wins.
|
|
496
|
+
function mergeRows(base, ours, theirs) {
|
|
497
|
+
const rows = text => new Map((text || "").split("\n").filter(l => l.trim() && !l.startsWith("#")).map(l => [l.split("\t")[0], l]));
|
|
498
|
+
const was = rows(base), mine = rows(ours), up = rows(theirs);
|
|
499
|
+
const out = [];
|
|
500
|
+
for (const line of theirs.split("\n")) {
|
|
501
|
+
const key = line.split("\t")[0];
|
|
502
|
+
if (!line.trim() || line.startsWith("#") || !up.has(key)) { out.push(line); continue; }
|
|
503
|
+
const o = mine.get(key), b = was.get(key);
|
|
504
|
+
if (o === undefined) { if (b === undefined) out.push(line); continue; }
|
|
505
|
+
out.push(o === b ? line : o);
|
|
506
|
+
}
|
|
507
|
+
const extra = [...mine].filter(([key]) => !up.has(key)).map(([, line]) => line);
|
|
508
|
+
if (!extra.length) return out.join("\n");
|
|
509
|
+
while (out.length && out[out.length - 1] === "") out.pop();
|
|
510
|
+
return [...out, ...extra, ""].join("\n");
|
|
511
|
+
}
|
|
512
|
+
|
|
485
513
|
// ---------------------------------------------------------------- the plan
|
|
486
514
|
//
|
|
487
515
|
// Everything a run will do to the target, decided before anything is written: an install rewrites
|
|
@@ -516,8 +544,13 @@ function plan({ upstream, target, rows, head, ref, previous, options, stamp = {}
|
|
|
516
544
|
}
|
|
517
545
|
// --adopt is how a repo whose harness files are wrong gets them replaced, and the commonest way to
|
|
518
546
|
// reach that state is an install that wrote the receipt and kept a stale harness. So the run stays
|
|
519
|
-
// open at the recorded commit, and main() answers "nothing to update" only without --adopt.
|
|
520
|
-
|
|
547
|
+
// open at the recorded commit, and main() answers "nothing to update" only without --adopt. An
|
|
548
|
+
// optional part asked for by its flag keeps it open too, and then only that part has anything to do.
|
|
549
|
+
if (previous && base === head) {
|
|
550
|
+
notices.push(options.adopt
|
|
551
|
+
? `harness is already at ${head.slice(0, 8)} (${ref}); --adopt takes every harness file again anyway`
|
|
552
|
+
: `harness is already at ${head.slice(0, 8)} (${ref}); installing only the optional part(s) asked for`);
|
|
553
|
+
}
|
|
521
554
|
|
|
522
555
|
// A repo carrying a harness from before harness-lock.json existed. Without a base the rule below
|
|
523
556
|
// keeps every file that is already there, which protects the project's work and also preserves
|
|
@@ -589,7 +622,10 @@ function plan({ upstream, target, rows, head, ref, previous, options, stamp = {}
|
|
|
589
622
|
: decideText({ policy, raw: held, theirs, hasBase, adopt: options.adopt }, {
|
|
590
623
|
baseText: () => upstream.blob(base, file),
|
|
591
624
|
recoverBase: ours => recoverBase(upstream, file, ours),
|
|
592
|
-
|
|
625
|
+
// A union table has no lines to conflict over, and no base means the project's rows win.
|
|
626
|
+
merge: policy === "union"
|
|
627
|
+
? (from, ours, up) => ({ text: mergeRows(from, ours, up), conflicts: false, failed: false })
|
|
628
|
+
: threeWay,
|
|
593
629
|
});
|
|
594
630
|
line(outcome, bucket, text === null ? {} : { write: text });
|
|
595
631
|
}
|
|
@@ -800,6 +836,13 @@ function report({ entries, base, head, ref, target, check, options }) {
|
|
|
800
836
|
|
|
801
837
|
// ---------------------------------------------------------------- the run
|
|
802
838
|
|
|
839
|
+
// Whether the run has nothing to do: the receipt already names the upstream's head, and neither
|
|
840
|
+
// --adopt nor the flag of an optional part asks for more. The first --astro-docs usually comes after
|
|
841
|
+
// the harness is current, so an optional part is something to install even at the recorded commit.
|
|
842
|
+
function upToDate(previous, head, options, optional) {
|
|
843
|
+
return !!previous && previous.commit === head && !options.adopt && !optional.some(name => options.wants(name));
|
|
844
|
+
}
|
|
845
|
+
|
|
803
846
|
// Returns the exit code, and throws Stop for a run that could not start.
|
|
804
847
|
function main(args) {
|
|
805
848
|
const options = parseOptions(args);
|
|
@@ -821,7 +864,7 @@ function main(args) {
|
|
|
821
864
|
const unknown = unknownArgs(args, optional);
|
|
822
865
|
if (unknown.length) fail(`unknown argument(s): ${unknown.join(" ")}. Nothing was written; run with --help for the options.`);
|
|
823
866
|
const head = at(templateDir, ["rev-parse", "HEAD"]).output.trim();
|
|
824
|
-
if (previous
|
|
867
|
+
if (upToDate(previous, head, options, optional)) {
|
|
825
868
|
say(`harness is already at ${head.slice(0, 8)} (${ref}); nothing to update`);
|
|
826
869
|
return 0;
|
|
827
870
|
}
|
|
@@ -848,7 +891,7 @@ function main(args) {
|
|
|
848
891
|
// The plan and the decisions under it, so the suite can put a case in and read the answer out rather
|
|
849
892
|
// than building a git checkout to reach one branch. apply() is here for its dry run, which prints and
|
|
850
893
|
// writes nothing; main() writes to somebody's repository and is reached through the command line.
|
|
851
|
-
module.exports = { installerStamp, unknownArgs, mistypedArgs, usage, parseOptions, policyFor, plan, apply, decideText, decideBinary, lineCounts, overlap, NEAREST, skeletonLines };
|
|
894
|
+
module.exports = { installerStamp, upToDate, mergeRows, unknownArgs, mistypedArgs, usage, parseOptions, policyFor, plan, apply, decideText, decideBinary, lineCounts, overlap, NEAREST, skeletonLines };
|
|
852
895
|
|
|
853
896
|
if (require.main === module) {
|
|
854
897
|
try { process.exitCode = main(process.argv.slice(2)); }
|