harness-everything 0.20.4 → 0.20.6

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.4",
13
+ "version": "0.20.6",
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.4",
4
+ "version": "0.20.6",
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.6](https://github.com/dyphn1/Harness-everything/compare/v0.20.5...v0.20.6) (2026-09-22)
6
+
7
+ ### Bug Fixes
8
+
9
+ * **distribution:** match resolve-on-path test extension casing to fixture files ([07f0e5a](https://github.com/dyphn1/Harness-everything/commit/07f0e5a815e779f5596d5c79d448b9ab383ee361))
10
+ * **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)
11
+
12
+ ## [0.20.5](https://github.com/dyphn1/Harness-everything/compare/v0.20.4...v0.20.5) (2026-09-22)
13
+
14
+ ### Bug Fixes
15
+
16
+ * **distribution:** support codex clis without --json or plugin install ([d6a48a6](https://github.com/dyphn1/Harness-everything/commit/d6a48a6763bb564eb7df14334579003823242af7)), closes [#221](https://github.com/dyphn1/Harness-everything/issues/221)
17
+
5
18
  ## [0.20.4](https://github.com/dyphn1/Harness-everything/compare/v0.20.3...v0.20.4) (2026-09-22)
6
19
 
7
20
  ### Bug Fixes
package/README.md CHANGED
@@ -95,7 +95,7 @@ For `--global`, the installer uses each host's supported user-level skill locati
95
95
 
96
96
  The repository also ships a separate local OpenAI/Codex plugin package under `plugins/harness-everything/` with marketplace metadata in `.agents/plugins/marketplace.json`. That package is not the same thing as the `--codex` advisory installer path. The public OpenAI Skills-only upload is narrower again; see [docs/openai-plugin.md](docs/openai-plugin.md).
97
97
 
98
- The native `plugin-sync` command detects the installed host CLIs and applies the state-specific operation: an absent plugin is installed, while an already-installed plugin is explicitly updated (Claude Code) or its configured marketplace is upgraded (Codex). If a host cannot report plugin state, the command fails closed without installing or updating blindly.
98
+ The native `plugin-sync` command detects the installed host CLIs and applies the state-specific operation: an absent plugin is installed, while an already-installed plugin is explicitly updated (Claude Code) or its configured marketplace is upgraded (Codex). Options the host CLI does not implement are dropped and retried, and a host build without plugin install commands is reported as an actionable skip. If a host cannot report plugin state, the command fails closed without installing or updating blindly.
99
99
 
100
100
  The installer records its state directories in `.git/info/exclude` — a local-only git ignore file — so Harness state never lands in a commit and your working tree (including `.gitignore`) is never modified. Everything owned by the general installer is removed with the built-in uninstaller:
101
101
 
@@ -9,12 +9,13 @@ const {
9
9
  findMarketplace,
10
10
  findPlugin,
11
11
  parseJsonOutput,
12
+ resolveOnPath,
12
13
  sync
13
14
  } = require('../scripts/plugin-sync');
14
15
 
15
16
  const ROOT = path.resolve(__dirname, '..');
16
17
 
17
- function fakeHost({ claudeInstalled = false, codexInstalled = false, claudeMarketplace = true, codexMarketplace = true, unknownClaudePluginState = false, unknownClaudeYesFlag = false, unknownClaudeJsonFlag = false } = {}) {
18
+ function fakeHost({ claudeInstalled = false, codexInstalled = false, claudeMarketplace = true, codexMarketplace = true, unknownClaudePluginState = false, unknownClaudeYesFlag = false, unknownClaudeJsonFlag = false, clapCodexJsonFlag = false, missingCodexPluginCommand = false } = {}) {
18
19
  const calls = [];
19
20
  const state = {
20
21
  claudeInstalled,
@@ -59,6 +60,27 @@ function fakeHost({ claudeInstalled = false, codexInstalled = false, claudeMarke
59
60
  }
60
61
 
61
62
  if (command === 'codex') {
63
+ // clap-based CLIs reject an option they do not implement as an
64
+ // unexpected argument, not as an unknown option. Read-only listings
65
+ // already have their own plain-text fallback, so this models a build
66
+ // whose mutating commands predate --json.
67
+ if (clapCodexJsonFlag && args.includes('--json') && /^plugin (marketplace (add|upgrade)|add)\b/.test(joined)) {
68
+ return {
69
+ status: 2,
70
+ stdout: '',
71
+ stderr: "error: unexpected argument '--json' found\n\n tip: to pass '--json' as a value, use '-- --json'\n\nUsage: codex plugin marketplace add --ref <REF> <SOURCE>"
72
+ };
73
+ }
74
+ // Codex builds that ship only `codex plugin marketplace` have no plugin
75
+ // install/list surface at all.
76
+ if (missingCodexPluginCommand && /^plugin (list|add)\b/.test(joined)) {
77
+ const subcommand = joined.split(' ')[1];
78
+ return {
79
+ status: 2,
80
+ stdout: '',
81
+ stderr: `error: unrecognized subcommand '${subcommand}'\n\nUsage: codex plugin [OPTIONS] <COMMAND>`
82
+ };
83
+ }
62
84
  if (joined.includes('marketplace list --json')) {
63
85
  return { status: 0, stdout: JSON.stringify({ marketplaces: state.codexMarketplace ? [{ name: 'harness-everything' }] : [] }), stderr: '' };
64
86
  }
@@ -165,6 +187,41 @@ assert.strictEqual(findPlugin({ plugins: [{ id: 'harness-everything@harness-ever
165
187
  assert.strictEqual(callsFor(fake.calls, 'codex', 'plugin add').length, 0, 'installed Codex plugin must not be re-added');
166
188
  }
167
189
 
190
+ {
191
+ const fake = fakeHost({ codexInstalled: false, codexMarketplace: false, clapCodexJsonFlag: true });
192
+ const result = runHost('codex', fake);
193
+ const addCalls = callsFor(fake.calls, 'codex', 'plugin marketplace add');
194
+ const installCalls = callsFor(fake.calls, 'codex', 'plugin add');
195
+ assert.strictEqual(result.failed, 0, 'clap-style --json rejection must fall back to a compatible Codex command');
196
+ assert.strictEqual(result.results[0].action, 'install');
197
+ assert.strictEqual(addCalls.length, 2, 'unsupported Codex --json must cause one marketplace add retry');
198
+ assert.ok(addCalls[0].args.includes('--json'), 'first Codex marketplace add should use the current flags');
199
+ assert.ok(!addCalls[1].args.includes('--json'), 'Codex compatibility retry must omit unsupported --json');
200
+ assert.ok(installCalls.some(call => !call.args.includes('--json')), 'Codex plugin install must also retry without --json');
201
+ }
202
+
203
+ {
204
+ const fake = fakeHost({ codexInstalled: true, clapCodexJsonFlag: true });
205
+ const result = runHost('codex', fake);
206
+ const upgradeCalls = callsFor(fake.calls, 'codex', 'plugin marketplace upgrade');
207
+ assert.strictEqual(result.failed, 0, 'clap-style --json rejection must not fail the Codex upgrade path');
208
+ assert.strictEqual(result.results[0].action, 'update');
209
+ assert.strictEqual(upgradeCalls.length, 2, 'unsupported Codex --json must cause one upgrade retry');
210
+ assert.ok(!upgradeCalls[1].args.includes('--json'), 'Codex upgrade retry must omit unsupported --json');
211
+ }
212
+
213
+ {
214
+ const fake = fakeHost({ codexMarketplace: false, missingCodexPluginCommand: true, clapCodexJsonFlag: true });
215
+ const result = runHost('codex', fake);
216
+ assert.strictEqual(result.failed, 0, 'a Codex build without plugin subcommands is a capability boundary, not a failure');
217
+ assert.strictEqual(result.skipped, 1, 'the boundary must be reported as a distinct skipped outcome');
218
+ assert.strictEqual(result.results[0].reasonCode, 'host-capability-boundary', 'a capability boundary must not be reported as a missing CLI');
219
+ assert.match(result.results[0].reason, /codex plugin list/, 'the skip reason must name the missing host command');
220
+ assert.match(result.results[0].reason, /upgrade/i, 'the skip reason must stay actionable');
221
+ assert.strictEqual(callsFor(fake.calls, 'codex', 'plugin marketplace add').length, 2, 'the supported marketplace registration must still run');
222
+ assert.strictEqual(callsFor(fake.calls, 'codex', 'plugin add').length, 0, 'an unsupported host must not be blindly installed into');
223
+ }
224
+
168
225
  {
169
226
  const fake = fakeHost({ unknownClaudePluginState: true });
170
227
  const result = runHost('claude', fake);
@@ -178,6 +235,38 @@ assert.strictEqual(findPlugin({ plugins: [{ id: 'harness-everything@harness-ever
178
235
  const result = sync({ host: 'all' }, { commandAvailable: () => false, log: () => {}, warn: () => {} });
179
236
  assert.strictEqual(result.skipped, 2);
180
237
  assert.strictEqual(result.failed, 0);
238
+ assert.ok(result.results.every(entry => entry.reasonCode === 'cli-not-found'), 'an absent CLI must stay distinguishable from a capability boundary');
239
+ }
240
+
241
+ // resolveOnPath() is pure filesystem logic (platform choice happens only at
242
+ // the defaultRunner call site), so its directory-order-first contract runs
243
+ // on every CI OS, not only Windows.
244
+ {
245
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-plugin-sync-resolve-'));
246
+ const cmdDir = path.join(base, 'cmd-dir');
247
+ const exeDir = path.join(base, 'exe-dir');
248
+ fs.mkdirSync(cmdDir);
249
+ fs.mkdirSync(exeDir);
250
+ fs.writeFileSync(path.join(cmdDir, 'harness-fixture.cmd'), '');
251
+ fs.writeFileSync(path.join(exeDir, 'harness-fixture.exe'), '');
252
+ try {
253
+ // Real PATHEXT is upper-case, but NTFS matches it case-insensitively
254
+ // against lower-case fixture files; this test also runs on Linux/macOS
255
+ // CI, whose case-sensitive filesystems would not, so the fixture extension
256
+ // casing matches the file names exactly. The point under test is
257
+ // directory order versus extension order, not PATHEXT's own casing.
258
+ const pathExt = '.com;.exe;.bat;.cmd';
259
+ const cmdFirst = resolveOnPath('harness-fixture', { pathValue: [cmdDir, exeDir].join(path.delimiter), pathExt });
260
+ 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');
261
+
262
+ const exeFirst = resolveOnPath('harness-fixture', { pathValue: [exeDir, cmdDir].join(path.delimiter), pathExt });
263
+ assert.strictEqual(String(exeFirst).toLowerCase(), path.join(exeDir, 'harness-fixture.exe').toLowerCase(), 'PATH order, not extension preference, must decide the match');
264
+
265
+ const missing = resolveOnPath('harness-fixture-does-not-exist', { pathValue: [cmdDir, exeDir].join(path.delimiter), pathExt });
266
+ assert.strictEqual(missing, null, 'an unresolved command must return null, not throw');
267
+ } finally {
268
+ fs.rmSync(base, { recursive: true, force: true });
269
+ }
181
270
  }
182
271
 
183
272
  if (process.platform === 'win32') {
@@ -195,6 +284,33 @@ if (process.platform === 'win32') {
195
284
  }
196
285
  }
197
286
 
287
+ // End-to-end: when a .cmd shim sits earlier on PATH than an unrelated real
288
+ // .exe, the runner must actually execute the .cmd (not silently prefer
289
+ // whichever candidate Node's non-shell spawn can launch directly).
290
+ if (process.platform === 'win32') {
291
+ const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-plugin-sync-order-'));
292
+ const shadowDir = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-plugin-sync-shadow-'));
293
+ const originalPath = process.env.PATH;
294
+ try {
295
+ fs.writeFileSync(path.join(fixtureDir, 'harness-order-fixture.cmd'), '@echo off\r\necho current-cmd-shim\r\n');
296
+ // A real, directly-launchable PE binary standing in for an unrelated,
297
+ // stale standalone install later on PATH. cmd.exe itself is a stable,
298
+ // always-present .exe to copy for this purpose.
299
+ fs.copyFileSync(path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'cmd.exe'), path.join(shadowDir, 'harness-order-fixture.exe'));
300
+ process.env.PATH = [fixtureDir, shadowDir, originalPath || ''].join(path.delimiter);
301
+
302
+ const context = createContext();
303
+ const result = context.runner('harness-order-fixture', []);
304
+ assert.strictEqual(result.status, 0, 'the earlier PATH entry must run successfully');
305
+ assert.match(result.stdout, /current-cmd-shim/, 'an earlier .cmd must be executed even though a later, directly-launchable .exe also matches');
306
+ } finally {
307
+ if (originalPath === undefined) delete process.env.PATH;
308
+ else process.env.PATH = originalPath;
309
+ fs.rmSync(fixtureDir, { recursive: true, force: true });
310
+ fs.rmSync(shadowDir, { recursive: true, force: true });
311
+ }
312
+ }
313
+
198
314
  for (const wrapper of ['scripts/plugin-sync.sh', 'scripts/plugin-sync.ps1']) {
199
315
  const text = fs.readFileSync(path.join(ROOT, wrapper), 'utf8');
200
316
  assert.match(text, /plugin-sync\.js/);
@@ -102,13 +102,13 @@ Two kernel mechanisms annotate plans without creating hard workflow enforcement:
102
102
 
103
103
  ### PreToolUse guard trio and denied-mutation probes
104
104
 
105
- On the Claude hook path, three narrowly scoped `PreToolUse` guards sit alongside the workflow/action gates:
105
+ On the Claude hook path, three narrowly scoped `PreToolUse` guards sit alongside the workflow/action gates. **All three are advisory and fail open — none of them can reject a tool call.** The mechanism that still performs real `exit(2)` hard blocking on this hook is `action-gate.js` (see "Claude Code" below), not this trio.
106
106
 
107
107
  - `boundary-guard.js` (Grep/Glob/Read): warns about very large reads and noisy search roots; it fails open.
108
108
  - `depth-guard.js` (Write): reminds when an existing file would be overwritten without prior inspection. The semantic obligation to establish current target state before destructive overwrite is a **MUST**; the hook itself stays fail-open. New files are unaffected.
109
109
  - `context-compact.js`: estimates working-tree context pressure from `git status`/`git diff --numstat` so later stages can compact or halt before lost-in-the-middle degradation.
110
110
 
111
- Shell mutation observation is best-effort evidence for reminders. The old mutation-probe reservation/lock subsystem was retired by #190.
111
+ Shell mutation observation is best-effort evidence for reminders. The old mutation-probe reservation/lock subsystem, and the hard `exit(2)` blocking `boundary-guard.js`/`depth-guard.js` previously performed, were retired by [#190](https://github.com/dyphn1/Harness-everything/issues/190), which reclassified every Harness-owned *cognitive* workflow gate (this trio, plus `stop-gate.js`, `subagent-scope-guard.js`, `atomic-commit-check.js`, `workflow-gate.js`) as a non-blocking reminder. #190 explicitly kept user/host permission and explicit dangerous-action approval boundaries — i.e. `action-gate.js` — out of scope and still enforced.
112
112
 
113
113
  ## Integration Touchpoints
114
114
 
@@ -124,11 +124,11 @@ The installer configures native lifecycle hooks and project skills.
124
124
 
125
125
  - `SessionStart`: bootstrap/restoration.
126
126
  - `UserPromptSubmit`: kernel routing + semantic workflow contract.
127
- - `PreToolUse`: workflow/boundary/depth/scope reminders, plus separate action permission and Rule-of-3 boundaries.
127
+ - `PreToolUse`: workflow/boundary/depth/scope reminders (all advisory, fail-open), plus the separate `action-gate.js` permission boundary and the Rule-of-3 same-signature-failure boundary.
128
128
  - `PostToolUse`: outcomes, state, repeated-failure evidence, stage checks.
129
129
  - `Stop`: action-gate audit plus non-blocking workflow/verification reminders.
130
130
 
131
- These hooks make semantic obligations observable; they do not promote cognitive workflow contracts into hard enforcement.
131
+ Cognitive workflow contracts (boundary/depth/scope/commit/workflow guards) are made observable, not hard-enforced — [#190](https://github.com/dyphn1/Harness-everything/issues/190) downgraded their `exit(2)` blocking to reminders. `action-gate.js` and the Rule-of-3 zoom-out boundary are the two mechanisms that still hard-block on this hook path; #190 explicitly scoped both out of that downgrade as user/host permission and dangerous-action safety boundaries.
132
132
 
133
133
  ### OpenCode — plugin enforcement, scoped live verification
134
134
 
@@ -80,11 +80,14 @@ The command detects Claude Code and Codex independently, ensures the Harness mar
80
80
  - absent plugin → install;
81
81
  - existing Claude Code plugin → `claude plugin update harness-everything@harness-everything`;
82
82
  - existing Codex plugin → `codex plugin marketplace upgrade harness-everything`;
83
+ - host without the plugin subcommands at all (a Codex build shipping only `codex plugin marketplace`) → register the marketplace source, then skip with an actionable capability-boundary reason;
83
84
  - unknown plugin state → fail closed, with no blind install/re-add.
84
85
 
86
+ Mutating commands are issued with the current option set and retried without any option the host rejects, covering both commander wording (`unknown option '--json'`) and clap wording (`unexpected argument '--json' found`), so older Claude and Codex builds stay supported.
87
+
85
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.
86
89
 
87
- 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.
88
91
 
89
92
  ### Explicit Codex user-hook compatibility fallback
90
93
 
@@ -114,7 +114,7 @@
114
114
  "globalScope": {"status": "Mechanism verified", "paths": {"skills": ["~/.agents/skills"]}},
115
115
  "installUninstallSymmetry": {"status": "Mechanism verified", "notes": "General Harness installer ownership is mechanism-tested. The optional codex-hooks compatibility fallback separately has exact-entry update/uninstall ownership tests and preserves unrelated user hooks; live Codex trust/dispatch still requires host evidence."},
116
116
  "assetsReferences": {"status": "Mechanism verified", "notes": "Packaged skill references are checked and remain inside the package."},
117
- "updateSync": {"status": "Mechanism verified", "notes": "scripts/plugin-sync.js detects installed state and uses marketplace upgrade for an installed Codex plugin; no live Codex host refresh is preserved."},
117
+ "updateSync": {"status": "Mechanism verified", "notes": "scripts/plugin-sync.js detects installed state and uses marketplace upgrade for an installed Codex plugin; unsupported CLI options are dropped and retried, and a Codex build without codex plugin list/add is reported as an actionable capability-boundary skip. No live Codex host refresh is preserved."},
118
118
  "liveHostVerification": {"status": "Unknown", "notes": "No preserved Codex plugin/session artifact proves current bundled-hook entry. Native plugin mounting is preferred; the explicit user-hook fallback is mechanism-verified only and does not solve host carriers that bypass hook dispatch."}
119
119
  }
120
120
  },
@@ -197,6 +197,13 @@ npx github:dyphn1/Harness-everything plugin-sync --json
197
197
 
198
198
  The command installs only when the plugin is absent. An installed Claude plugin uses the explicit Claude update command; an installed Codex plugin uses marketplace upgrade. If plugin state cannot be read, synchronization stops without re-adding or overwriting the plugin.
199
199
 
200
+ Host CLIs differ in which options and subcommands they implement, so the synchronizer degrades instead of failing:
201
+
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
+ - 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
+
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
+
200
207
  See [openai-plugin.md](openai-plugin.md) for installation and validation details.
201
208
 
202
209
  ### Public OpenAI Skills-only plugin behaves differently from local Codex plugin
@@ -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.4",
4
+ "version": "0.20.6",
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.4",
3
+ "version": "0.20.6",
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.4",
3
+ "version": "0.20.6",
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.4",
4
+ "version": "0.20.6",
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:478ab7f0-6aaf-55ae-9e1a-9e950ea30f9d",
5
+ "serialNumber": "urn:uuid:dad0ee6f-3060-5d2e-a044-10420f0fa6e3",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-22T02:13:44.851Z",
8
+ "timestamp": "2026-09-22T15:27:33.748Z",
9
9
  "tools": [
10
10
  {
11
11
  "vendor": "Harness Everything",
12
12
  "name": "generate-sbom.js",
13
- "version": "0.20.4"
13
+ "version": "0.20.6"
14
14
  }
15
15
  ],
16
16
  "component": {
17
17
  "type": "application",
18
- "bom-ref": "pkg:npm/harness-everything@0.20.4",
18
+ "bom-ref": "pkg:npm/harness-everything@0.20.6",
19
19
  "name": "harness-everything",
20
- "version": "0.20.4",
21
- "purl": "pkg:npm/harness-everything@0.20.4"
20
+ "version": "0.20.6",
21
+ "purl": "pkg:npm/harness-everything@0.20.6"
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.4",
2002
+ "ref": "pkg:npm/harness-everything@0.20.6",
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);
@@ -189,41 +257,73 @@ function query(context, command, args) {
189
257
  return context.runner(command, args, { query: true }) || { status: 1, stdout: '', stderr: 'runner returned no result' };
190
258
  }
191
259
 
192
- function hasUnknownOption(result, option) {
193
- const output = [result && result.stderr, result && result.stdout, result && result.error && result.error.message]
260
+ function resultText(result) {
261
+ return [result && result.stderr, result && result.stdout, result && result.error && result.error.message]
194
262
  .map(value => String(value || ''))
195
263
  .join('\n');
196
- return new RegExp(`unknown option\\s+['"]?${escapeRegExp(option)}['"]?`, 'i').test(output);
197
264
  }
198
265
 
199
- function runClaudePluginMutation(context, action, pluginId) {
200
- const baseArgs = [
201
- 'plugin', action, pluginId,
202
- '--scope', 'user'
203
- ];
204
- let args = [...baseArgs, '--yes', '--json'];
266
+ // Host CLIs disagree on how they reject an option they never implemented:
267
+ // commander-based CLIs (Claude) say "unknown option '--json'", while
268
+ // clap-based CLIs (Codex) say "unexpected argument '--json' found". Both mean
269
+ // the same thing, so compatibility retries must recognize either wording.
270
+ function hasUnsupportedOption(result, option) {
271
+ const output = resultText(result);
272
+ const name = escapeRegExp(option);
273
+ return [
274
+ `unknown option\\s+['"]?${name}['"]?`,
275
+ `unrecognized option\\s+['"]?${name}['"]?`,
276
+ `unexpected argument\\s+['"]?${name}['"]?`,
277
+ `found argument\\s+['"]?${name}['"]?`
278
+ ].some(pattern => new RegExp(pattern, 'i').test(output));
279
+ }
280
+
281
+ // A host build that never shipped the subcommand is a capability boundary, not
282
+ // a transient failure, so it must be reported differently from a broken call.
283
+ function hasUnsupportedSubcommand(result, subcommand) {
284
+ const expression = new RegExp(
285
+ `(unrecognized|unknown|invalid)\\s+(sub)?command\\s+['"]?${escapeRegExp(subcommand)}['"]?`,
286
+ 'i'
287
+ );
288
+ return expression.test(resultText(result));
289
+ }
290
+
291
+ // Optional arguments are dropped one at a time so a host that implements only
292
+ // some of them still reaches a command it accepts.
293
+ function runHostMutation(context, command, baseArgs, optionalArgs, label) {
294
+ let args = [...baseArgs, ...optionalArgs];
205
295
  let attempt = 0;
206
296
 
207
297
  while (true) {
208
- const result = invoke(context, 'claude', args, {
298
+ const result = invoke(context, command, args, {
209
299
  mutate: true,
210
- label: attempt === 0 ? `claude plugin ${action}` : `claude plugin ${action} compatibility retry`,
300
+ label: attempt === 0 ? label : `${label} compatibility retry`,
211
301
  warnOnFailure: false
212
302
  });
213
303
  if (result.status === 0) return result;
214
304
 
215
- const unsupported = ['--yes', '--json'].find(option => args.includes(option) && hasUnknownOption(result, option));
305
+ const unsupported = optionalArgs.find(option => args.includes(option) && hasUnsupportedOption(result, option));
216
306
  if (!unsupported) {
217
- context.warn(`[failed] ${commandLine('claude', args)}\n ${resultError(result)}`);
307
+ context.warn(`[failed] ${commandLine(command, args)}\n ${resultError(result)}`);
218
308
  return result;
219
309
  }
220
310
 
221
311
  args = args.filter(arg => arg !== unsupported);
222
312
  attempt += 1;
223
- context.warn(`[notice] Claude CLI does not support ${unsupported}; retrying ${action} without that option.`);
313
+ context.warn(`[notice] ${command} CLI does not support ${unsupported}; retrying ${label} without that option.`);
224
314
  }
225
315
  }
226
316
 
317
+ function runClaudePluginMutation(context, action, pluginId) {
318
+ return runHostMutation(
319
+ context,
320
+ 'claude',
321
+ ['plugin', action, pluginId, '--scope', 'user'],
322
+ ['--yes', '--json'],
323
+ `claude plugin ${action}`
324
+ );
325
+ }
326
+
227
327
  function listState(context, { command, jsonArgs, plainArgs, detectJson, detectText }) {
228
328
  const jsonResult = query(context, command, jsonArgs);
229
329
  if (jsonResult.status === 0) {
@@ -239,6 +339,7 @@ function listState(context, { command, jsonArgs, plainArgs, detectJson, detectTe
239
339
  return {
240
340
  known: false,
241
341
  value: false,
342
+ attempts: [jsonResult, plainResult],
242
343
  error: `${resultError(jsonResult)}; fallback: ${resultError(plainResult)}`
243
344
  };
244
345
  }
@@ -312,11 +413,13 @@ function ensureCodexMarketplace(context) {
312
413
  const state = queryCodexMarketplace(context);
313
414
  if (!state.known) context.warn(`[notice] Codex marketplace state is unavailable; ensuring ${context.marketplace} is configured.`);
314
415
  if (!state.known || !state.value) {
315
- const added = invoke(context, 'codex', [
316
- 'plugin', 'marketplace', 'add', context.repositorySource,
317
- '--ref', context.ref,
318
- '--json'
319
- ], { mutate: true, label: 'codex marketplace add' });
416
+ const added = runHostMutation(
417
+ context,
418
+ 'codex',
419
+ ['plugin', 'marketplace', 'add', context.repositorySource, '--ref', context.ref],
420
+ ['--json'],
421
+ 'codex marketplace add'
422
+ );
320
423
  if (added.status !== 0) return { ok: false, error: resultError(added) };
321
424
  }
322
425
  return { ok: true };
@@ -332,10 +435,13 @@ function refreshClaudeMarketplace(context) {
332
435
  }
333
436
 
334
437
  function refreshCodexMarketplace(context) {
335
- const upgraded = invoke(context, 'codex', [
336
- 'plugin', 'marketplace', 'upgrade', context.marketplace,
337
- '--json'
338
- ], { mutate: true, label: 'codex marketplace upgrade' });
438
+ const upgraded = runHostMutation(
439
+ context,
440
+ 'codex',
441
+ ['plugin', 'marketplace', 'upgrade', context.marketplace],
442
+ ['--json'],
443
+ 'codex marketplace upgrade'
444
+ );
339
445
  return upgraded.status === 0
340
446
  ? { ok: true }
341
447
  : { ok: false, error: resultError(upgraded) };
@@ -350,7 +456,7 @@ function verifyPlugin(context, host, queryPlugin) {
350
456
  }
351
457
 
352
458
  function syncClaude(context) {
353
- if (!context.commandAvailable('claude')) return { host: 'claude', status: 'skipped', reason: 'claude CLI not found' };
459
+ if (!context.commandAvailable('claude')) return { host: 'claude', status: 'skipped', reasonCode: 'cli-not-found', reason: 'claude CLI not found' };
354
460
  const marketplace = ensureClaudeMarketplace(context);
355
461
  if (!marketplace.ok) return { host: 'claude', status: 'failed', error: marketplace.error };
356
462
  const installed = queryClaudePlugin(context);
@@ -372,11 +478,25 @@ function syncClaude(context) {
372
478
  }
373
479
 
374
480
  function syncCodex(context) {
375
- if (!context.commandAvailable('codex')) return { host: 'codex', status: 'skipped', reason: 'codex CLI not found' };
481
+ if (!context.commandAvailable('codex')) return { host: 'codex', status: 'skipped', reasonCode: 'cli-not-found', reason: 'codex CLI not found' };
376
482
  const marketplace = ensureCodexMarketplace(context);
377
483
  if (!marketplace.ok) return { host: 'codex', status: 'failed', error: marketplace.error };
378
484
  const installed = queryCodexPlugin(context);
379
- if (!installed.known) return { host: 'codex', status: 'failed', error: `Codex plugin state could not be determined: ${installed.error}` };
485
+ if (!installed.known) {
486
+ // A Codex build that ships only `codex plugin marketplace` cannot report
487
+ // or change plugin state at all. The marketplace source is now registered,
488
+ // which is everything this build supports, so report the host boundary
489
+ // instead of a generic failure.
490
+ if ((installed.attempts || []).some(attempt => hasUnsupportedSubcommand(attempt, 'list'))) {
491
+ return {
492
+ host: 'codex',
493
+ status: 'skipped',
494
+ reasonCode: 'host-capability-boundary',
495
+ reason: `this Codex CLI has no \`codex plugin list\` command, so plugin state cannot be read; ${context.marketplace} is registered as a marketplace source — enable the plugin from Codex, or upgrade the Codex CLI to a build that ships \`codex plugin list\`/\`codex plugin add\``
496
+ };
497
+ }
498
+ return { host: 'codex', status: 'failed', error: `Codex plugin state could not be determined: ${installed.error}` };
499
+ }
380
500
 
381
501
  let action;
382
502
  let result;
@@ -392,9 +512,13 @@ function syncCodex(context) {
392
512
  action = 'install';
393
513
  result = refreshCodexMarketplace(context);
394
514
  if (result.ok) {
395
- result = invoke(context, 'codex', [
396
- 'plugin', 'add', `${context.plugin}@${context.marketplace}`, '--json'
397
- ], { mutate: true, label: 'codex plugin install' });
515
+ result = runHostMutation(
516
+ context,
517
+ 'codex',
518
+ ['plugin', 'add', `${context.plugin}@${context.marketplace}`],
519
+ ['--json'],
520
+ 'codex plugin install'
521
+ );
398
522
  } else {
399
523
  result = { status: 1, stdout: '', stderr: result.error };
400
524
  }
@@ -488,7 +612,9 @@ function main(argv = process.argv.slice(2)) {
488
612
  else console.error(`[fail] ${result.host}: ${result.error}`);
489
613
  }
490
614
  if (summary.synchronized === 0 && summary.failed === 0) {
491
- console.error('No supported host CLI was found. Install Claude Code and/or Codex, then retry.');
615
+ console.error(summary.results.every(result => result.reasonCode === 'cli-not-found')
616
+ ? 'No supported host CLI was found. Install Claude Code and/or Codex, then retry.'
617
+ : 'No host plugin was synchronized. Review the skip reasons above.');
492
618
  }
493
619
  }
494
620
  return summary.failed > 0 || summary.synchronized === 0 ? 1 : 0;
@@ -505,6 +631,7 @@ module.exports = {
505
631
  main,
506
632
  parseArgs,
507
633
  parseJsonOutput,
634
+ resolveOnPath,
508
635
  sync,
509
636
  textHasMarketplace,
510
637
  textHasPlugin