harness-everything 0.18.1 → 0.19.0

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.18.1",
13
+ "version": "0.19.0",
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.18.1",
4
+ "version": "0.19.0",
5
5
  "author": {
6
6
  "name": "Miya Daniel"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.19.0](https://github.com/dyphn1/Harness-everything/compare/v0.18.1...v0.19.0) (2026-09-21)
6
+
7
+ ### Features
8
+
9
+ * **distribution:** add cross-platform plugin install/update command ([0566bf3](https://github.com/dyphn1/Harness-everything/commit/0566bf347feab785af829e0f257fa22899f5c259))
10
+
5
11
  ## [0.18.1](https://github.com/dyphn1/Harness-everything/compare/v0.18.0...v0.18.1) (2026-09-21)
6
12
 
7
13
  ### Bug Fixes
package/README.md CHANGED
@@ -58,6 +58,11 @@ Harness integrates directly into your workspace. There is no heavy daemon, no pa
58
58
  # Option B: install Harness hooks/skills/advisory integrations into your workspace
59
59
  npx github:dyphn1/Harness-everything install
60
60
 
61
+ # Option C: install or update the native Claude Code/Codex plugin
62
+ # npx github:dyphn1/Harness-everything plugin-sync
63
+ # ./scripts/plugin-sync.sh # POSIX shells / Git Bash
64
+ # powershell -File scripts/plugin-sync.ps1 # Windows PowerShell
65
+
61
66
  # OpenAI/Codex local plugin packaging is repository-owned under:
62
67
  # .agents/plugins/marketplace.json
63
68
  # plugins/harness-everything/.codex-plugin/plugin.json
@@ -90,6 +95,8 @@ For `--global`, the installer uses each host's supported user-level skill locati
90
95
 
91
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).
92
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.
99
+
93
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:
94
101
 
95
102
  ```bash
package/bin/cli.js CHANGED
@@ -40,6 +40,9 @@ switch (command) {
40
40
  case 'codex-hooks':
41
41
  runCodexHooksCompat(args.slice(1));
42
42
  break;
43
+ case 'plugin-sync':
44
+ runPluginSync(args.slice(1));
45
+ break;
43
46
  default:
44
47
  console.error(`[Error] Unknown command: "${command}"`);
45
48
  showHelp();
@@ -104,6 +107,9 @@ Commands:
104
107
  that fail to mount enabled plugin-bundled hooks.
105
108
  Usage: harness codex-hooks <install|status|uninstall>
106
109
  Does not trust hooks or modify Codex config.toml.
110
+ plugin-sync Install or update the native Harness plugin on Claude Code
111
+ and Codex. Existing installations always take the update
112
+ path. Supports --host, --dry-run, and --json.
107
113
  verify Run the pre-delivery verification gate (lint/test from the
108
114
  nearest package.json). Exits non-zero if checks fail. This is
109
115
  the explicit stand-in for a hard stop gate on platforms where
@@ -179,3 +185,13 @@ function runCodexHooksCompat(compatArgs) {
179
185
  const result = spawnSync('node', [script, ...compatArgs], { stdio: 'inherit' });
180
186
  process.exit(result.status === null ? 1 : result.status);
181
187
  }
188
+
189
+ function runPluginSync(pluginArgs) {
190
+ const script = path.resolve(__dirname, '..', 'scripts', 'plugin-sync.js');
191
+ if (!fs.existsSync(script)) {
192
+ console.error('[Error] Native plugin synchronization script not found.');
193
+ process.exit(1);
194
+ }
195
+ const result = spawnSync('node', [script, ...pluginArgs], { stdio: 'inherit' });
196
+ process.exit(result.status === null ? 1 : result.status);
197
+ }
@@ -0,0 +1,163 @@
1
+ 'use strict';
2
+
3
+ const assert = require('assert');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const {
7
+ findMarketplace,
8
+ findPlugin,
9
+ parseJsonOutput,
10
+ sync
11
+ } = require('../scripts/plugin-sync');
12
+
13
+ const ROOT = path.resolve(__dirname, '..');
14
+
15
+ function fakeHost({ claudeInstalled = false, codexInstalled = false, claudeMarketplace = true, codexMarketplace = true, unknownClaudePluginState = false } = {}) {
16
+ const calls = [];
17
+ const state = {
18
+ claudeInstalled,
19
+ codexInstalled,
20
+ claudeMarketplace,
21
+ codexMarketplace
22
+ };
23
+ const runner = (command, args) => {
24
+ calls.push({ command, args: [...args] });
25
+ const joined = args.join(' ');
26
+
27
+ if (command === 'claude') {
28
+ if (joined.includes('marketplace list --json')) {
29
+ return { status: 0, stdout: JSON.stringify({ marketplaces: state.claudeMarketplace ? [{ name: 'harness-everything' }] : [] }), stderr: '' };
30
+ }
31
+ if (joined === 'plugin marketplace list') {
32
+ return { status: 0, stdout: state.claudeMarketplace ? 'harness-everything\n' : '', stderr: '' };
33
+ }
34
+ if (joined.startsWith('plugin marketplace add')) {
35
+ state.claudeMarketplace = true;
36
+ return { status: 0, stdout: '', stderr: '' };
37
+ }
38
+ if (joined.startsWith('plugin marketplace update')) return { status: 0, stdout: '', stderr: '' };
39
+ if (joined.includes('plugin list --json')) {
40
+ if (unknownClaudePluginState) return { status: 1, stdout: '', stderr: 'unsupported --json' };
41
+ return { status: 0, stdout: JSON.stringify({ plugins: state.claudeInstalled ? [{ id: 'harness-everything@harness-everything' }] : [] }), stderr: '' };
42
+ }
43
+ if (joined === 'plugin list') {
44
+ if (unknownClaudePluginState) return { status: 1, stdout: '', stderr: 'unsupported list' };
45
+ return { status: 0, stdout: state.claudeInstalled ? 'harness-everything@harness-everything\n' : '', stderr: '' };
46
+ }
47
+ if (joined.startsWith('plugin install') || joined.startsWith('plugin update')) {
48
+ state.claudeInstalled = true;
49
+ return { status: 0, stdout: '{}', stderr: '' };
50
+ }
51
+ }
52
+
53
+ if (command === 'codex') {
54
+ if (joined.includes('marketplace list --json')) {
55
+ return { status: 0, stdout: JSON.stringify({ marketplaces: state.codexMarketplace ? [{ name: 'harness-everything' }] : [] }), stderr: '' };
56
+ }
57
+ if (joined === 'plugin marketplace list') {
58
+ return { status: 0, stdout: state.codexMarketplace ? 'harness-everything\n' : '', stderr: '' };
59
+ }
60
+ if (joined.startsWith('plugin marketplace add')) {
61
+ state.codexMarketplace = true;
62
+ return { status: 0, stdout: '{}', stderr: '' };
63
+ }
64
+ if (joined.startsWith('plugin marketplace upgrade')) return { status: 0, stdout: '{}', stderr: '' };
65
+ if (joined.includes('plugin list --marketplace harness-everything --json') || joined === 'plugin list --json') {
66
+ return { status: 0, stdout: JSON.stringify({ plugins: state.codexInstalled ? [{ name: 'harness-everything', marketplace: 'harness-everything' }] : [] }), stderr: '' };
67
+ }
68
+ if (joined === 'plugin list --marketplace harness-everything' || joined === 'plugin list') {
69
+ return { status: 0, stdout: state.codexInstalled ? 'harness-everything@harness-everything\n' : '', stderr: '' };
70
+ }
71
+ if (joined.startsWith('plugin add')) {
72
+ state.codexInstalled = true;
73
+ return { status: 0, stdout: '{}', stderr: '' };
74
+ }
75
+ }
76
+
77
+ return { status: 0, stdout: '', stderr: '' };
78
+ };
79
+
80
+ return {
81
+ calls,
82
+ runner,
83
+ commandAvailable: () => true
84
+ };
85
+ }
86
+
87
+ function callsFor(calls, command, prefix) {
88
+ return calls.filter(call => call.command === command && call.args.join(' ').startsWith(prefix));
89
+ }
90
+
91
+ function runHost(host, fake) {
92
+ return sync({ host, repository: 'dyphn1/Harness-everything', ref: 'main' }, {
93
+ ...fake,
94
+ log: () => {},
95
+ warn: () => {}
96
+ });
97
+ }
98
+
99
+ assert.strictEqual(parseJsonOutput('status\n{"ok":true}\n').ok, true);
100
+ assert.strictEqual(findMarketplace({ marketplaces: [{ name: 'harness-everything' }] }, 'harness-everything'), true);
101
+ assert.strictEqual(findPlugin({ plugins: [{ id: 'harness-everything@harness-everything' }] }, 'harness-everything', 'harness-everything'), true);
102
+
103
+ {
104
+ const fake = fakeHost({ claudeInstalled: false });
105
+ const result = runHost('claude', fake);
106
+ assert.strictEqual(result.failed, 0);
107
+ assert.strictEqual(result.results[0].action, 'install');
108
+ assert.strictEqual(callsFor(fake.calls, 'claude', 'plugin install').length, 1, 'absent Claude plugin must install');
109
+ assert.strictEqual(callsFor(fake.calls, 'claude', 'plugin update').length, 0, 'absent Claude plugin must not update');
110
+ }
111
+
112
+ {
113
+ const fake = fakeHost({ claudeInstalled: true });
114
+ const result = runHost('claude', fake);
115
+ assert.strictEqual(result.failed, 0);
116
+ assert.strictEqual(result.results[0].action, 'update');
117
+ assert.strictEqual(callsFor(fake.calls, 'claude', 'plugin update').length, 1, 'installed Claude plugin must update');
118
+ assert.strictEqual(callsFor(fake.calls, 'claude', 'plugin install').length, 0, 'installed Claude plugin must not reinstall');
119
+ }
120
+
121
+ {
122
+ const fake = fakeHost({ codexInstalled: false });
123
+ const result = runHost('codex', fake);
124
+ assert.strictEqual(result.failed, 0);
125
+ assert.strictEqual(result.results[0].action, 'install');
126
+ assert.strictEqual(callsFor(fake.calls, 'codex', 'plugin add').length, 1, 'absent Codex plugin must install');
127
+ }
128
+
129
+ {
130
+ const fake = fakeHost({ codexInstalled: true });
131
+ const result = runHost('codex', fake);
132
+ assert.strictEqual(result.failed, 0);
133
+ assert.strictEqual(result.results[0].action, 'update');
134
+ assert.strictEqual(callsFor(fake.calls, 'codex', 'plugin marketplace upgrade').length, 1, 'installed Codex plugin must upgrade its marketplace');
135
+ assert.strictEqual(callsFor(fake.calls, 'codex', 'plugin add').length, 0, 'installed Codex plugin must not be re-added');
136
+ }
137
+
138
+ {
139
+ const fake = fakeHost({ unknownClaudePluginState: true });
140
+ const result = runHost('claude', fake);
141
+ assert.strictEqual(result.failed, 1, 'unknown installed state must fail closed');
142
+ assert.strictEqual(callsFor(fake.calls, 'claude', 'plugin install').length, 0, 'unknown state must not install');
143
+ assert.strictEqual(callsFor(fake.calls, 'claude', 'plugin update').length, 0, 'unknown state must not update blindly');
144
+ assert.strictEqual(callsFor(fake.calls, 'claude', 'plugin marketplace update').length, 0, 'unknown state must not refresh the marketplace blindly');
145
+ }
146
+
147
+ {
148
+ const result = sync({ host: 'all' }, { commandAvailable: () => false, log: () => {}, warn: () => {} });
149
+ assert.strictEqual(result.skipped, 2);
150
+ assert.strictEqual(result.failed, 0);
151
+ }
152
+
153
+ for (const wrapper of ['scripts/plugin-sync.sh', 'scripts/plugin-sync.ps1']) {
154
+ const text = fs.readFileSync(path.join(ROOT, wrapper), 'utf8');
155
+ assert.match(text, /plugin-sync\.js/);
156
+ assert.doesNotMatch(text, /\.codex\/plugins\/cache|config\.toml/);
157
+ }
158
+
159
+ const cli = fs.readFileSync(path.join(ROOT, 'bin', 'cli.js'), 'utf8');
160
+ assert.match(cli, /case 'plugin-sync'/);
161
+ assert.match(cli, /Existing installations always take the update/);
162
+
163
+ console.log('Native plugin sync verified: absent plugins install, installed plugins update, and unknown state fails closed.');
@@ -27,9 +27,9 @@ This is a **repository/agent contract**, not an automatic upgrade to any platfor
27
27
 
28
28
  | Platform surface | Standalone skills | Plugin support | Hooks/lifecycle | Project scope | Global scope | Current evidence boundary |
29
29
  | --- | --- | --- | --- | --- | --- | --- |
30
- | Codex | `Supported` | `Mechanism verified` | `Mechanism verified` for the local OpenAI package, including session, prompt, supported-tool, subagent, and stop hooks | `.agents/skills/`, `AGENTS.md` | `~/.agents/skills/` | Local package hooks are mechanically checked; live loading and marketplace refresh remain `Unknown`. |
30
+ | Codex | `Supported` | `Mechanism verified` | `Mechanism verified` for the local OpenAI package, including session, prompt, supported-tool, subagent, and stop hooks | `.agents/skills/`, `AGENTS.md` | `~/.agents/skills/` | Local package hooks and the state-specific marketplace sync command are mechanically checked; live loading and marketplace refresh remain unverified. |
31
31
  | ChatGPT / OpenAI Plugin | `Unknown` for a repository-local standalone path | `Mechanism verified` | `Partial`: local Work/Codex hooks are separate from ordinary Chat | Managed workspace marketplace | Not established | The public Skills-only bundle is mechanically reproducible; import, approval, and live behavior remain `Unknown`. |
32
- | Claude Code | `Supported` | `Supported` | `Mechanism verified` for Harness configuration | `.claude/skills/` | `~/.claude/skills/` | Native Claude surfaces are documented and package/installer checks pass; fresh-host execution is not preserved here. |
32
+ | Claude Code | `Supported` | `Supported` | `Mechanism verified` for Harness configuration | `.claude/skills/` | `~/.claude/skills/` | Native Claude surfaces and the state-specific plugin sync command are mechanically checked; fresh-host execution is not preserved here. |
33
33
  | OpenCode | `Supported` | `Partial` live-host loading for the project-scope, `.js`-filename install path on OpenCode 1.18.31 (macOS); other install paths remain `Mechanism verified` | `Partial` live-host evidence for project-scope `.js` loading and edit/verification state; hard lock reported only, with no retained blocked-tool trace and a post-reset final snapshot | `.opencode/skills/`, `.agents/skills/`, `.opencode/plugins/` | `~/.config/opencode/skills/`, `~/.agents/skills/`, `~/.config/opencode/plugins/` | [Retained evidence](../benchmarks/results/live-host/opencode-2026-09-16/README.md): loading and state effects verified in one host session; final snapshot post-reset; hard lock only an interactive observation; reflection operator-seeded, then agent-rewritten; no raw transcript or behavioral-effectiveness claim. `.mjs` auto-discovery broken (issue #127); global scope, npm-package installation, and other host versions remain unverified. |
34
34
  | GitHub Copilot agent surfaces | `Supported` | `Unknown` for a Harness-specific plugin install | `Unknown` | `.github/skills/`, `.agents/skills/`, repository instructions | `~/.copilot/skills/`, `~/.agents/skills/` | GitHub Agent Skills paths and installer behavior are checked; no live Copilot session or plugin install is preserved. |
35
35
  | Cursor | `Supported` | `Mechanism verified` for the portable package shape | `Unknown` for Harness | `.cursor/skills/`, `.agents/skills/` | `~/.cursor/skills/`, `~/.agents/skills/` | Official paths and package shape are checked; Cursor plugin loading and hooks remain unverified. |
@@ -64,6 +64,26 @@ Codex has two paths that must not be collapsed:
64
64
 
65
65
  The package is tested at the mechanism layer and remains subject to the host’s hook review/trust flow. A fresh host session is still required before claiming that a particular ChatGPT/Codex installation loaded and fired the hooks or that the model followed read-before-skip.
66
66
 
67
+ ### Native plugin install/update synchronization
68
+
69
+ The repository also ships a host-facing synchronization command for the native marketplace/plugin surfaces:
70
+
71
+ ```bash
72
+ npx github:dyphn1/Harness-everything plugin-sync
73
+ # or, from a checkout:
74
+ ./scripts/plugin-sync.sh
75
+ powershell -File scripts/plugin-sync.ps1
76
+ ```
77
+
78
+ The command detects Claude Code and Codex independently, ensures the Harness marketplace is configured, reads the installed-plugin state, then refreshes that marketplace before choosing the operation:
79
+
80
+ - absent plugin → install;
81
+ - existing Claude Code plugin → `claude plugin update harness-everything@harness-everything`;
82
+ - existing Codex plugin → `codex plugin marketplace upgrade harness-everything`;
83
+ - unknown plugin state → fail closed, with no blind install/re-add.
84
+
85
+ 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
+
67
87
  ### Explicit Codex user-hook compatibility fallback
68
88
 
69
89
  Current Codex documentation supports plugin-bundled hooks, so native plugin mounting remains the preferred path. Some host/source combinations have nevertheless failed to expose enabled plugin hooks for review/entry attribution (#122 and upstream Codex reports). Harness therefore provides an **explicit compatibility fallback**, not an automatic replacement for native plugin hooks:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "format": "harness-platform-compatibility-v1",
3
- "last_reviewed": "2026-09-19",
3
+ "last_reviewed": "2026-09-21",
4
4
  "status_vocabulary": {
5
5
  "Supported": "An official or native mechanism exists and the Harness behavior is verified.",
6
6
  "Mechanism verified": "Harness package, adapter, or installer checks pass, but a real host run is not confirmed.",
@@ -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": "Unknown", "notes": "A real marketplace refresh has not been run in a Codex host."},
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."},
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
  },
@@ -159,7 +159,7 @@
159
159
  "globalScope": {"status": "Mechanism verified", "paths": {"skills": ["~/.claude/skills"], "hooks": ["~/.claude/settings.json"]}},
160
160
  "installUninstallSymmetry": {"status": "Mechanism verified", "notes": "Owned settings, skills, agents, and user files are covered by round-trip tests."},
161
161
  "assetsReferences": {"status": "Mechanism verified", "notes": "Skill-local references, scripts, and templates are checked."},
162
- "updateSync": {"status": "Unknown", "notes": "A live marketplace update has not been run in Claude Code."},
162
+ "updateSync": {"status": "Mechanism verified", "notes": "scripts/plugin-sync.js detects installed state and uses the explicit Claude plugin update command for an installed plugin; no live Claude host refresh is preserved."},
163
163
  "liveHostVerification": {"status": "Unknown", "notes": "No fresh Claude Code plugin session artifact is committed for this revision."}
164
164
  }
165
165
  },
@@ -267,4 +267,4 @@
267
267
  }
268
268
  }
269
269
  ]
270
- }
270
+ }
@@ -189,6 +189,14 @@ If those checks pass but the host still shows an older copy:
189
189
 
190
190
  The local plugin mechanically injects the session policy and invariant-first routing contract, and its package includes mechanism-tested supported-tool, subagent, and stop adapters. If those events do not appear in a live session, verify the host loaded the current package and that changed command hooks were reviewed/trusted; package tests alone cannot prove host loading.
191
191
 
192
+ For the native host install/update path, run:
193
+
194
+ ```bash
195
+ npx github:dyphn1/Harness-everything plugin-sync --json
196
+ ```
197
+
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
+
192
200
  See [openai-plugin.md](openai-plugin.md) for installation and validation details.
193
201
 
194
202
  ### 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.18.1",
4
+ "version": "0.19.0",
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.18.1",
3
+ "version": "0.19.0",
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": {
@@ -17,7 +17,7 @@
17
17
  "repo:contract": "node scripts/repository-contract.js",
18
18
  "docs:sync": "node scripts/sync-current-docs.js",
19
19
  "test:repo-contract": "node ci/repository-contract.test.js",
20
- "test:consistency": "node ci/consistency-check.js && node ci/doc-capability-consistency.test.js && node ci/repository-contract.test.js",
20
+ "test:consistency": "node ci/consistency-check.js && node ci/doc-capability-consistency.test.js && node ci/repository-contract.test.js && node ci/mechanism-2ao-plugin-sync.test.js",
21
21
  "test:docs:capabilities": "node ci/doc-capability-consistency.test.js",
22
22
  "test:references": "node ci/reference-check.js",
23
23
  "test:release": "node ci/release-consistency-check.js && node ci/mechanism-2x-release-automation.test.js && node ci/mechanism-2x-release-commit-message.test.js",
@@ -28,10 +28,12 @@
28
28
  "test:routing": "node ci/routing-check.js",
29
29
  "test:routing:invariants": "node ci/invariant-routing.test.js && node ci/router-workflow-plan.test.js && node ci/workflow-execution-contract.test.js",
30
30
  "plugin:sync": "node scripts/sync-openai-plugin.js",
31
+ "plugin:hosts": "node scripts/plugin-sync.js",
31
32
  "test:plugin:claude": "node ci/mechanism-2z-claude-plugin-manifest.test.js",
32
33
  "plugin:claude:stage": "node scripts/stage-claude-plugin.js",
33
34
  "plugin:claude:validate": "node scripts/validate-claude-plugin.js",
34
35
  "test:plugin:openai": "node ci/openai-plugin-package.test.js && node ci/openai-plugin-runtime.test.js",
36
+ "test:plugin:hosts": "node ci/mechanism-2ao-plugin-sync.test.js",
35
37
  "plugin:submission:build": "node scripts/build-openai-submission.js",
36
38
  "test:plugin:submission": "node ci/openai-plugin-submission.test.js",
37
39
  "test:platform:compatibility": "node ci/mechanism-2w-platform-compatibility.test.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.18.1",
3
+ "version": "0.19.0",
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.18.1",
4
+ "version": "0.19.0",
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",
@@ -0,0 +1,465 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Synchronize the native Harness Everything plugin on Claude Code and Codex.
6
+ *
7
+ * This is deliberately separate from installer.js: installer.js manages the
8
+ * portable skills layout, while this command talks to each host's native
9
+ * marketplace/plugin CLI. The installed-state branch is intentionally
10
+ * explicit: an existing plugin is updated, never silently re-added.
11
+ */
12
+
13
+ const { spawnSync } = require('child_process');
14
+
15
+ const DEFAULTS = Object.freeze({
16
+ marketplace: 'harness-everything',
17
+ plugin: 'harness-everything',
18
+ repository: process.env.HARNESS_PLUGIN_REPOSITORY || 'dyphn1/Harness-everything',
19
+ ref: process.env.HARNESS_PLUGIN_REF || 'main'
20
+ });
21
+
22
+ function defaultRunner(command, args) {
23
+ const result = spawnSync(command, args, {
24
+ encoding: 'utf8',
25
+ stdio: ['ignore', 'pipe', 'pipe'],
26
+ windowsHide: true
27
+ });
28
+ return {
29
+ status: result.status === null ? 1 : result.status,
30
+ stdout: result.stdout || '',
31
+ stderr: result.stderr || '',
32
+ error: result.error || null
33
+ };
34
+ }
35
+
36
+ function shellQuote(value) {
37
+ const text = String(value);
38
+ return /^[A-Za-z0-9_./:@%+=,-]+$/.test(text) ? text : JSON.stringify(text);
39
+ }
40
+
41
+ function commandLine(command, args) {
42
+ return [command, ...args].map(shellQuote).join(' ');
43
+ }
44
+
45
+ function parseJsonOutput(output) {
46
+ const text = String(output || '').trim();
47
+ if (!text) return null;
48
+ try {
49
+ return JSON.parse(text);
50
+ } catch (_) {
51
+ // Some host versions print a short status line before one-line JSON.
52
+ const lines = text.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
53
+ for (let index = lines.length - 1; index >= 0; index--) {
54
+ try {
55
+ return JSON.parse(lines[index]);
56
+ } catch (__) {
57
+ // Keep looking for the machine-readable line.
58
+ }
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ function collectObjects(value, out = [], keyHint = '') {
65
+ if (Array.isArray(value)) {
66
+ value.forEach((entry, index) => collectObjects(entry, out, String(index)));
67
+ } else if (value && typeof value === 'object') {
68
+ out.push({ value, keyHint });
69
+ for (const [key, child] of Object.entries(value)) collectObjects(child, out, key);
70
+ }
71
+ return out;
72
+ }
73
+
74
+ function collectStrings(value, out = []) {
75
+ if (typeof value === 'string') out.push(value);
76
+ else if (Array.isArray(value)) value.forEach(entry => collectStrings(entry, out));
77
+ else if (value && typeof value === 'object') Object.values(value).forEach(entry => collectStrings(entry, out));
78
+ return out;
79
+ }
80
+
81
+ function exactString(value, expected) {
82
+ return typeof value === 'string' && value.trim() === expected;
83
+ }
84
+
85
+ function findMarketplace(payload, marketplace) {
86
+ if (collectStrings(payload).some(value => exactString(value, marketplace))) return true;
87
+ return collectObjects(payload).some(({ value }) => {
88
+ const candidates = [
89
+ value.name,
90
+ value.marketplace,
91
+ value.marketplaceName,
92
+ value.slug,
93
+ value.id,
94
+ value.source && value.source.name,
95
+ value.source && value.source.id
96
+ ];
97
+ return candidates.some(candidate => exactString(candidate, marketplace));
98
+ });
99
+ }
100
+
101
+ function findPlugin(payload, plugin, marketplace) {
102
+ const pluginId = `${plugin}@${marketplace}`;
103
+ return collectStrings(payload).some(value => value.trim() === pluginId) || collectObjects(payload).some(({ value, keyHint }) => {
104
+ const identifiers = [value.id, value.pluginId, value.identifier, keyHint];
105
+ if (identifiers.some(candidate => exactString(candidate, pluginId))) return true;
106
+ const names = [value.name, value.plugin, value.pluginName];
107
+ if (!names.some(candidate => exactString(candidate, plugin))) return false;
108
+ const sources = [
109
+ value.marketplace,
110
+ value.marketplaceName,
111
+ value.source && value.source.name,
112
+ value.source && value.source.id
113
+ ].filter(Boolean);
114
+ return sources.length === 0 || sources.some(source => exactString(source, marketplace));
115
+ });
116
+ }
117
+
118
+ function escapeRegExp(value) {
119
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
120
+ }
121
+
122
+ function textHasMarketplace(output, marketplace) {
123
+ const expression = new RegExp(`(^|[\\s"'])${escapeRegExp(marketplace)}($|[\\s"'@])`, 'm');
124
+ return expression.test(String(output || ''));
125
+ }
126
+
127
+ function textHasPlugin(output, plugin, marketplace) {
128
+ const text = String(output || '');
129
+ if (text.includes(`${plugin}@${marketplace}`)) return true;
130
+ const expression = new RegExp(`(^|[\\s"'])${escapeRegExp(plugin)}($|[\\s"'@])`, 'm');
131
+ return expression.test(text);
132
+ }
133
+
134
+ function resultError(result) {
135
+ const detail = [result && result.stderr, result && result.stdout, result && result.error && result.error.message]
136
+ .map(value => String(value || '').trim())
137
+ .find(Boolean);
138
+ return detail || `exit status ${result && result.status !== undefined ? result.status : 'unknown'}`;
139
+ }
140
+
141
+ function createContext(options = {}) {
142
+ const runner = options.runner || defaultRunner;
143
+ const log = options.log || (message => console.log(message));
144
+ const warn = options.warn || (message => console.warn(message));
145
+ const context = {
146
+ ...DEFAULTS,
147
+ ...options,
148
+ runner,
149
+ log,
150
+ warn,
151
+ dryRun: Boolean(options.dryRun),
152
+ commandAvailable: options.commandAvailable || (command => {
153
+ const result = runner(command, ['--version'], { probe: true });
154
+ return result && result.status === 0 && !result.error;
155
+ })
156
+ };
157
+ context.repositorySource = options.repositorySource || (
158
+ /^https?:\/\//.test(context.repository) || /^git@/.test(context.repository)
159
+ ? context.repository
160
+ : `https://github.com/${context.repository}.git`
161
+ );
162
+ return context;
163
+ }
164
+
165
+ function invoke(context, command, args, { mutate = false, label = command } = {}) {
166
+ const rendered = commandLine(command, args);
167
+ if (mutate && context.dryRun) {
168
+ context.log(`[dry-run] ${rendered}`);
169
+ return { status: 0, stdout: '', stderr: '', dryRun: true };
170
+ }
171
+ const result = context.runner(command, args, { label });
172
+ if (!result || result.status !== 0) {
173
+ context.warn(`[failed] ${rendered}\n ${resultError(result)}`);
174
+ }
175
+ return result || { status: 1, stdout: '', stderr: 'runner returned no result' };
176
+ }
177
+
178
+ function query(context, command, args) {
179
+ return context.runner(command, args, { query: true }) || { status: 1, stdout: '', stderr: 'runner returned no result' };
180
+ }
181
+
182
+ function listState(context, { command, jsonArgs, plainArgs, detectJson, detectText }) {
183
+ const jsonResult = query(context, command, jsonArgs);
184
+ if (jsonResult.status === 0) {
185
+ const payload = parseJsonOutput(jsonResult.stdout);
186
+ if (payload !== null) return { known: true, value: detectJson(payload), result: jsonResult };
187
+ }
188
+
189
+ const plainResult = query(context, command, plainArgs);
190
+ if (plainResult.status === 0 && String(plainResult.stdout || '').trim()) {
191
+ return { known: true, value: detectText(plainResult.stdout), result: plainResult };
192
+ }
193
+
194
+ return {
195
+ known: false,
196
+ value: false,
197
+ error: `${resultError(jsonResult)}; fallback: ${resultError(plainResult)}`
198
+ };
199
+ }
200
+
201
+ function queryClaudeMarketplace(context) {
202
+ return listState(context, {
203
+ command: 'claude',
204
+ jsonArgs: ['plugin', 'marketplace', 'list', '--json'],
205
+ plainArgs: ['plugin', 'marketplace', 'list'],
206
+ detectJson: payload => findMarketplace(payload, context.marketplace),
207
+ detectText: output => textHasMarketplace(output, context.marketplace)
208
+ });
209
+ }
210
+
211
+ function queryClaudePlugin(context) {
212
+ return listState(context, {
213
+ command: 'claude',
214
+ jsonArgs: ['plugin', 'list', '--json'],
215
+ plainArgs: ['plugin', 'list'],
216
+ detectJson: payload => findPlugin(payload, context.plugin, context.marketplace),
217
+ detectText: output => textHasPlugin(output, context.plugin, context.marketplace)
218
+ });
219
+ }
220
+
221
+ function queryCodexMarketplace(context) {
222
+ return listState(context, {
223
+ command: 'codex',
224
+ jsonArgs: ['plugin', 'marketplace', 'list', '--json'],
225
+ plainArgs: ['plugin', 'marketplace', 'list'],
226
+ detectJson: payload => findMarketplace(payload, context.marketplace),
227
+ detectText: output => textHasMarketplace(output, context.marketplace)
228
+ });
229
+ }
230
+
231
+ function queryCodexPlugin(context) {
232
+ const state = listState(context, {
233
+ command: 'codex',
234
+ jsonArgs: ['plugin', 'list', '--marketplace', context.marketplace, '--json'],
235
+ plainArgs: ['plugin', 'list', '--marketplace', context.marketplace],
236
+ detectJson: payload => findPlugin(payload, context.plugin, context.marketplace),
237
+ detectText: output => textHasPlugin(output, context.plugin, context.marketplace)
238
+ });
239
+ if (state.known) return state;
240
+
241
+ // Older Codex builds may not accept --marketplace on plugin list. A full
242
+ // list still lets us fail closed while supporting those builds when JSON is
243
+ // available.
244
+ return listState(context, {
245
+ command: 'codex',
246
+ jsonArgs: ['plugin', 'list', '--json'],
247
+ plainArgs: ['plugin', 'list'],
248
+ detectJson: payload => findPlugin(payload, context.plugin, context.marketplace),
249
+ detectText: output => textHasPlugin(output, context.plugin, context.marketplace)
250
+ });
251
+ }
252
+
253
+ function ensureClaudeMarketplace(context) {
254
+ const state = queryClaudeMarketplace(context);
255
+ if (!state.known) context.warn(`[notice] Claude marketplace state is unavailable; ensuring ${context.marketplace} is configured.`);
256
+ if (!state.known || !state.value) {
257
+ const added = invoke(context, 'claude', [
258
+ 'plugin', 'marketplace', 'add', context.repository,
259
+ '--scope', 'user'
260
+ ], { mutate: true, label: 'claude marketplace add' });
261
+ if (added.status !== 0) return { ok: false, error: resultError(added) };
262
+ }
263
+ return { ok: true };
264
+ }
265
+
266
+ function ensureCodexMarketplace(context) {
267
+ const state = queryCodexMarketplace(context);
268
+ if (!state.known) context.warn(`[notice] Codex marketplace state is unavailable; ensuring ${context.marketplace} is configured.`);
269
+ if (!state.known || !state.value) {
270
+ const added = invoke(context, 'codex', [
271
+ 'plugin', 'marketplace', 'add', context.repositorySource,
272
+ '--ref', context.ref,
273
+ '--json'
274
+ ], { mutate: true, label: 'codex marketplace add' });
275
+ if (added.status !== 0) return { ok: false, error: resultError(added) };
276
+ }
277
+ return { ok: true };
278
+ }
279
+
280
+ function refreshClaudeMarketplace(context) {
281
+ const updated = invoke(context, 'claude', [
282
+ 'plugin', 'marketplace', 'update', context.marketplace
283
+ ], { mutate: true, label: 'claude marketplace update' });
284
+ return updated.status === 0
285
+ ? { ok: true }
286
+ : { ok: false, error: resultError(updated) };
287
+ }
288
+
289
+ function refreshCodexMarketplace(context) {
290
+ const upgraded = invoke(context, 'codex', [
291
+ 'plugin', 'marketplace', 'upgrade', context.marketplace,
292
+ '--json'
293
+ ], { mutate: true, label: 'codex marketplace upgrade' });
294
+ return upgraded.status === 0
295
+ ? { ok: true }
296
+ : { ok: false, error: resultError(upgraded) };
297
+ }
298
+
299
+ function verifyPlugin(context, host, queryPlugin) {
300
+ if (context.dryRun) return { ok: true, skipped: true };
301
+ const state = queryPlugin(context);
302
+ if (!state.known) return { ok: false, error: `${host} plugin state could not be verified: ${state.error}` };
303
+ if (!state.value) return { ok: false, error: `${host} reports ${context.plugin}@${context.marketplace} is not installed after synchronization` };
304
+ return { ok: true };
305
+ }
306
+
307
+ function syncClaude(context) {
308
+ if (!context.commandAvailable('claude')) return { host: 'claude', status: 'skipped', reason: 'claude CLI not found' };
309
+ const marketplace = ensureClaudeMarketplace(context);
310
+ if (!marketplace.ok) return { host: 'claude', status: 'failed', error: marketplace.error };
311
+ const installed = queryClaudePlugin(context);
312
+ if (!installed.known) return { host: 'claude', status: 'failed', error: `Claude plugin state could not be determined: ${installed.error}` };
313
+ const refreshed = refreshClaudeMarketplace(context);
314
+ if (!refreshed.ok) return { host: 'claude', status: 'failed', error: refreshed.error };
315
+
316
+ const action = installed.value ? 'update' : 'install';
317
+ const result = invoke(context, 'claude', [
318
+ 'plugin', action, `${context.plugin}@${context.marketplace}`,
319
+ '--scope', 'user', '--yes', '--json'
320
+ ], { mutate: true, label: `claude plugin ${action}` });
321
+ if (result.status !== 0) return { host: 'claude', status: 'failed', error: resultError(result) };
322
+
323
+ const verification = verifyPlugin(context, 'Claude', queryClaudePlugin);
324
+ if (!verification.ok) return { host: 'claude', status: 'failed', error: verification.error };
325
+ return { host: 'claude', status: 'synchronized', action };
326
+ }
327
+
328
+ function syncCodex(context) {
329
+ if (!context.commandAvailable('codex')) return { host: 'codex', status: 'skipped', reason: 'codex CLI not found' };
330
+ const marketplace = ensureCodexMarketplace(context);
331
+ if (!marketplace.ok) return { host: 'codex', status: 'failed', error: marketplace.error };
332
+ const installed = queryCodexPlugin(context);
333
+ if (!installed.known) return { host: 'codex', status: 'failed', error: `Codex plugin state could not be determined: ${installed.error}` };
334
+
335
+ let action;
336
+ let result;
337
+ if (installed.value) {
338
+ // Codex currently refreshes an installed marketplace plugin through the
339
+ // marketplace upgrade command. Do not fall back to `plugin add` here:
340
+ // an installed plugin must take the update path.
341
+ action = 'update';
342
+ result = refreshCodexMarketplace(context);
343
+ if (result.ok) result = { status: 0, stdout: '', stderr: '' };
344
+ else result = { status: 1, stdout: '', stderr: result.error };
345
+ } else {
346
+ action = 'install';
347
+ result = refreshCodexMarketplace(context);
348
+ if (result.ok) {
349
+ result = invoke(context, 'codex', [
350
+ 'plugin', 'add', `${context.plugin}@${context.marketplace}`, '--json'
351
+ ], { mutate: true, label: 'codex plugin install' });
352
+ } else {
353
+ result = { status: 1, stdout: '', stderr: result.error };
354
+ }
355
+ }
356
+ if (result.status !== 0) return { host: 'codex', status: 'failed', error: resultError(result) };
357
+
358
+ const verification = verifyPlugin(context, 'Codex', queryCodexPlugin);
359
+ if (!verification.ok) return { host: 'codex', status: 'failed', error: verification.error };
360
+ return { host: 'codex', status: 'synchronized', action };
361
+ }
362
+
363
+ function parseArgs(argv) {
364
+ const options = { host: 'all', dryRun: false, json: false };
365
+ for (let index = 0; index < argv.length; index++) {
366
+ const arg = argv[index];
367
+ if (arg === '--help' || arg === '-h') options.help = true;
368
+ else if (arg === '--dry-run') options.dryRun = true;
369
+ else if (arg === '--json') options.json = true;
370
+ else if (arg === '--host') {
371
+ options.host = argv[++index];
372
+ if (!options.host) throw new Error('--host requires all, claude, or codex');
373
+ } else if (arg === '--repository') {
374
+ options.repository = argv[++index];
375
+ if (!options.repository) throw new Error('--repository requires a repository or URL');
376
+ } else if (arg === '--ref') {
377
+ options.ref = argv[++index];
378
+ if (!options.ref) throw new Error('--ref requires a git ref');
379
+ } else {
380
+ throw new Error(`Unknown option: ${arg}`);
381
+ }
382
+ }
383
+ options.host = String(options.host).toLowerCase();
384
+ if (!['all', 'claude', 'codex'].includes(options.host)) throw new Error(`Unsupported host: ${options.host}`);
385
+ return options;
386
+ }
387
+
388
+ function help() {
389
+ return `Usage: node scripts/plugin-sync.js [options]
390
+
391
+ Synchronize the native Harness Everything plugin on installed host CLIs.
392
+ Existing installations take the explicit update/upgrade path; absent
393
+ installations take the install path.
394
+
395
+ Options:
396
+ --host <all|claude|codex> Limit synchronization to one host (default: all)
397
+ --repository <repo|url> Marketplace source (default: dyphn1/Harness-everything)
398
+ --ref <git-ref> Codex marketplace ref (default: main)
399
+ --dry-run Detect state and print mutating commands
400
+ --json Print a machine-readable summary
401
+ -h, --help Show this help
402
+ `;
403
+ }
404
+
405
+ function sync(options, dependencies = {}) {
406
+ const context = createContext({ ...options, ...dependencies });
407
+ const hosts = context.host === 'all' ? ['claude', 'codex'] : [context.host];
408
+ const results = hosts.map(host => host === 'claude' ? syncClaude(context) : syncCodex(context));
409
+ const summary = {
410
+ repository: context.repository,
411
+ marketplace: context.marketplace,
412
+ plugin: context.plugin,
413
+ dryRun: context.dryRun,
414
+ results,
415
+ synchronized: results.filter(result => result.status === 'synchronized').length,
416
+ skipped: results.filter(result => result.status === 'skipped').length,
417
+ failed: results.filter(result => result.status === 'failed').length
418
+ };
419
+ return summary;
420
+ }
421
+
422
+ function main(argv = process.argv.slice(2)) {
423
+ let options;
424
+ try {
425
+ options = parseArgs(argv);
426
+ } catch (error) {
427
+ console.error(`[Error] ${error.message}`);
428
+ console.error(help());
429
+ return 2;
430
+ }
431
+ if (options.help) {
432
+ console.log(help());
433
+ return 0;
434
+ }
435
+
436
+ const summary = sync(options);
437
+ if (options.json) console.log(JSON.stringify(summary, null, 2));
438
+ else {
439
+ for (const result of summary.results) {
440
+ if (result.status === 'synchronized') console.log(`[ok] ${result.host}: ${result.action}`);
441
+ else if (result.status === 'skipped') console.log(`[skip] ${result.host}: ${result.reason}`);
442
+ else console.error(`[fail] ${result.host}: ${result.error}`);
443
+ }
444
+ if (summary.synchronized === 0 && summary.failed === 0) {
445
+ console.error('No supported host CLI was found. Install Claude Code and/or Codex, then retry.');
446
+ }
447
+ }
448
+ return summary.failed > 0 || summary.synchronized === 0 ? 1 : 0;
449
+ }
450
+
451
+ if (require.main === module) process.exitCode = main();
452
+
453
+ module.exports = {
454
+ DEFAULTS,
455
+ commandLine,
456
+ createContext,
457
+ findMarketplace,
458
+ findPlugin,
459
+ main,
460
+ parseArgs,
461
+ parseJsonOutput,
462
+ sync,
463
+ textHasMarketplace,
464
+ textHasPlugin
465
+ };
@@ -0,0 +1,5 @@
1
+ $ErrorActionPreference = 'Stop'
2
+
3
+ $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
4
+ & node (Join-Path $scriptDir 'plugin-sync.js') @args
5
+ exit $LASTEXITCODE
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env sh
2
+ set -eu
3
+
4
+ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5
+ exec node "$SCRIPT_DIR/plugin-sync.js" "$@"