@dombaras/agent-harness 0.1.5 → 0.1.7
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 +13 -0
- package/bin/agent-harness.js +81 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,6 +123,19 @@ npx @dombaras/agent-harness update --target /path/to/project
|
|
|
123
123
|
modified harness files. Your unrelated uncommitted work is never staged.
|
|
124
124
|
- `--no-commit` to skip commit+push, `--no-push` to commit but not push.
|
|
125
125
|
- `init` never commits.
|
|
126
|
+
- Harness-owned paths are **force-added**, so a project that gitignores deployment
|
|
127
|
+
artifacts (an inherited `.agents/` `.opencode/` `AGENTS.md` pattern) still gets the
|
|
128
|
+
harness commit instead of a raw `git add` failure that silently skips it — force-added
|
|
129
|
+
files are called out in the summary.
|
|
130
|
+
- **`[Nothing-to-Update]` confirmation**: if the target is already at this CLI's version and no
|
|
131
|
+
harness-owned file changed, `update` says so explicitly, writes nothing, and makes no commit —
|
|
132
|
+
a no-op is never mistaken for a sync and never churns `.harness.json`.
|
|
133
|
+
- **Downgrade guard**: `update` refuses to run on a target whose deployed harness is NEWER than
|
|
134
|
+
this CLI (e.g. deployed from a local checkout that was never published) and tells you to publish
|
|
135
|
+
that version first.
|
|
136
|
+
- **Published-version check**: if this CLI's version is not the latest `dist-tags.latest` on npm,
|
|
137
|
+
`update` warns you — catches the "bumped the version and updated a project from a local
|
|
138
|
+
checkout, but never published" gap before it silently forks the projects.
|
|
126
139
|
- **Non-destructive**: if a harness-owned file was modified locally since the
|
|
127
140
|
last deploy (tracked by checksum in `.harness.json`), it is backed up to
|
|
128
141
|
`.harness-backup/<timestamp>/` before overwrite.
|
package/bin/agent-harness.js
CHANGED
|
@@ -210,9 +210,12 @@ function isGitRepo(target) {
|
|
|
210
210
|
// project's next session never sees "unexplained" modified harness files. Scoped
|
|
211
211
|
// to this run's changed harness files (never `git add -A`) so unrelated uncommitted
|
|
212
212
|
// work is NEVER swept in. Memory/backup files are excluded (project data / rollback).
|
|
213
|
-
//
|
|
213
|
+
// Harness-owned paths are force-added (`-f`): target projects often gitignore
|
|
214
|
+
// .agents/ .opencode/ AGENTS.md (inherited from the harness's own .gitignore), and
|
|
215
|
+
// a plain `git add` would fail on the first ignored path and commit NOTHING.
|
|
216
|
+
// Returns { committed:boolean, staged:string[], skipped:string[], pushed:boolean|null, forceAdded:string[] }.
|
|
214
217
|
function commitHarnessChanges(target, changedRels, opts) {
|
|
215
|
-
const out = { committed: false, staged: [], skipped: [], pushed: null };
|
|
218
|
+
const out = { committed: false, staged: [], skipped: [], pushed: null, forceAdded: [] };
|
|
216
219
|
if (!opts.commit) return out;
|
|
217
220
|
if (!isGitRepo(target)) {
|
|
218
221
|
out.skipped.push("not a git repo");
|
|
@@ -222,9 +225,13 @@ function commitHarnessChanges(target, changedRels, opts) {
|
|
|
222
225
|
out.skipped.push("no harness files changed");
|
|
223
226
|
return out;
|
|
224
227
|
}
|
|
225
|
-
const
|
|
228
|
+
const ignored = runGit(target, ["check-ignore", ...changedRels]);
|
|
229
|
+
if (ignored.ok && ignored.out) {
|
|
230
|
+
out.forceAdded = ignored.out.split(/\r?\n/).filter(Boolean).map(relKey);
|
|
231
|
+
}
|
|
232
|
+
const addR = runGit(target, ["add", "-f", "--", ...changedRels]);
|
|
226
233
|
if (!addR.ok) {
|
|
227
|
-
out.skipped.push("git add failed: " + addR.err);
|
|
234
|
+
out.skipped.push("git add failed: " + firstGitErrorLine(addR.err));
|
|
228
235
|
return out;
|
|
229
236
|
}
|
|
230
237
|
const diffCached = runGit(target, ["diff", "--cached", "--name-only"]);
|
|
@@ -238,7 +245,7 @@ function commitHarnessChanges(target, changedRels, opts) {
|
|
|
238
245
|
const message = `chore(harness): @dombaras/agent-harness ${prior}${PKG.version}`;
|
|
239
246
|
const commitR = runGit(target, ["commit", "-m", message]);
|
|
240
247
|
if (!commitR.ok) {
|
|
241
|
-
out.skipped.push("commit failed: " + commitR.err);
|
|
248
|
+
out.skipped.push("commit failed: " + firstGitErrorLine(commitR.err));
|
|
242
249
|
return out;
|
|
243
250
|
}
|
|
244
251
|
out.committed = true;
|
|
@@ -247,7 +254,7 @@ function commitHarnessChanges(target, changedRels, opts) {
|
|
|
247
254
|
if (hasRemote) {
|
|
248
255
|
const pushR = runGit(target, ["push", "origin", "HEAD"]);
|
|
249
256
|
out.pushed = pushR.ok;
|
|
250
|
-
if (!pushR.ok) out.skipped.push("push failed: " + pushR.err);
|
|
257
|
+
if (!pushR.ok) out.skipped.push("push failed: " + firstGitErrorLine(pushR.err));
|
|
251
258
|
} else {
|
|
252
259
|
out.skipped.push("no git remote to push");
|
|
253
260
|
}
|
|
@@ -255,6 +262,16 @@ function commitHarnessChanges(target, changedRels, opts) {
|
|
|
255
262
|
return out;
|
|
256
263
|
}
|
|
257
264
|
|
|
265
|
+
/* Pull the first real error line out of git's stderr, dropping the LF/CRLF
|
|
266
|
+
* warnings and `hint:` noise that bury the actual message. */
|
|
267
|
+
function firstGitErrorLine(err) {
|
|
268
|
+
const line = String(err || "")
|
|
269
|
+
.split(/\r?\n/)
|
|
270
|
+
.map((l) => l.trim())
|
|
271
|
+
.find((l) => l && !/^warning:/i.test(l) && !/^hint:/i.test(l));
|
|
272
|
+
return line || "git command failed";
|
|
273
|
+
}
|
|
274
|
+
|
|
258
275
|
// ---------------------------------------------------------------- deploy
|
|
259
276
|
|
|
260
277
|
function backup(target, rel, content) {
|
|
@@ -274,6 +291,17 @@ async function deploy(target, opts) {
|
|
|
274
291
|
}
|
|
275
292
|
const VARIABLES = { PROJECT_NAME: projectName, PROJECT_DOMAIN: projectDomain };
|
|
276
293
|
|
|
294
|
+
// Refuse to downgrade: the target's deployed harness is newer than this CLI.
|
|
295
|
+
// This is how the "I updated from a local checkout that I never published"
|
|
296
|
+
// mistake surfaces instead of silently writing older templates over a newer
|
|
297
|
+
// deployment (and auto-committing that regression).
|
|
298
|
+
if (isUpdate && existing.version && compareVersions(existing.version, PKG.version) > 0) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`"${projectName}" has harness v${existing.version} — NEWER than this CLI (v${PKG.version}). ` +
|
|
301
|
+
`Not downgrading. Publish v${existing.version} (or this CLI) so \`update\` can re-sync from the registry.`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
277
305
|
const previousFiles = existing.files || {};
|
|
278
306
|
const actions = [];
|
|
279
307
|
const manifest = {};
|
|
@@ -349,6 +377,16 @@ async function deploy(target, opts) {
|
|
|
349
377
|
}
|
|
350
378
|
}
|
|
351
379
|
|
|
380
|
+
// Nothing-to-update detection: an update that made no effective change to any
|
|
381
|
+
// harness-owned file and was already running this exact version is a no-op —
|
|
382
|
+
// report it clearly instead of printing a zero-delta "Updated", and leave the
|
|
383
|
+
// target byte-for-byte untouched (no .harness.json timestamp churn, no commit).
|
|
384
|
+
const changedRelsAll = actions
|
|
385
|
+
.filter((a) => ["create", "overwrite", "overwrite (backup)", "merge"].includes(a.kind))
|
|
386
|
+
.map((a) => a.rel);
|
|
387
|
+
const nothingToUpdate =
|
|
388
|
+
!!isUpdate && changedRelsAll.length === 0 && compareVersions(existing.version || "0", PKG.version) === 0;
|
|
389
|
+
|
|
352
390
|
const config = {
|
|
353
391
|
version: PKG.version,
|
|
354
392
|
projectName,
|
|
@@ -357,22 +395,13 @@ async function deploy(target, opts) {
|
|
|
357
395
|
updatedAt: new Date().toISOString(),
|
|
358
396
|
files: manifest,
|
|
359
397
|
};
|
|
360
|
-
if (!dryRun) {
|
|
398
|
+
if (!dryRun && !nothingToUpdate) {
|
|
361
399
|
fs.writeFileSync(path.join(target, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
362
400
|
}
|
|
363
401
|
|
|
364
|
-
// Nothing-to-update detection: an update that made no effective change to any
|
|
365
|
-
// harness-owned file and was already running this exact version is a no-op —
|
|
366
|
-
// still report it clearly instead of printing a zero-delta "Updated".
|
|
367
|
-
const changedRelsAll = actions
|
|
368
|
-
.filter((a) => ["create", "overwrite", "overwrite (backup)", "merge"].includes(a.kind))
|
|
369
|
-
.map((a) => a.rel);
|
|
370
|
-
const nothingToUpdate =
|
|
371
|
-
!!isUpdate && changedRelsAll.length === 0 && compareVersions(existing.version || "0", PKG.version) === 0;
|
|
372
|
-
|
|
373
402
|
// Auto-commit only this run's changed harness-owned files (update only).
|
|
374
403
|
let commitResult = null;
|
|
375
|
-
if (isUpdate && !dryRun) {
|
|
404
|
+
if (isUpdate && !dryRun && !nothingToUpdate) {
|
|
376
405
|
const changedRels = changedRelsAll.filter(
|
|
377
406
|
(r) => !r.startsWith(".agents/memory/") && !r.startsWith(".harness-backup/")
|
|
378
407
|
);
|
|
@@ -404,10 +433,22 @@ function summarize(actions) {
|
|
|
404
433
|
|
|
405
434
|
function printSummary(result, isUpdate) {
|
|
406
435
|
const { projectName, actions, dryRun } = result;
|
|
436
|
+
|
|
437
|
+
if (isUpdate && result.nothingToUpdate) {
|
|
438
|
+
console.log(
|
|
439
|
+
`\n[Nothing-to-Update] agent-harness v${PKG.version}: "${projectName}" is already at ` +
|
|
440
|
+
`v${result.priorVersion || PKG.version} and no harness-owned file changed.`
|
|
441
|
+
);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
407
445
|
const verb = isUpdate
|
|
408
446
|
? (dryRun ? "Would update" : "Updated")
|
|
409
447
|
: (dryRun ? "Would initialize" : "Initialized");
|
|
410
448
|
console.log(`\nagent-harness v${PKG.version} ${verb.toLowerCase()} "${projectName}"` + (dryRun ? " (dry run)" : ""));
|
|
449
|
+
if (isUpdate && result.priorVersion && result.priorVersion !== PKG.version) {
|
|
450
|
+
console.log(` harness: v${result.priorVersion} -> v${PKG.version}`);
|
|
451
|
+
}
|
|
411
452
|
for (const a of actions) console.log(` [${a.kind}] ${a.rel}`);
|
|
412
453
|
const counts = summarize(actions);
|
|
413
454
|
console.log(
|
|
@@ -417,6 +458,22 @@ function printSummary(result, isUpdate) {
|
|
|
417
458
|
);
|
|
418
459
|
}
|
|
419
460
|
|
|
461
|
+
/* Best-effort warning: when this CLI's version is not the latest published on
|
|
462
|
+
* the registry, flag it. This catches the "bumped + ran update from a local
|
|
463
|
+
* checkout, but never published" silent gap. */
|
|
464
|
+
function printUnpublishedWarning() {
|
|
465
|
+
const published = publishedLatestVersion();
|
|
466
|
+
if (published == null || published === "" || published === PKG.version) return;
|
|
467
|
+
const cmp = compareVersions(PKG.version, published);
|
|
468
|
+
if (cmp <= 0) return;
|
|
469
|
+
console.log(
|
|
470
|
+
`\n WARNING: this CLI is v${PKG.version}, but the latest version published on ` +
|
|
471
|
+
`npm is v${published}. If you ran this from a local checkout, the changes were ` +
|
|
472
|
+
`NOT published — Publish first, then \`update\` other projects, or they will ` +
|
|
473
|
+
`branch off an older harness.`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
420
477
|
function printNextSteps() {
|
|
421
478
|
console.log(" Next steps:");
|
|
422
479
|
console.log(" 1. Fill in `.agents/memory/domain-map.md`, `.agents/memory/stack-versions.md`,");
|
|
@@ -487,6 +544,7 @@ async function update(target, flags) {
|
|
|
487
544
|
push: !flags["--no-commit"] && !flags["--no-push"],
|
|
488
545
|
});
|
|
489
546
|
printSummary(result, true);
|
|
547
|
+
printUnpublishedWarning();
|
|
490
548
|
printCommitResult(result);
|
|
491
549
|
}
|
|
492
550
|
|
|
@@ -496,6 +554,12 @@ function printCommitResult(result) {
|
|
|
496
554
|
console.log("");
|
|
497
555
|
if (c.committed) {
|
|
498
556
|
console.log(" committed harness files: " + c.staged.join(", "));
|
|
557
|
+
if (c.forceAdded.length) {
|
|
558
|
+
console.log(
|
|
559
|
+
" force-added (gitignored in this project, but harness-owned): " +
|
|
560
|
+
c.forceAdded.join(", ")
|
|
561
|
+
);
|
|
562
|
+
}
|
|
499
563
|
console.log(
|
|
500
564
|
c.pushed === true
|
|
501
565
|
? " pushed to origin."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dombaras/agent-harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Reusable multi-agent harness for AI-assisted development: personas, skills, operating rules, model routing, and QA gates. Deploy into any project with `npx @dombaras/agent-harness init`.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agent-harness": "bin/agent-harness.js"
|