harness-everything 0.20.3 → 0.20.5

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.3",
13
+ "version": "0.20.5",
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.3",
4
+ "version": "0.20.5",
5
5
  "author": {
6
6
  "name": "Miya Daniel"
7
7
  },
@@ -68,3 +68,11 @@ jobs:
68
68
  NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
69
69
  NPM_CONFIG_PROVENANCE: "true"
70
70
  run: npx --no-install semantic-release
71
+
72
+ - name: Upload release SBOM artifact
73
+ if: ${{ hashFiles('sbom.cdx.json') != '' }}
74
+ uses: actions/upload-artifact@v4
75
+ with:
76
+ name: release-sbom
77
+ path: sbom.cdx.json
78
+ if-no-files-found: error
package/.releaserc.json CHANGED
@@ -103,6 +103,12 @@
103
103
  [
104
104
  "@semantic-release/github",
105
105
  {
106
+ "assets": [
107
+ {
108
+ "path": "sbom.cdx.json",
109
+ "label": "CycloneDX SBOM"
110
+ }
111
+ ],
106
112
  "successComment": false,
107
113
  "failTitle": false,
108
114
  "labels": false,
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.20.5](https://github.com/dyphn1/Harness-everything/compare/v0.20.4...v0.20.5) (2026-09-22)
6
+
7
+ ### Bug Fixes
8
+
9
+ * **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)
10
+
11
+ ## [0.20.4](https://github.com/dyphn1/Harness-everything/compare/v0.20.3...v0.20.4) (2026-09-22)
12
+
13
+ ### Bug Fixes
14
+
15
+ * **release:** publish sbom release artifacts ([83eeb96](https://github.com/dyphn1/Harness-everything/commit/83eeb96c1746ccbffcdcba478ba6b54c522887a0)), closes [#222](https://github.com/dyphn1/Harness-everything/issues/222)
16
+
5
17
  ## [0.20.3](https://github.com/dyphn1/Harness-everything/compare/v0.20.2...v0.20.3) (2026-09-22)
6
18
 
7
19
  ### 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
 
package/RELEASING.md CHANGED
@@ -40,9 +40,9 @@ A push to `main` runs the full reusable CI gate and then semantic-release. A man
40
40
  6. Run `scripts/sync-release-version.js` to synchronize package, Claude, OpenAI/Codex, OpenCode, lockfile, and changed canonical skill version fields.
41
41
  7. Regenerate and verify plugin packages, then generate and validate the release-only `sbom.cdx.json` CycloneDX 1.7 file.
42
42
  8. Update `CHANGELOG.md` and create `chore(release): X.Y.Z [skip ci]`.
43
- 9. Create tag `vX.Y.Z`, publish npm with provenance (including the generated SBOM), and create the GitHub Release.
43
+ 9. Create tag `vX.Y.Z`, publish npm with provenance (including the generated SBOM), create the GitHub Release with the SBOM attached, and retain the same file as an Actions artifact.
44
44
 
45
- The SBOM is generated from the release `package.json`/`package-lock.json` as CycloneDX 1.7, the current 1.x format, to satisfy consumers and regulatory baselines requiring CycloneDX 1.6 or later. It is intentionally not committed to Git. npm packaging is tested to ensure `sbom.cdx.json` is included in the published tarball.
45
+ The SBOM is generated from the release `package.json`/`package-lock.json` as CycloneDX 1.7, the current 1.x format, to satisfy consumers and regulatory baselines requiring CycloneDX 1.6 or later. It is intentionally not committed to Git. The release publishes `sbom.cdx.json` in the npm tarball and as a GitHub Release asset, while the workflow also retains it as an Actions artifact.
46
46
 
47
47
  Release concurrency is serialized so two updates to `main` cannot publish competing versions.
48
48
 
@@ -90,7 +90,8 @@ The release workflow runs the reusable CI gate before semantic-release. After pu
90
90
  ```text
91
91
  gh run list --workflow release.yml --limit 5
92
92
  gh run view <run-id> --json conclusion,headSha,jobs
93
- gh release view v<version> --json tagName,publishedAt,url
93
+ gh release view v<version> --json tagName,publishedAt,url,assets
94
+ gh run view <run-id> --json conclusion,headSha,jobs,artifacts
94
95
  npm view harness-everything version dist-tags --json
95
96
  ```
96
97
 
@@ -14,7 +14,7 @@ const {
14
14
 
15
15
  const ROOT = path.resolve(__dirname, '..');
16
16
 
17
- function fakeHost({ claudeInstalled = false, codexInstalled = false, claudeMarketplace = true, codexMarketplace = true, unknownClaudePluginState = false, unknownClaudeYesFlag = false, unknownClaudeJsonFlag = false } = {}) {
17
+ function fakeHost({ claudeInstalled = false, codexInstalled = false, claudeMarketplace = true, codexMarketplace = true, unknownClaudePluginState = false, unknownClaudeYesFlag = false, unknownClaudeJsonFlag = false, clapCodexJsonFlag = false, missingCodexPluginCommand = false } = {}) {
18
18
  const calls = [];
19
19
  const state = {
20
20
  claudeInstalled,
@@ -59,6 +59,27 @@ function fakeHost({ claudeInstalled = false, codexInstalled = false, claudeMarke
59
59
  }
60
60
 
61
61
  if (command === 'codex') {
62
+ // clap-based CLIs reject an option they do not implement as an
63
+ // unexpected argument, not as an unknown option. Read-only listings
64
+ // already have their own plain-text fallback, so this models a build
65
+ // whose mutating commands predate --json.
66
+ if (clapCodexJsonFlag && args.includes('--json') && /^plugin (marketplace (add|upgrade)|add)\b/.test(joined)) {
67
+ return {
68
+ status: 2,
69
+ stdout: '',
70
+ 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>"
71
+ };
72
+ }
73
+ // Codex builds that ship only `codex plugin marketplace` have no plugin
74
+ // install/list surface at all.
75
+ if (missingCodexPluginCommand && /^plugin (list|add)\b/.test(joined)) {
76
+ const subcommand = joined.split(' ')[1];
77
+ return {
78
+ status: 2,
79
+ stdout: '',
80
+ stderr: `error: unrecognized subcommand '${subcommand}'\n\nUsage: codex plugin [OPTIONS] <COMMAND>`
81
+ };
82
+ }
62
83
  if (joined.includes('marketplace list --json')) {
63
84
  return { status: 0, stdout: JSON.stringify({ marketplaces: state.codexMarketplace ? [{ name: 'harness-everything' }] : [] }), stderr: '' };
64
85
  }
@@ -165,6 +186,41 @@ assert.strictEqual(findPlugin({ plugins: [{ id: 'harness-everything@harness-ever
165
186
  assert.strictEqual(callsFor(fake.calls, 'codex', 'plugin add').length, 0, 'installed Codex plugin must not be re-added');
166
187
  }
167
188
 
189
+ {
190
+ const fake = fakeHost({ codexInstalled: false, codexMarketplace: false, clapCodexJsonFlag: true });
191
+ const result = runHost('codex', fake);
192
+ const addCalls = callsFor(fake.calls, 'codex', 'plugin marketplace add');
193
+ const installCalls = callsFor(fake.calls, 'codex', 'plugin add');
194
+ assert.strictEqual(result.failed, 0, 'clap-style --json rejection must fall back to a compatible Codex command');
195
+ assert.strictEqual(result.results[0].action, 'install');
196
+ assert.strictEqual(addCalls.length, 2, 'unsupported Codex --json must cause one marketplace add retry');
197
+ assert.ok(addCalls[0].args.includes('--json'), 'first Codex marketplace add should use the current flags');
198
+ assert.ok(!addCalls[1].args.includes('--json'), 'Codex compatibility retry must omit unsupported --json');
199
+ assert.ok(installCalls.some(call => !call.args.includes('--json')), 'Codex plugin install must also retry without --json');
200
+ }
201
+
202
+ {
203
+ const fake = fakeHost({ codexInstalled: true, clapCodexJsonFlag: true });
204
+ const result = runHost('codex', fake);
205
+ const upgradeCalls = callsFor(fake.calls, 'codex', 'plugin marketplace upgrade');
206
+ assert.strictEqual(result.failed, 0, 'clap-style --json rejection must not fail the Codex upgrade path');
207
+ assert.strictEqual(result.results[0].action, 'update');
208
+ assert.strictEqual(upgradeCalls.length, 2, 'unsupported Codex --json must cause one upgrade retry');
209
+ assert.ok(!upgradeCalls[1].args.includes('--json'), 'Codex upgrade retry must omit unsupported --json');
210
+ }
211
+
212
+ {
213
+ const fake = fakeHost({ codexMarketplace: false, missingCodexPluginCommand: true, clapCodexJsonFlag: true });
214
+ const result = runHost('codex', fake);
215
+ assert.strictEqual(result.failed, 0, 'a Codex build without plugin subcommands is a capability boundary, not a failure');
216
+ assert.strictEqual(result.skipped, 1, 'the boundary must be reported as a distinct skipped outcome');
217
+ assert.strictEqual(result.results[0].reasonCode, 'host-capability-boundary', 'a capability boundary must not be reported as a missing CLI');
218
+ assert.match(result.results[0].reason, /codex plugin list/, 'the skip reason must name the missing host command');
219
+ assert.match(result.results[0].reason, /upgrade/i, 'the skip reason must stay actionable');
220
+ assert.strictEqual(callsFor(fake.calls, 'codex', 'plugin marketplace add').length, 2, 'the supported marketplace registration must still run');
221
+ assert.strictEqual(callsFor(fake.calls, 'codex', 'plugin add').length, 0, 'an unsupported host must not be blindly installed into');
222
+ }
223
+
168
224
  {
169
225
  const fake = fakeHost({ unknownClaudePluginState: true });
170
226
  const result = runHost('claude', fake);
@@ -178,6 +234,7 @@ assert.strictEqual(findPlugin({ plugins: [{ id: 'harness-everything@harness-ever
178
234
  const result = sync({ host: 'all' }, { commandAvailable: () => false, log: () => {}, warn: () => {} });
179
235
  assert.strictEqual(result.skipped, 2);
180
236
  assert.strictEqual(result.failed, 0);
237
+ assert.ok(result.results.every(entry => entry.reasonCode === 'cli-not-found'), 'an absent CLI must stay distinguishable from a capability boundary');
181
238
  }
182
239
 
183
240
  if (process.platform === 'win32') {
@@ -24,6 +24,22 @@ assert.ok(execPlugin, 'semantic-release exec plugin is required for SBOM generat
24
24
  assert.match(execPlugin[1].prepareCmd, /generate-sbom\.js --output sbom\.cdx\.json --version \$\{nextRelease\.version\}/);
25
25
  assert.match(execPlugin[1].prepareCmd, /generate-sbom\.js --validate sbom\.cdx\.json --version \$\{nextRelease\.version\}/);
26
26
 
27
+ const githubPlugin = releaseConfig.plugins.find(plugin => Array.isArray(plugin) && plugin[0] === '@semantic-release/github');
28
+ assert.ok(githubPlugin, 'semantic-release github plugin is required for release publication');
29
+ assert.ok(Array.isArray(githubPlugin[1].assets), 'GitHub Release assets must be configured');
30
+ assert.ok(
31
+ githubPlugin[1].assets.some(asset => asset && typeof asset === 'object' && asset.path === 'sbom.cdx.json'),
32
+ 'GitHub Release must publish the generated SBOM as an asset'
33
+ );
34
+
35
+ const releaseWorkflow = fs.readFileSync(path.join(ROOT, '.github/workflows/release.yml'), 'utf8');
36
+ assert.match(releaseWorkflow, /uses:\s+actions\/upload-artifact@v4/,
37
+ 'release workflow must retain the generated SBOM as an Actions artifact');
38
+ assert.match(releaseWorkflow, /if:\s+\$\{\{\s*hashFiles\('sbom\.cdx\.json'\)\s*!=\s*''\s*\}\}/,
39
+ 'release workflow must skip artifact upload when semantic-release has no release');
40
+ assert.match(releaseWorkflow, /path:\s+sbom\.cdx\.json/,
41
+ 'release workflow artifact must point at the generated SBOM');
42
+
27
43
  const bom = buildBom(ROOT, {
28
44
  version: packageJson.version,
29
45
  timestamp: '2026-09-21T00:00:00.000Z',
@@ -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,8 +80,11 @@ 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
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.
@@ -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` 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.
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.3",
4
+ "version": "0.20.5",
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.3",
3
+ "version": "0.20.5",
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.3",
3
+ "version": "0.20.5",
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.3",
4
+ "version": "0.20.5",
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:c82181bb-3272-5951-8990-f785d038f412",
5
+ "serialNumber": "urn:uuid:2fe1f791-72b1-528d-91ab-182e028b8f11",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-22T01:49:40.805Z",
8
+ "timestamp": "2026-09-22T14:48:08.473Z",
9
9
  "tools": [
10
10
  {
11
11
  "vendor": "Harness Everything",
12
12
  "name": "generate-sbom.js",
13
- "version": "0.20.3"
13
+ "version": "0.20.5"
14
14
  }
15
15
  ],
16
16
  "component": {
17
17
  "type": "application",
18
- "bom-ref": "pkg:npm/harness-everything@0.20.3",
18
+ "bom-ref": "pkg:npm/harness-everything@0.20.5",
19
19
  "name": "harness-everything",
20
- "version": "0.20.3",
21
- "purl": "pkg:npm/harness-everything@0.20.3"
20
+ "version": "0.20.5",
21
+ "purl": "pkg:npm/harness-everything@0.20.5"
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.3",
2002
+ "ref": "pkg:npm/harness-everything@0.20.5",
2003
2003
  "dependsOn": [
2004
2004
  "pkg:npm/%40commitlint/cli@19.8.1",
2005
2005
  "pkg:npm/%40commitlint/config-conventional@19.8.1",
@@ -189,41 +189,73 @@ function query(context, command, args) {
189
189
  return context.runner(command, args, { query: true }) || { status: 1, stdout: '', stderr: 'runner returned no result' };
190
190
  }
191
191
 
192
- function hasUnknownOption(result, option) {
193
- const output = [result && result.stderr, result && result.stdout, result && result.error && result.error.message]
192
+ function resultText(result) {
193
+ return [result && result.stderr, result && result.stdout, result && result.error && result.error.message]
194
194
  .map(value => String(value || ''))
195
195
  .join('\n');
196
- return new RegExp(`unknown option\\s+['"]?${escapeRegExp(option)}['"]?`, 'i').test(output);
197
196
  }
198
197
 
199
- function runClaudePluginMutation(context, action, pluginId) {
200
- const baseArgs = [
201
- 'plugin', action, pluginId,
202
- '--scope', 'user'
203
- ];
204
- let args = [...baseArgs, '--yes', '--json'];
198
+ // Host CLIs disagree on how they reject an option they never implemented:
199
+ // commander-based CLIs (Claude) say "unknown option '--json'", while
200
+ // clap-based CLIs (Codex) say "unexpected argument '--json' found". Both mean
201
+ // the same thing, so compatibility retries must recognize either wording.
202
+ function hasUnsupportedOption(result, option) {
203
+ const output = resultText(result);
204
+ const name = escapeRegExp(option);
205
+ return [
206
+ `unknown option\\s+['"]?${name}['"]?`,
207
+ `unrecognized option\\s+['"]?${name}['"]?`,
208
+ `unexpected argument\\s+['"]?${name}['"]?`,
209
+ `found argument\\s+['"]?${name}['"]?`
210
+ ].some(pattern => new RegExp(pattern, 'i').test(output));
211
+ }
212
+
213
+ // A host build that never shipped the subcommand is a capability boundary, not
214
+ // a transient failure, so it must be reported differently from a broken call.
215
+ function hasUnsupportedSubcommand(result, subcommand) {
216
+ const expression = new RegExp(
217
+ `(unrecognized|unknown|invalid)\\s+(sub)?command\\s+['"]?${escapeRegExp(subcommand)}['"]?`,
218
+ 'i'
219
+ );
220
+ return expression.test(resultText(result));
221
+ }
222
+
223
+ // Optional arguments are dropped one at a time so a host that implements only
224
+ // some of them still reaches a command it accepts.
225
+ function runHostMutation(context, command, baseArgs, optionalArgs, label) {
226
+ let args = [...baseArgs, ...optionalArgs];
205
227
  let attempt = 0;
206
228
 
207
229
  while (true) {
208
- const result = invoke(context, 'claude', args, {
230
+ const result = invoke(context, command, args, {
209
231
  mutate: true,
210
- label: attempt === 0 ? `claude plugin ${action}` : `claude plugin ${action} compatibility retry`,
232
+ label: attempt === 0 ? label : `${label} compatibility retry`,
211
233
  warnOnFailure: false
212
234
  });
213
235
  if (result.status === 0) return result;
214
236
 
215
- const unsupported = ['--yes', '--json'].find(option => args.includes(option) && hasUnknownOption(result, option));
237
+ const unsupported = optionalArgs.find(option => args.includes(option) && hasUnsupportedOption(result, option));
216
238
  if (!unsupported) {
217
- context.warn(`[failed] ${commandLine('claude', args)}\n ${resultError(result)}`);
239
+ context.warn(`[failed] ${commandLine(command, args)}\n ${resultError(result)}`);
218
240
  return result;
219
241
  }
220
242
 
221
243
  args = args.filter(arg => arg !== unsupported);
222
244
  attempt += 1;
223
- context.warn(`[notice] Claude CLI does not support ${unsupported}; retrying ${action} without that option.`);
245
+ context.warn(`[notice] ${command} CLI does not support ${unsupported}; retrying ${label} without that option.`);
224
246
  }
225
247
  }
226
248
 
249
+ function runClaudePluginMutation(context, action, pluginId) {
250
+ return runHostMutation(
251
+ context,
252
+ 'claude',
253
+ ['plugin', action, pluginId, '--scope', 'user'],
254
+ ['--yes', '--json'],
255
+ `claude plugin ${action}`
256
+ );
257
+ }
258
+
227
259
  function listState(context, { command, jsonArgs, plainArgs, detectJson, detectText }) {
228
260
  const jsonResult = query(context, command, jsonArgs);
229
261
  if (jsonResult.status === 0) {
@@ -239,6 +271,7 @@ function listState(context, { command, jsonArgs, plainArgs, detectJson, detectTe
239
271
  return {
240
272
  known: false,
241
273
  value: false,
274
+ attempts: [jsonResult, plainResult],
242
275
  error: `${resultError(jsonResult)}; fallback: ${resultError(plainResult)}`
243
276
  };
244
277
  }
@@ -312,11 +345,13 @@ function ensureCodexMarketplace(context) {
312
345
  const state = queryCodexMarketplace(context);
313
346
  if (!state.known) context.warn(`[notice] Codex marketplace state is unavailable; ensuring ${context.marketplace} is configured.`);
314
347
  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' });
348
+ const added = runHostMutation(
349
+ context,
350
+ 'codex',
351
+ ['plugin', 'marketplace', 'add', context.repositorySource, '--ref', context.ref],
352
+ ['--json'],
353
+ 'codex marketplace add'
354
+ );
320
355
  if (added.status !== 0) return { ok: false, error: resultError(added) };
321
356
  }
322
357
  return { ok: true };
@@ -332,10 +367,13 @@ function refreshClaudeMarketplace(context) {
332
367
  }
333
368
 
334
369
  function refreshCodexMarketplace(context) {
335
- const upgraded = invoke(context, 'codex', [
336
- 'plugin', 'marketplace', 'upgrade', context.marketplace,
337
- '--json'
338
- ], { mutate: true, label: 'codex marketplace upgrade' });
370
+ const upgraded = runHostMutation(
371
+ context,
372
+ 'codex',
373
+ ['plugin', 'marketplace', 'upgrade', context.marketplace],
374
+ ['--json'],
375
+ 'codex marketplace upgrade'
376
+ );
339
377
  return upgraded.status === 0
340
378
  ? { ok: true }
341
379
  : { ok: false, error: resultError(upgraded) };
@@ -350,7 +388,7 @@ function verifyPlugin(context, host, queryPlugin) {
350
388
  }
351
389
 
352
390
  function syncClaude(context) {
353
- if (!context.commandAvailable('claude')) return { host: 'claude', status: 'skipped', reason: 'claude CLI not found' };
391
+ if (!context.commandAvailable('claude')) return { host: 'claude', status: 'skipped', reasonCode: 'cli-not-found', reason: 'claude CLI not found' };
354
392
  const marketplace = ensureClaudeMarketplace(context);
355
393
  if (!marketplace.ok) return { host: 'claude', status: 'failed', error: marketplace.error };
356
394
  const installed = queryClaudePlugin(context);
@@ -372,11 +410,25 @@ function syncClaude(context) {
372
410
  }
373
411
 
374
412
  function syncCodex(context) {
375
- if (!context.commandAvailable('codex')) return { host: 'codex', status: 'skipped', reason: 'codex CLI not found' };
413
+ if (!context.commandAvailable('codex')) return { host: 'codex', status: 'skipped', reasonCode: 'cli-not-found', reason: 'codex CLI not found' };
376
414
  const marketplace = ensureCodexMarketplace(context);
377
415
  if (!marketplace.ok) return { host: 'codex', status: 'failed', error: marketplace.error };
378
416
  const installed = queryCodexPlugin(context);
379
- if (!installed.known) return { host: 'codex', status: 'failed', error: `Codex plugin state could not be determined: ${installed.error}` };
417
+ if (!installed.known) {
418
+ // A Codex build that ships only `codex plugin marketplace` cannot report
419
+ // or change plugin state at all. The marketplace source is now registered,
420
+ // which is everything this build supports, so report the host boundary
421
+ // instead of a generic failure.
422
+ if ((installed.attempts || []).some(attempt => hasUnsupportedSubcommand(attempt, 'list'))) {
423
+ return {
424
+ host: 'codex',
425
+ status: 'skipped',
426
+ reasonCode: 'host-capability-boundary',
427
+ 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\``
428
+ };
429
+ }
430
+ return { host: 'codex', status: 'failed', error: `Codex plugin state could not be determined: ${installed.error}` };
431
+ }
380
432
 
381
433
  let action;
382
434
  let result;
@@ -392,9 +444,13 @@ function syncCodex(context) {
392
444
  action = 'install';
393
445
  result = refreshCodexMarketplace(context);
394
446
  if (result.ok) {
395
- result = invoke(context, 'codex', [
396
- 'plugin', 'add', `${context.plugin}@${context.marketplace}`, '--json'
397
- ], { mutate: true, label: 'codex plugin install' });
447
+ result = runHostMutation(
448
+ context,
449
+ 'codex',
450
+ ['plugin', 'add', `${context.plugin}@${context.marketplace}`],
451
+ ['--json'],
452
+ 'codex plugin install'
453
+ );
398
454
  } else {
399
455
  result = { status: 1, stdout: '', stderr: result.error };
400
456
  }
@@ -488,7 +544,9 @@ function main(argv = process.argv.slice(2)) {
488
544
  else console.error(`[fail] ${result.host}: ${result.error}`);
489
545
  }
490
546
  if (summary.synchronized === 0 && summary.failed === 0) {
491
- console.error('No supported host CLI was found. Install Claude Code and/or Codex, then retry.');
547
+ console.error(summary.results.every(result => result.reasonCode === 'cli-not-found')
548
+ ? 'No supported host CLI was found. Install Claude Code and/or Codex, then retry.'
549
+ : 'No host plugin was synchronized. Review the skip reasons above.');
492
550
  }
493
551
  }
494
552
  return summary.failed > 0 || summary.synchronized === 0 ? 1 : 0;