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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/release.yml +8 -0
- package/.releaserc.json +6 -0
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/RELEASING.md +4 -3
- package/ci/mechanism-2ao-plugin-sync.test.js +58 -1
- package/ci/mechanism-2ap-sbom.test.js +16 -0
- package/docs/architecture.md +4 -4
- package/docs/platform-capabilities.md +3 -0
- package/docs/platform-compatibility.json +1 -1
- package/docs/troubleshooting.md +7 -0
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/sbom.cdx.json +7 -7
- package/scripts/plugin-sync.js +88 -30
|
@@ -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.
|
|
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.
|
|
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
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),
|
|
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.
|
|
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',
|
package/docs/architecture.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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;
|
|
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
|
},
|
package/docs/troubleshooting.md
CHANGED
|
@@ -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
|
package/package.json
CHANGED
|
@@ -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
|
+
"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:
|
|
5
|
+
"serialNumber": "urn:uuid:2fe1f791-72b1-528d-91ab-182e028b8f11",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-
|
|
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.
|
|
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.
|
|
18
|
+
"bom-ref": "pkg:npm/harness-everything@0.20.5",
|
|
19
19
|
"name": "harness-everything",
|
|
20
|
-
"version": "0.20.
|
|
21
|
-
"purl": "pkg:npm/harness-everything@0.20.
|
|
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.
|
|
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",
|
package/scripts/plugin-sync.js
CHANGED
|
@@ -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
|
|
193
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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,
|
|
230
|
+
const result = invoke(context, command, args, {
|
|
209
231
|
mutate: true,
|
|
210
|
-
label: attempt === 0 ?
|
|
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 =
|
|
237
|
+
const unsupported = optionalArgs.find(option => args.includes(option) && hasUnsupportedOption(result, option));
|
|
216
238
|
if (!unsupported) {
|
|
217
|
-
context.warn(`[failed] ${commandLine(
|
|
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]
|
|
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 =
|
|
316
|
-
|
|
317
|
-
'
|
|
318
|
-
'--
|
|
319
|
-
|
|
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 =
|
|
336
|
-
|
|
337
|
-
'
|
|
338
|
-
|
|
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)
|
|
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 =
|
|
396
|
-
|
|
397
|
-
|
|
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(
|
|
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;
|