@salaros/ai-harness 0.1.0 → 0.2.1
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/README.md +6 -2
- package/package.json +1 -1
- package/scripts/update-harness.js +103 -22
package/README.md
CHANGED
|
@@ -26,6 +26,10 @@ Running it again updates. The first run writes `harness-lock.json` naming the up
|
|
|
26
26
|
|
|
27
27
|
Skills are added and updated, never removed: one you vendored yourself survives every update, and `skills-lock.json` is merged as a union.
|
|
28
28
|
|
|
29
|
+
A repo whose harness predates `harness-lock.json` is the one case where "never touch what is already there" works against you: with no receipt there is no merge base, so the old harness stays and its checks then run against the new skills and fail, naming rules this version dropped. The run says so and names `--adopt`, which replaces every harness file with the upstream's — hooks, agents, scripts, `AGENTS.md`, and a `.claude/agents` that checked out as a text file rather than a symlink. It stays opt-in because it discards your edits to those files. `--dry-run --quiet` lists them first.
|
|
30
|
+
|
|
31
|
+
The run reports each path as it works on it: the policy, the mode Git records, what happened, and the path, with the skills tree collapsed to one line per skill. `--quiet` gives the summary alone.
|
|
32
|
+
|
|
29
33
|
## Getting started
|
|
30
34
|
|
|
31
35
|
1. **Windows only, before cloning:** enable Developer Mode (Settings → System → For developers) and run `git config --global core.symlinks true`. The `.claude/` folder is tracked as symlinks; without this, Git checks them out as text files and Claude Code sees no skills.
|
|
@@ -41,14 +45,14 @@ Skills are added and updated, never removed: one you vendored yourself survives
|
|
|
41
45
|
| `src/`, `tests/`, `scripts/`, `tools/`, `docs/` | Product code, tests, repo automation, dev utilities, documents. Each has a README describing what belongs there. |
|
|
42
46
|
| `AGENTS.md` | The one file every agent reads: layout, where skills are, domain-language pointers. Kept short on purpose. |
|
|
43
47
|
| `CLAUDE.md` | One line, `@AGENTS.md`, because Claude Code reads `CLAUDE.md` rather than `AGENTS.md`. |
|
|
44
|
-
| `MEMORY.md` | The project facts (name, purpose, requirements location, stack, the frontend framework when the project has browser code, and the issue tracker when there is one), one per line. Written by the `project-init` skill; absent until it runs. |
|
|
48
|
+
| `MEMORY.md` | The project facts (name, purpose, the language its prose is written in, requirements location, stack, the frontend framework when the project has browser code, and the issue tracker when there is one), one per line. Written by the `project-init` skill; absent until it runs. |
|
|
45
49
|
| `TODO.md` | The loose-ends ledger: questions nobody has answered, assumptions taken on trust, and work knowingly left undone. Follows the [todo-md standard](https://github.com/todo-md/todo-md) -- a `# TODO` header, one entry per line as `- [ ] <text> #<tag> (<source>)`, `@user` and indented subtasks optional -- so its tooling reads this file. Written by the `loose-ends` skill and absent until the first entry, since an empty ledger says less than no ledger. It departs from the standard in one place: a settled entry is deleted in the commit that settles it rather than kept as `[x]` or `[-]`, because entries nobody will act on are context every agent re-reads. Needs no issue tracker. |
|
|
46
50
|
| `THIRD-PARTY-NOTICES.md`, `scripts/skill-licences.tsv` | Vendoring a skill copies someone else’s work here, and MIT and Apache-2.0 both ask that the copyright and permission notice travel with the copy; `npx skills` carries only what sits inside the skill folder, so an upstream keeping its licence at the repo root sends none. The TSV holds one row per upstream (SPDX id, copyright line, licence URL, and any restriction); `node scripts/skills.js notices` regenerates the notice from it and `skills-lock.json`. A new upstream with no row fails the harness suite. |
|
|
47
51
|
| `skills-lock.json` | Written by `npx skills`: source, path and hash of every installed skill. The single record of what is installed; `node scripts/skills.js install` restores from it. |
|
|
48
52
|
| `.agents/skills/<name>/` | The canonical, vendored copy of each skill (`SKILL.md` plus its reference files). |
|
|
49
53
|
| `.agents/hooks/` | Harness-neutral hook scripts (see [Hooks](#hooks)). |
|
|
50
54
|
| `.agents/agents/` | Agent definitions (see [Agents](#agents)). |
|
|
51
|
-
| `scripts/update-harness.js`, `scripts/harness-files.tsv`, `harness-lock.json` | The installer, the table of what travels and what does not, and the receipt naming the upstream commit a repo last took. The first two are the upstream’s own and are never installed; the receipt is written into the repo being updated. |
|
|
55
|
+
| `scripts/update-harness.js`, `scripts/harness-files.tsv`, `harness-lock.json` | The installer, the table of what travels and what does not, and the receipt naming the upstream commit a repo last took and the released installer that wrote it. The first two are the upstream’s own and are never installed; the receipt is written into the repo being updated. |
|
|
52
56
|
| `.agents/routing.md` | Route rows shared by some agents but not all, in a section per audience, plus the rule for what earns a row. What every agent needs is in `AGENTS.md` ("Working here") instead, since every session loads that. Above `agents/` because `.claude/agents` is a symlink to that folder and a harness reads everything in it as an agent. |
|
|
53
57
|
| `.claude/` | Claude Code wiring: `skills/*` and `agents` are symlinks into `.agents/`, `settings.json` wires the hooks. |
|
|
54
58
|
| `.githooks/`, `scripts/githooks-init.js` | Git hooks, run through `core.hooksPath` after `githooks-init.js` is run once per clone. They are the only shell scripts left, because Git runs them through its own `sh` on every OS; each is a two-line wrapper piping changed paths into a Node script. `post-merge` feeds `scripts/on-manifest-change.js`, which restores what `scripts/stacks.tsv` says (skills, npm, pnpm, yarn, NuGet, uv). `pre-commit` and `pre-push` both start with `scripts/check-initialised.js`, which refuses an unconfigured clone: `MEMORY.md` must record the six facts that describe the project itself (name, purpose, requirements, unit type, language, runtime), none of them left as a `<placeholder>`. The `Jira` line is not among them: a tracker is a choice, not a property of the code. `pre-commit` then pipes the staged `TODO.md` into `scripts/check-todo.js`, which checks the shape of the loose-ends ledger against the todo-md standard: the `# TODO` header, and an entry carrying a known tag (`#question`, `#assumption`, `#deferred`) and a source the chain would accept. Only checkbox lines are entries, every other line is prose, and an indented subtask inherits its parent's tag and source. It never asks for entries to exist, and it rejects `[x]` and `[-]`, because a settled entry is deleted rather than kept. `pre-commit` then feeds `scripts/check-staged-docs.js`, which blocks a commit that would break the documentation chain. `commit-msg` feeds `scripts/check-commit-msg.js`, which blocks a commit whose message is not a conventional commit (`<type>(<scope>)?!?: <description>`, subject at most 72 characters, a description of at least four words, and a body of real prose after a blank line — trailers such as `Refs:` are metadata and do not count as one); messages Git writes itself for a merge, revert, fixup or squash are left alone. A message citing no Jira issue key is warned about rather than rejected, to encourage the habit without blocking a change nobody has raised a ticket for; the key comes from `docs/agents/issue-tracker.md`, and any `PROJ-123` shape counts until `project-init` records the real one. A project whose `MEMORY.md` records `Issue tracker: none` is not warned: it has no key to cite. A tracker whose references are not `KEY-123` sets `Key format:` in `docs/agents/issue-tracker.md`, so a GitHub-Issues project is warned about `#42` instead. `pre-push` feeds `scripts/format-changed.js`, which checks that the files a push publishes are formatted, using the stack's own formatter from `scripts/stacks.tsv` (`prettier`, `dotnet format` or a project's Husky.NET task runner, `ruff`). It never rewrites files: it reports and blocks, and skips silently when the stack's formatter is not installed. Formatting is checked on push rather than on commit because some formatters load the whole project and cost seconds. The restore, chain and format scripts take `--dry-run`; `check-commit-msg.js` takes the message file, or reads stdin. `git commit --no-verify` and `git push --no-verify` skip them. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salaros/ai-harness",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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"
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
// node scripts/update-harness.js --from ../ai-harness use a checkout you already have, no clone
|
|
23
23
|
// node scripts/update-harness.js --astro-docs add tools/docs-site, the Astro renderer for the chain
|
|
24
24
|
// node scripts/update-harness.js --no-check install without proving it afterwards
|
|
25
|
+
// node scripts/update-harness.js --quiet the summary alone, no line per path
|
|
26
|
+
// node scripts/update-harness.js --adopt replace a harness that predates harness-lock.json
|
|
25
27
|
// Installing into a repo that has no harness yet, from anywhere:
|
|
26
28
|
// git clone https://github.com/salaros/ai-harness .harness && \
|
|
27
29
|
// node .harness/scripts/update-harness.js --from .harness --target . && rm -rf .harness
|
|
@@ -39,6 +41,7 @@ const argv = process.argv.slice(2);
|
|
|
39
41
|
const flag = name => argv.includes(name);
|
|
40
42
|
const value = (name, fallback) => { const i = argv.indexOf(name); return i >= 0 && argv[i + 1] ? argv[i + 1] : fallback; };
|
|
41
43
|
const dryRun = flag("--dry-run");
|
|
44
|
+
const adopt = flag("--adopt");
|
|
42
45
|
|
|
43
46
|
// ---------------------------------------------------------------- the target
|
|
44
47
|
|
|
@@ -80,6 +83,19 @@ function templateCheckout(ref) {
|
|
|
80
83
|
return { dir, temporary: true };
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
// The installer's own name and version, read from the package it ships inside rather than written
|
|
87
|
+
// down here, so a release cannot forget to update it. It answers what the upstream commit cannot:
|
|
88
|
+
// which released tool wrote this tree. Both routes land on the right file, because the script always
|
|
89
|
+
// sits in the scripts/ folder of either the npm package or a checkout of the upstream. Omitted
|
|
90
|
+
// rather than recorded as null when it cannot be read, so the receipt never claims a version it
|
|
91
|
+
// does not know.
|
|
92
|
+
function installer() {
|
|
93
|
+
try {
|
|
94
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, "..", "package.json"), "utf8"));
|
|
95
|
+
return pkg.name && pkg.version ? { installer: `${pkg.name}@${pkg.version}` } : {};
|
|
96
|
+
} catch { return {}; }
|
|
97
|
+
}
|
|
98
|
+
|
|
83
99
|
const at = (dir, args) => lib.run("git", ["-C", dir, ...args]);
|
|
84
100
|
|
|
85
101
|
// The upstream's version of a path at a commit, or null when the file did not exist there. Also how
|
|
@@ -142,9 +158,12 @@ function link(target, file, to) {
|
|
|
142
158
|
let existing = null;
|
|
143
159
|
try { existing = fs.lstatSync(full); } catch { /* absent */ }
|
|
144
160
|
if (existing) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
161
|
+
// Something of the project's is in the way -- or, in a repo whose harness predates the lock
|
|
162
|
+
// file, the link itself checked out as a text file holding a path, which is the failure that
|
|
163
|
+
// leaves an agent seeing no skills at all. --adopt is the only thing that replaces it.
|
|
164
|
+
if (!existing.isSymbolicLink()) { if (!adopt) return "kept"; fs.unlinkSync(full); }
|
|
165
|
+
else if (fs.readlinkSync(full).split(path.sep).join("/") === to) return null;
|
|
166
|
+
else fs.unlinkSync(full);
|
|
148
167
|
}
|
|
149
168
|
try {
|
|
150
169
|
fs.symlinkSync(to.split("/").join(path.sep), full, "dir");
|
|
@@ -173,6 +192,7 @@ const SKELETONS = {
|
|
|
173
192
|
"",
|
|
174
193
|
"- **Name:** <name>",
|
|
175
194
|
"- **Purpose:** <purpose>",
|
|
195
|
+
"- **Prose language:** <prose language>",
|
|
176
196
|
"- **Requirements:** <requirements>",
|
|
177
197
|
"- **Unit type:** <unit type>",
|
|
178
198
|
"- **Language:** <language>",
|
|
@@ -199,9 +219,9 @@ const SKELETONS = {
|
|
|
199
219
|
|
|
200
220
|
function skeletons(target) {
|
|
201
221
|
for (const [file, lines] of Object.entries(SKELETONS)) {
|
|
202
|
-
if (fs.existsSync(path.join(target, file))) continue;
|
|
222
|
+
if (fs.existsSync(path.join(target, file))) { step("seed", "100644", "yours", file); continue; }
|
|
203
223
|
write(target, file, lines.join("\n"));
|
|
204
|
-
|
|
224
|
+
step("seed", "100644", "created", file, "seeded");
|
|
205
225
|
}
|
|
206
226
|
}
|
|
207
227
|
|
|
@@ -228,6 +248,19 @@ const notes = { written: [], merged: [], conflicted: [], seeded: [], kept: [], s
|
|
|
228
248
|
const say = m => console.log(m);
|
|
229
249
|
function fail(m) { console.error(`update-harness: ${m}`); process.exit(1); }
|
|
230
250
|
|
|
251
|
+
// An install rewrites someone else's repository, so it says what it did to every path while it does
|
|
252
|
+
// it, and --quiet asks for the summary alone. The mode is worth a column of its own: a hook that
|
|
253
|
+
// lands 100644 gates nothing and a skill link written as a regular file leaves the agent with no
|
|
254
|
+
// skills, and both look installed. One call records the outcome and prints the line, so the running
|
|
255
|
+
// commentary and the summary below cannot drift apart.
|
|
256
|
+
const quiet = flag("--quiet");
|
|
257
|
+
const mode = f => f.link ? "120000" : f.exec ? "100755" : "100644";
|
|
258
|
+
function step(policy, m, outcome, file, bucket) {
|
|
259
|
+
if (bucket) notes[bucket].push(file);
|
|
260
|
+
if (!quiet) say(` ${policy.padEnd(9)}${m} ${outcome.padEnd(12)}${file}`);
|
|
261
|
+
}
|
|
262
|
+
const phase = m => { if (!quiet) say(`\n${m}`); };
|
|
263
|
+
|
|
231
264
|
function write(target, file, text, exec) {
|
|
232
265
|
const full = path.join(target, file);
|
|
233
266
|
if (dryRun) return;
|
|
@@ -263,6 +296,7 @@ function selfCheck(target, templateDir, head, files) {
|
|
|
263
296
|
write(target, file, text);
|
|
264
297
|
borrowed.push(file);
|
|
265
298
|
}
|
|
299
|
+
phase(`self check: ${borrowed.length} file(s) borrowed from the upstream suite`);
|
|
266
300
|
if (!borrowed.length) return { skipped: "the suite is already in this repo; run it yourself with node .agents/hooks/test.js" };
|
|
267
301
|
|
|
268
302
|
try {
|
|
@@ -300,17 +334,34 @@ function main() {
|
|
|
300
334
|
}
|
|
301
335
|
if (previous && base === head) { say(`harness is already at ${head.slice(0, 8)} (${ref}); nothing to update`); return; }
|
|
302
336
|
|
|
337
|
+
// A repo carrying a harness from before harness-lock.json existed. Without a base the rule
|
|
338
|
+
// below keeps every file that is already there, which protects the project's work and also
|
|
339
|
+
// preserves the old harness: its checks then run against the new skills and agents and fail,
|
|
340
|
+
// naming rules this version dropped. Worth saying out loud, because the run otherwise looks
|
|
341
|
+
// like a success.
|
|
342
|
+
const MARKERS = [".agents/hooks/lib.js", "scripts/lib.js", ".githooks/pre-commit"];
|
|
343
|
+
const stale = !previous && MARKERS.filter(f => fs.existsSync(path.join(target, f)));
|
|
344
|
+
if (stale && stale.length) {
|
|
345
|
+
if (adopt) say(`this repo has a harness but no ${LOCK}, and --adopt was given: harness files are replaced with ${ref}'s, and edits to them are lost`);
|
|
346
|
+
else say(`this repo has a harness (${stale.join(", ")}) but no ${LOCK}, so it predates the receipt and there is no merge base.\nEvery harness file already here is kept, which leaves old checks running against new skills. Re-run with --adopt to replace them, or --dry-run --quiet to list them first.`);
|
|
347
|
+
}
|
|
348
|
+
|
|
303
349
|
const rows = policies(templateDir);
|
|
304
350
|
const files = templateFiles(templateDir);
|
|
305
351
|
const skills = [];
|
|
306
352
|
|
|
307
|
-
|
|
353
|
+
phase(`${files.length} path(s) in ${ref} at ${head.slice(0, 8)}`);
|
|
354
|
+
for (const entry of files) {
|
|
355
|
+
const { file, link: isLink, exec } = entry;
|
|
308
356
|
const policy = policyFor(rows, file);
|
|
357
|
+
const m = mode(entry);
|
|
309
358
|
const theirs = blob(templateDir, head, file);
|
|
310
359
|
if (theirs === null) continue;
|
|
311
360
|
const full = path.join(target, file);
|
|
312
361
|
const exists = fs.existsSync(full);
|
|
313
362
|
|
|
363
|
+
// Not installed anywhere, and named in one line of the summary instead: sixty-five
|
|
364
|
+
// lines saying nothing happened bury the thirty-eight saying something did.
|
|
314
365
|
if (policy === "template") { notes.template.push(file); continue; }
|
|
315
366
|
if (isLink) {
|
|
316
367
|
// A skill link is relink's to make, once the directory it lives in exists: it knows
|
|
@@ -320,40 +371,51 @@ function main() {
|
|
|
320
371
|
continue;
|
|
321
372
|
}
|
|
322
373
|
const how = link(target, file, theirs.trim());
|
|
323
|
-
|
|
374
|
+
step(policy, m, how === "kept" ? "yours" : how || "unchanged", file, how);
|
|
324
375
|
continue;
|
|
325
376
|
}
|
|
377
|
+
// Reported one line per skill by mergeSkills below, not one per reference file: a skill
|
|
378
|
+
// is the unit a project installs, and its files run to several hundred.
|
|
326
379
|
if (policy === "skills") { skills.push(file); continue; }
|
|
327
380
|
// Reported only when the target actually has it: "left alone, yours" about a file the
|
|
328
381
|
// repo does not have names something that was never there.
|
|
329
|
-
if (policy === "skip") {
|
|
382
|
+
if (policy === "skip") { step(policy, m, exists ? "yours" : "absent", file, exists && "skipped"); continue; }
|
|
330
383
|
|
|
331
384
|
if (policy === "seed") {
|
|
332
|
-
if (exists) {
|
|
385
|
+
if (exists) { step(policy, m, "yours", file, "kept"); continue; }
|
|
333
386
|
write(target, file, theirs, exec);
|
|
334
|
-
|
|
387
|
+
step(policy, m, "created", file, "seeded");
|
|
335
388
|
continue;
|
|
336
389
|
}
|
|
337
390
|
// merge
|
|
338
|
-
if (!exists) { write(target, file, theirs, exec);
|
|
391
|
+
if (!exists) { write(target, file, theirs, exec); step(policy, m, "written", file, "written"); continue; }
|
|
339
392
|
const ours = fs.readFileSync(full, "utf8");
|
|
340
|
-
if (ours === theirs) continue;
|
|
341
|
-
if (base === null) {
|
|
393
|
+
if (ours === theirs) { step(policy, m, "unchanged", file); continue; }
|
|
394
|
+
if (base === null) {
|
|
395
|
+
if (!adopt) { step(policy, m, "yours, no base", file, "kept"); continue; }
|
|
396
|
+
write(target, file, theirs, exec);
|
|
397
|
+
step(policy, m, "adopted", file, "written");
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
342
400
|
const from = blob(templateDir, base, file);
|
|
343
|
-
if (from === null) {
|
|
344
|
-
if (ours === from) { write(target, file, theirs, exec);
|
|
401
|
+
if (from === null) { step(policy, m, "yours, new here", file, "kept"); continue; }
|
|
402
|
+
if (ours === from) { write(target, file, theirs, exec); step(policy, m, "written", file, "written"); continue; }
|
|
345
403
|
const merged = threeWay(from, ours, theirs);
|
|
346
|
-
if (merged.failed) {
|
|
404
|
+
if (merged.failed) { step(policy, m, "yours, merge failed", file, "kept"); continue; }
|
|
347
405
|
write(target, file, merged.text, exec);
|
|
348
|
-
(merged.conflicts
|
|
406
|
+
if (merged.conflicts) step(policy, m, "CONFLICT", file, "conflicted");
|
|
407
|
+
else step(policy, m, "merged", file, "merged");
|
|
349
408
|
}
|
|
350
409
|
|
|
410
|
+
phase("skeletons a project starts with");
|
|
351
411
|
skeletons(target);
|
|
412
|
+
phase("skills, merged by name");
|
|
352
413
|
mergeSkills(target, templateDir, head, skills);
|
|
353
414
|
|
|
354
415
|
if (!dryRun) {
|
|
355
416
|
fs.writeFileSync(lockPath, JSON.stringify({
|
|
356
|
-
template: TEMPLATE, ref, commit: head,
|
|
417
|
+
template: TEMPLATE, ref, commit: head, ...installer(),
|
|
418
|
+
updated: new Date().toISOString().slice(0, 10),
|
|
357
419
|
}, null, 2) + "\n");
|
|
358
420
|
finish(target);
|
|
359
421
|
// After finish(), because the suite checks the links relink has just written.
|
|
@@ -376,18 +438,29 @@ function mergeSkills(target, templateDir, head, files) {
|
|
|
376
438
|
ourLock.skills = ourLock.skills || {};
|
|
377
439
|
|
|
378
440
|
const mine = new Set(Object.keys(ourLock.skills));
|
|
441
|
+
// One line per skill, not per file. Outcome is decided across the whole folder: a skill counts
|
|
442
|
+
// as changed the moment any file in it did, and only an untouched folder reads "unchanged".
|
|
443
|
+
const outcomes = new Map();
|
|
444
|
+
const seen = name => outcomes.get(name) || outcomes.set(name, { added: 0, updated: 0, files: 0 }).get(name);
|
|
379
445
|
for (const file of files) {
|
|
380
446
|
if (!file.startsWith(SKILLS)) continue; // .claude/skills links are rebuilt, not copied
|
|
381
447
|
const name = file.slice(SKILLS.length).split("/")[0];
|
|
448
|
+
const tally = seen(name);
|
|
449
|
+
tally.files++;
|
|
382
450
|
// A skill the project installed under a name the upstream also uses stays the project's.
|
|
383
|
-
if (mine.has(name) && !theirLock.skills[name]) continue;
|
|
451
|
+
if (mine.has(name) && !theirLock.skills[name]) { tally.yours = true; continue; }
|
|
384
452
|
const text = blob(templateDir, head, file);
|
|
385
453
|
if (text === null) continue;
|
|
386
454
|
const full = path.join(target, file);
|
|
387
455
|
const exists = fs.existsSync(full);
|
|
388
456
|
if (exists && fs.readFileSync(full, "utf8") === text) continue;
|
|
389
457
|
write(target, file, text);
|
|
390
|
-
(exists
|
|
458
|
+
if (exists) { tally.updated++; notes.merged.push(file); }
|
|
459
|
+
else { tally.added++; notes.written.push(file); }
|
|
460
|
+
}
|
|
461
|
+
for (const [name, t] of [...outcomes].sort()) {
|
|
462
|
+
const what = t.yours ? "yours" : t.added ? "added" : t.updated ? "updated" : "unchanged";
|
|
463
|
+
step("skills", "100644", what, `${SKILLS}${name} (${t.files} file(s))`);
|
|
391
464
|
}
|
|
392
465
|
for (const [name, entry] of Object.entries(theirLock.skills)) {
|
|
393
466
|
if (!ourLock.skills[name]) ourLock.skills[name] = entry;
|
|
@@ -400,6 +473,7 @@ function mergeSkills(target, templateDir, head, files) {
|
|
|
400
473
|
// project's lock file. Both are generated, so the install leaves a harness that works rather than a
|
|
401
474
|
// list of commands to remember.
|
|
402
475
|
function finish(target) {
|
|
476
|
+
phase("links and notices");
|
|
403
477
|
for (const [label, args] of [["links", ["relink"]], ["notices", ["notices"]]]) {
|
|
404
478
|
const r = lib.node([path.join(target, "scripts/skills.js"), ...args], { cwd: target });
|
|
405
479
|
say(r.status === 0 ? r.output : `${label}: ${r.output}`);
|
|
@@ -407,7 +481,14 @@ function finish(target) {
|
|
|
407
481
|
}
|
|
408
482
|
|
|
409
483
|
function report(target, head, ref, base) {
|
|
410
|
-
|
|
484
|
+
// Every path was named as it happened, so repeating the lists here doubles the output; a quiet
|
|
485
|
+
// run never saw them and gets them in full. Conflicts are listed either way: they are what the
|
|
486
|
+
// reader has to act on, and they belong beside the instructions for acting on them.
|
|
487
|
+
const list = (label, arr, always) => {
|
|
488
|
+
if (!arr.length) return;
|
|
489
|
+
if (quiet || always) say(`\n${label} (${arr.length}):\n ${arr.sort().join("\n ")}`);
|
|
490
|
+
else say(`\n${label}: ${arr.length}`);
|
|
491
|
+
};
|
|
411
492
|
say("");
|
|
412
493
|
say(dryRun ? `dry run against ${ref} at ${head.slice(0, 8)}` : `harness updated to ${ref} at ${head.slice(0, 8)}`);
|
|
413
494
|
if (!base) say("no merge base: this was an install, so nothing that already existed was changed");
|
|
@@ -422,7 +503,7 @@ function report(target, head, ref, base) {
|
|
|
422
503
|
say(`\nnot installed, the upstream's own (${notes.template.length}): ${named.join(", ")}, and the suite's fixtures`);
|
|
423
504
|
}
|
|
424
505
|
if (notes.conflicted.length) {
|
|
425
|
-
list("CONFLICTED, resolve the markers by hand", notes.conflicted);
|
|
506
|
+
list("CONFLICTED, resolve the markers by hand", notes.conflicted, true);
|
|
426
507
|
say(`\nEach one holds <<<<<<< yours / ======= / >>>>>>> upstream (new). Resolve them, then run the suite:\n node .agents/hooks/test.js`);
|
|
427
508
|
}
|
|
428
509
|
const check = notes.check;
|