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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/ci/mechanism-2ao-plugin-sync.test.js +117 -1
- package/docs/architecture.md +4 -4
- package/docs/platform-capabilities.md +4 -1
- 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 +160 -33
|
@@ -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.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
|
+
"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/);
|
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,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
|
|
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;
|
|
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`/`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
|
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.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:
|
|
5
|
+
"serialNumber": "urn:uuid:dad0ee6f-3060-5d2e-a044-10420f0fa6e3",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-
|
|
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.
|
|
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.
|
|
18
|
+
"bom-ref": "pkg:npm/harness-everything@0.20.6",
|
|
19
19
|
"name": "harness-everything",
|
|
20
|
-
"version": "0.20.
|
|
21
|
-
"purl": "pkg:npm/harness-everything@0.20.
|
|
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.
|
|
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",
|
package/scripts/plugin-sync.js
CHANGED
|
@@ -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
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
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
|
|
193
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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,
|
|
298
|
+
const result = invoke(context, command, args, {
|
|
209
299
|
mutate: true,
|
|
210
|
-
label: attempt === 0 ?
|
|
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 =
|
|
305
|
+
const unsupported = optionalArgs.find(option => args.includes(option) && hasUnsupportedOption(result, option));
|
|
216
306
|
if (!unsupported) {
|
|
217
|
-
context.warn(`[failed] ${commandLine(
|
|
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]
|
|
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 =
|
|
316
|
-
|
|
317
|
-
'
|
|
318
|
-
'--
|
|
319
|
-
|
|
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 =
|
|
336
|
-
|
|
337
|
-
'
|
|
338
|
-
|
|
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)
|
|
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 =
|
|
396
|
-
|
|
397
|
-
|
|
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(
|
|
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
|