harness-everything 0.20.5 → 0.20.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.
@@ -10,7 +10,7 @@
10
10
  "name": "harness-everything",
11
11
  "source": "./",
12
12
  "description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
13
- "version": "0.20.5",
13
+ "version": "0.20.7",
14
14
  "author": {
15
15
  "name": "Miya Daniel"
16
16
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harness-everything",
3
3
  "description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
4
- "version": "0.20.5",
4
+ "version": "0.20.7",
5
5
  "author": {
6
6
  "name": "Miya Daniel"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.20.7](https://github.com/dyphn1/Harness-everything/compare/v0.20.6...v0.20.7) (2026-09-22)
6
+
7
+ ### Bug Fixes
8
+
9
+ * **distribution:** emit per-host progress so plugin-sync does not look hung ([d746871](https://github.com/dyphn1/Harness-everything/commit/d746871b7a39c10decc841e9982b27982271df1e)), closes [#238](https://github.com/dyphn1/Harness-everything/issues/238)
10
+
11
+ ## [0.20.6](https://github.com/dyphn1/Harness-everything/compare/v0.20.5...v0.20.6) (2026-09-22)
12
+
13
+ ### Bug Fixes
14
+
15
+ * **distribution:** match resolve-on-path test extension casing to fixture files ([07f0e5a](https://github.com/dyphn1/Harness-everything/commit/07f0e5a815e779f5596d5c79d448b9ab383ee361))
16
+ * **distribution:** resolve codex/claude on windows in path order, not exe preference ([048c5e4](https://github.com/dyphn1/Harness-everything/commit/048c5e40e0d4727700234c49e509fc6edfac4f01)), closes [#236](https://github.com/dyphn1/Harness-everything/issues/236)
17
+
5
18
  ## [0.20.5](https://github.com/dyphn1/Harness-everything/compare/v0.20.4...v0.20.5) (2026-09-22)
6
19
 
7
20
  ### Bug Fixes
@@ -9,6 +9,7 @@ const {
9
9
  findMarketplace,
10
10
  findPlugin,
11
11
  parseJsonOutput,
12
+ resolveOnPath,
12
13
  sync
13
14
  } = require('../scripts/plugin-sync');
14
15
 
@@ -237,6 +238,60 @@ assert.strictEqual(findPlugin({ plugins: [{ id: 'harness-everything@harness-ever
237
238
  assert.ok(result.results.every(entry => entry.reasonCode === 'cli-not-found'), 'an absent CLI must stay distinguishable from a capability boundary');
238
239
  }
239
240
 
241
+ {
242
+ // Each host sync is fully synchronous and does real network I/O; without a
243
+ // progress notice emitted before that work starts, the terminal stays
244
+ // silent for the whole run, which is indistinguishable from a hang.
245
+ const events = [];
246
+ const fake = fakeHost({ claudeInstalled: true, codexInstalled: true });
247
+ const runner = (command, args, opts) => {
248
+ events.push(`call:${command}`);
249
+ return fake.runner(command, args, opts);
250
+ };
251
+ const result = sync({ host: 'all', repository: 'dyphn1/Harness-everything', ref: 'main' }, {
252
+ runner,
253
+ commandAvailable: () => true,
254
+ log: () => {},
255
+ warn: message => events.push(`warn:${message}`)
256
+ });
257
+ assert.strictEqual(result.failed, 0);
258
+ assert.ok(events.includes('warn:[...] claude: syncing'), 'a claude progress notice must be emitted');
259
+ assert.ok(events.includes('warn:[...] codex: syncing'), 'a codex progress notice must be emitted');
260
+ assert.ok(events.indexOf('warn:[...] claude: syncing') < events.indexOf('call:claude'), 'the claude notice must be emitted before its first host command, not buffered until the run finishes');
261
+ assert.ok(events.indexOf('warn:[...] codex: syncing') < events.indexOf('call:codex'), 'the codex notice must be emitted before its first host command');
262
+ }
263
+
264
+ // resolveOnPath() is pure filesystem logic (platform choice happens only at
265
+ // the defaultRunner call site), so its directory-order-first contract runs
266
+ // on every CI OS, not only Windows.
267
+ {
268
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-plugin-sync-resolve-'));
269
+ const cmdDir = path.join(base, 'cmd-dir');
270
+ const exeDir = path.join(base, 'exe-dir');
271
+ fs.mkdirSync(cmdDir);
272
+ fs.mkdirSync(exeDir);
273
+ fs.writeFileSync(path.join(cmdDir, 'harness-fixture.cmd'), '');
274
+ fs.writeFileSync(path.join(exeDir, 'harness-fixture.exe'), '');
275
+ try {
276
+ // Real PATHEXT is upper-case, but NTFS matches it case-insensitively
277
+ // against lower-case fixture files; this test also runs on Linux/macOS
278
+ // CI, whose case-sensitive filesystems would not, so the fixture extension
279
+ // casing matches the file names exactly. The point under test is
280
+ // directory order versus extension order, not PATHEXT's own casing.
281
+ const pathExt = '.com;.exe;.bat;.cmd';
282
+ const cmdFirst = resolveOnPath('harness-fixture', { pathValue: [cmdDir, exeDir].join(path.delimiter), pathExt });
283
+ assert.strictEqual(String(cmdFirst).toLowerCase(), path.join(cmdDir, 'harness-fixture.cmd').toLowerCase(), 'an earlier PATH directory must win even when a later one has the extension PATHEXT prefers');
284
+
285
+ const exeFirst = resolveOnPath('harness-fixture', { pathValue: [exeDir, cmdDir].join(path.delimiter), pathExt });
286
+ assert.strictEqual(String(exeFirst).toLowerCase(), path.join(exeDir, 'harness-fixture.exe').toLowerCase(), 'PATH order, not extension preference, must decide the match');
287
+
288
+ const missing = resolveOnPath('harness-fixture-does-not-exist', { pathValue: [cmdDir, exeDir].join(path.delimiter), pathExt });
289
+ assert.strictEqual(missing, null, 'an unresolved command must return null, not throw');
290
+ } finally {
291
+ fs.rmSync(base, { recursive: true, force: true });
292
+ }
293
+ }
294
+
240
295
  if (process.platform === 'win32') {
241
296
  const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-plugin-sync-command-'));
242
297
  const originalPath = process.env.PATH;
@@ -252,6 +307,33 @@ if (process.platform === 'win32') {
252
307
  }
253
308
  }
254
309
 
310
+ // End-to-end: when a .cmd shim sits earlier on PATH than an unrelated real
311
+ // .exe, the runner must actually execute the .cmd (not silently prefer
312
+ // whichever candidate Node's non-shell spawn can launch directly).
313
+ if (process.platform === 'win32') {
314
+ const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-plugin-sync-order-'));
315
+ const shadowDir = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-plugin-sync-shadow-'));
316
+ const originalPath = process.env.PATH;
317
+ try {
318
+ fs.writeFileSync(path.join(fixtureDir, 'harness-order-fixture.cmd'), '@echo off\r\necho current-cmd-shim\r\n');
319
+ // A real, directly-launchable PE binary standing in for an unrelated,
320
+ // stale standalone install later on PATH. cmd.exe itself is a stable,
321
+ // always-present .exe to copy for this purpose.
322
+ fs.copyFileSync(path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'cmd.exe'), path.join(shadowDir, 'harness-order-fixture.exe'));
323
+ process.env.PATH = [fixtureDir, shadowDir, originalPath || ''].join(path.delimiter);
324
+
325
+ const context = createContext();
326
+ const result = context.runner('harness-order-fixture', []);
327
+ assert.strictEqual(result.status, 0, 'the earlier PATH entry must run successfully');
328
+ assert.match(result.stdout, /current-cmd-shim/, 'an earlier .cmd must be executed even though a later, directly-launchable .exe also matches');
329
+ } finally {
330
+ if (originalPath === undefined) delete process.env.PATH;
331
+ else process.env.PATH = originalPath;
332
+ fs.rmSync(fixtureDir, { recursive: true, force: true });
333
+ fs.rmSync(shadowDir, { recursive: true, force: true });
334
+ }
335
+ }
336
+
255
337
  for (const wrapper of ['scripts/plugin-sync.sh', 'scripts/plugin-sync.ps1']) {
256
338
  const text = fs.readFileSync(path.join(ROOT, wrapper), 'utf8');
257
339
  assert.match(text, /plugin-sync\.js/);
@@ -87,7 +87,7 @@ Mutating commands are issued with the current option set and retried without any
87
87
 
88
88
  The installed branch is intentionally an update branch even when the current release is already the newest version; the host command may report “already latest.” Codex marketplace upgrade is the current native refresh operation; the command does not edit private cache paths or `config.toml`. These are deterministic command/state tests, not live-host evidence, so `liveHostVerification` remains `Unknown` in the matrix.
89
89
 
90
- On Windows, the synchronizer retries through `ComSpec` when Node cannot directly start a CLI shim, so npm-installed `codex.cmd` and `claude.cmd` commands remain discoverable when the wrapper is launched from Git Bash.
90
+ On Windows, the synchronizer resolves `codex`/`claude` the same way an interactive shell does: it walks `PATH` directories in order and tries `PATHEXT` extensions within each directory before moving on, so directory order — not which candidate happens to be a directly launchable `.exe` — decides the match. This matters because Node's non-shell `spawnSync` cannot execute a `.cmd`/`.bat` shim directly and, left to its own resolution, can silently prefer an unrelated, older standalone `.exe` elsewhere on `PATH` over a newer npm-installed shim with no error to signal it. Only the one case Windows genuinely requires an interpreter for — a resolved `.cmd`/`.bat` target — is run through `ComSpec`, using the already-resolved absolute path rather than re-resolving the bare command name.
91
91
 
92
92
  ### Explicit Codex user-hook compatibility fallback
93
93
 
@@ -202,7 +202,7 @@ Host CLIs differ in which options and subcommands they implement, so the synchro
202
202
  - An option the host rejects — `unknown option '--json'` (commander) or `unexpected argument '--json' found` (clap/Codex) — is dropped and the command is retried without it.
203
203
  - A Codex build that ships only `codex plugin marketplace` has no `codex plugin list`/`codex plugin add`. The marketplace source is still registered and the host is reported as `[skip] codex: …` with an actionable reason, not as a failure. Upgrade the Codex CLI, or enable the plugin from Codex, to complete installation.
204
204
 
205
- If several `codex` binaries are on `PATH` (for example an npm shim plus an installer-managed build), the synchronizer runs whichever one Node resolves first, which may be older than the one your interactive shell picks up. Compare `codex --version` with `node -e "console.log(require('child_process').spawnSync('codex',['--version'],{encoding:'utf8'}).stdout)"` when the reported capabilities look wrong.
205
+ If several `codex`/`claude` binaries are on `PATH` (for example an npm shim plus a separately installed build), the synchronizer resolves the same one your interactive shell would: it walks `PATH` directories in order and, on Windows, tries `PATHEXT` extensions within each directory before moving to the next — matching `cmd.exe`/PowerShell/`which` resolution, so directory order decides the match, not which candidate happens to be a directly launchable `.exe`. If the reported capabilities still look stale, an orphaned install is genuinely earlier on `PATH` than the one you expect; check `where codex` (Windows) or `which -a codex` (POSIX) to see the full resolution order, and remove or reorder the stale entry.
206
206
 
207
207
  See [openai-plugin.md](openai-plugin.md) for installation and validation details.
208
208
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harness-enforcement",
3
3
  "description": "Repository inventory for the opencode enforcement plugin and its standalone helper",
4
- "version": "0.20.5",
4
+ "version": "0.20.7",
5
5
  "runtime": "index.mjs",
6
6
  "hooks": {
7
7
  "verify": "hooks/verify.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.20.5",
3
+ "version": "0.20.7",
4
4
  "description": "Harness OS: An Orchestrated Agent Operating System for AI-driven Development",
5
5
  "main": "harness-everything/scripts/bootstrap.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.20.5",
3
+ "version": "0.20.7",
4
4
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
5
5
  "author": {
6
6
  "name": "Miya Daniel",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "harness-everything",
4
- "version": "0.20.5",
4
+ "version": "0.20.7",
5
5
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
6
6
  "author": {
7
7
  "name": "Miya Daniel",
package/sbom.cdx.json CHANGED
@@ -2,23 +2,23 @@
2
2
  "$schema": "https://cyclonedx.org/schema/bom-1.7.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.7",
5
- "serialNumber": "urn:uuid:2fe1f791-72b1-528d-91ab-182e028b8f11",
5
+ "serialNumber": "urn:uuid:12ceeeac-7f99-5fb2-82a3-768d5d708e00",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-22T14:48:08.473Z",
8
+ "timestamp": "2026-09-22T16:17:00.105Z",
9
9
  "tools": [
10
10
  {
11
11
  "vendor": "Harness Everything",
12
12
  "name": "generate-sbom.js",
13
- "version": "0.20.5"
13
+ "version": "0.20.7"
14
14
  }
15
15
  ],
16
16
  "component": {
17
17
  "type": "application",
18
- "bom-ref": "pkg:npm/harness-everything@0.20.5",
18
+ "bom-ref": "pkg:npm/harness-everything@0.20.7",
19
19
  "name": "harness-everything",
20
- "version": "0.20.5",
21
- "purl": "pkg:npm/harness-everything@0.20.5"
20
+ "version": "0.20.7",
21
+ "purl": "pkg:npm/harness-everything@0.20.7"
22
22
  }
23
23
  },
24
24
  "components": [
@@ -1999,7 +1999,7 @@
1999
1999
  ]
2000
2000
  },
2001
2001
  {
2002
- "ref": "pkg:npm/harness-everything@0.20.5",
2002
+ "ref": "pkg:npm/harness-everything@0.20.7",
2003
2003
  "dependsOn": [
2004
2004
  "pkg:npm/%40commitlint/cli@19.8.1",
2005
2005
  "pkg:npm/%40commitlint/config-conventional@19.8.1",
@@ -11,6 +11,8 @@
11
11
  */
12
12
 
13
13
  const { spawnSync } = require('child_process');
14
+ const fs = require('fs');
15
+ const path = require('path');
14
16
 
15
17
  const DEFAULTS = Object.freeze({
16
18
  marketplace: 'harness-everything',
@@ -19,17 +21,83 @@ const DEFAULTS = Object.freeze({
19
21
  ref: process.env.HARNESS_PLUGIN_REF || 'main'
20
22
  });
21
23
 
24
+ // Windows CLIs that ship as a .cmd/.bat shim are not executable via the same
25
+ // direct CreateProcess call Node uses for a .exe, and — unlike a real shell —
26
+ // Node's non-shell spawnSync does not search PATHEXT-suffixed candidates for
27
+ // a bare command name at all. When more than one `codex`/`claude` is on
28
+ // PATH (an npm shim plus a separately installed .exe is a common pairing),
29
+ // the non-shell spawn silently resolves whichever entry happens to be a real
30
+ // .exe, regardless of PATH order — so an unrelated, older standalone install
31
+ // can shadow a newer npm-updated shim with no error to signal it.
32
+ //
33
+ // resolveOnPath() replicates what a real shell / `where.exe` does instead:
34
+ // walk PATH directories in order, and within each directory try PATHEXT
35
+ // extensions in order, returning the first match. Directory order wins over
36
+ // extension preference, exactly like cmd.exe and PowerShell's own lookup, so
37
+ // the result matches what typing the command in a terminal would run.
38
+ function resolveOnPath(command, { pathValue = process.env.PATH || process.env.Path || '', pathExt = process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD' } = {}) {
39
+ if (path.isAbsolute(command)) return fs.existsSync(command) ? command : null;
40
+
41
+ const dirs = pathValue.split(path.delimiter).filter(Boolean);
42
+ const extensions = pathExt.split(';').map(ext => ext.trim()).filter(Boolean);
43
+ const hasKnownExtension = extensions.some(ext => command.toLowerCase().endsWith(ext.toLowerCase()));
44
+
45
+ for (const dir of dirs) {
46
+ if (hasKnownExtension) {
47
+ const candidate = path.join(dir, command);
48
+ if (fs.existsSync(candidate)) return candidate;
49
+ continue;
50
+ }
51
+ for (const ext of extensions) {
52
+ const candidate = path.join(dir, command + ext);
53
+ if (fs.existsSync(candidate)) return candidate;
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+
59
+ // A .bat/.cmd target is not a Win32 executable image; Windows requires the
60
+ // cmd.exe interpreter to run one at all, the same way a shebang-less script
61
+ // needs an explicit interpreter on POSIX. This is the one case a shell is
62
+ // unavoidable, and only that resolved file is passed to it — never the bare
63
+ // command name or user-controlled arguments re-interpreted as a command line.
64
+ const SHELL_REQUIRED_EXTENSIONS = new Set(['.bat', '.cmd']);
65
+
66
+ function runResolved(resolved, args, options) {
67
+ const ext = path.extname(resolved).toLowerCase();
68
+ if (SHELL_REQUIRED_EXTENSIONS.has(ext)) {
69
+ const comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe';
70
+ return spawnSync(comspec, ['/d', '/c', resolved, ...args], options);
71
+ }
72
+ return spawnSync(resolved, args, options);
73
+ }
74
+
22
75
  function defaultRunner(command, args) {
23
76
  const options = {
24
77
  encoding: 'utf8',
25
78
  stdio: ['ignore', 'pipe', 'pipe'],
26
79
  windowsHide: true
27
80
  };
81
+
82
+ if (process.platform === 'win32') {
83
+ const resolved = resolveOnPath(command);
84
+ if (resolved) {
85
+ const result = runResolved(resolved, args, options);
86
+ return {
87
+ status: result.status === null ? 1 : result.status,
88
+ stdout: result.stdout || '',
89
+ stderr: result.stderr || '',
90
+ error: result.error || null
91
+ };
92
+ }
93
+ }
94
+
28
95
  let result = spawnSync(command, args, options);
29
96
 
30
- // npm-installed Windows CLIs are often .cmd shims. Node can report those
31
- // shims as an executable-resolution error when this script is launched from
32
- // Git Bash, even though the same command is available to the shell.
97
+ // Resolution above only checks the filesystem; if PATH itself could not be
98
+ // read, or the command is reachable through a mechanism resolveOnPath does
99
+ // not model (e.g. an App Execution Alias), fall back to letting the shell
100
+ // do its own resolution rather than failing outright.
33
101
  if (process.platform === 'win32' && result.error && ['EACCES', 'EINVAL', 'ENOENT'].includes(result.error.code)) {
34
102
  const comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe';
35
103
  result = spawnSync(comspec, ['/d', '/c', command, ...args], options);
@@ -507,7 +575,15 @@ Options:
507
575
  function sync(options, dependencies = {}) {
508
576
  const context = createContext({ ...options, ...dependencies });
509
577
  const hosts = context.host === 'all' ? ['claude', 'codex'] : [context.host];
510
- const results = hosts.map(host => host === 'claude' ? syncClaude(context) : syncCodex(context));
578
+ // Each host sync is fully synchronous and the marketplace update/refresh
579
+ // step does real network I/O (observed ~10s+ for Claude alone), so without
580
+ // this the terminal stays completely silent until every host finishes —
581
+ // indistinguishable from a hang. This goes to stderr (context.warn), not
582
+ // stdout, so `--json` consumers' stdout stays pure JSON.
583
+ const results = hosts.map(host => {
584
+ context.warn(`[...] ${host}: syncing`);
585
+ return host === 'claude' ? syncClaude(context) : syncCodex(context);
586
+ });
511
587
  const summary = {
512
588
  repository: context.repository,
513
589
  marketplace: context.marketplace,
@@ -563,6 +639,7 @@ module.exports = {
563
639
  main,
564
640
  parseArgs,
565
641
  parseJsonOutput,
642
+ resolveOnPath,
566
643
  sync,
567
644
  textHasMarketplace,
568
645
  textHasPlugin