iterate-harness 1.6.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 OpenHarness Contributors, iterate-harness Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # iterate-harness (npm distribution wrapper)
2
+
3
+ npm one-liner install for [iterate-harness](https://github.com/jingzhao-l/iterate-harness) —
4
+ the dedicated agent harness for the iterate review/fix loop (`ih` CLI, React TUI,
5
+ six `iterate_*` tools, engine-enforced convergence).
6
+
7
+ ```bash
8
+ npm install -g iterate-harness
9
+ ih --version # first run bootstraps the Python side, then prints the version
10
+ ```
11
+
12
+ ## What this package is (and is not)
13
+
14
+ The harness itself is a **Python package**. This npm package is a thin
15
+ distribution wrapper: it does not re-implement anything, it just makes `ih`
16
+ installable with npm and keeps it up to date.
17
+
18
+ On first run the wrapper:
19
+
20
+ 1. Finds a Python interpreter >= 3.10 (`py -3` / `python` / `python3`,
21
+ overridable via `ITERATE_HARNESS_PYTHON`).
22
+ 2. Creates a managed virtualenv at `~/.iterate-harness-npm/venv`
23
+ (overridable via `ITERATE_HARNESS_NPM_HOME`).
24
+ 3. `pip install`s the harness **release tarball pinned to this npm package's
25
+ version** — npm `1.6.0` always installs harness `v1.6.0`. Upgrading the npm
26
+ package automatically re-installs the matching harness version on the next
27
+ run.
28
+ 4. Delegates to the venv's real `ih` executable with argv, stdio, signals and
29
+ exit codes forwarded.
30
+
31
+ The React TUI's frontend dependencies (`node_modules`) are installed
32
+ automatically by the harness itself on first TUI launch — npm users always
33
+ have Node, so the TUI always works.
34
+
35
+ ## Requirements
36
+
37
+ - Node.js >= 16 (you have it — you installed this via npm)
38
+ - Python >= 3.10 (`ITERATE_HARNESS_PYTHON=/path/to/python3.12` to override)
39
+ - Network access to registry.npmjs.org (done), pypi.org and github.com (for
40
+ the one-time pip install)
41
+
42
+ ## Environment variables
43
+
44
+ | Variable | Effect |
45
+ | --- | --- |
46
+ | `ITERATE_HARNESS_PYTHON` | Use this interpreter instead of auto-detection |
47
+ | `ITERATE_HARNESS_NPM_HOME` | Runtime dir (default `~/.iterate-harness-npm`) |
48
+ | `ITERATE_HARNESS_INSTALL_URL` | Install from this pip URL instead of the pinned release tarball (e.g. a git branch for testing) |
49
+ | `ITERATE_HARNESS_SKIP_INSTALL=1` | Skip bootstrap entirely; just run the already-installed `ih` |
50
+
51
+ ## npx (no global install)
52
+
53
+ ```bash
54
+ npx -y iterate-harness iterate review --changed
55
+ ```
56
+
57
+ ## Uninstall
58
+
59
+ ```bash
60
+ npm uninstall -g iterate-harness
61
+ rm -rf ~/.iterate-harness-npm # the managed venv
62
+ ```
63
+
64
+ ## Versioning
65
+
66
+ This package's version is kept in lockstep with the harness: wrapper `x.y.z`
67
+ installs harness tag `vx.y.z`. The wrapper is maintained in the
68
+ [iterate-skill monorepo](https://github.com/jingzhao-l/iterate-skill) at
69
+ `npm/iterate-harness/` and published to npm from there.
70
+
71
+ ## License
72
+
73
+ MIT — same as the harness. The agent kernel originates from
74
+ [OpenHarness](https://github.com/HKUDS/OpenHarness); the iterate semantic layer
75
+ originates from [iterate-skill](https://github.com/jingzhao-l/iterate-skill).
package/bin/ih.js ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ /* `ih` — global entry point of the iterate-harness npm wrapper. */
3
+
4
+ "use strict";
5
+
6
+ const { BootstrapError, runHarness } = require("../lib/bootstrap");
7
+
8
+ try {
9
+ runHarness(process.argv.slice(2));
10
+ } catch (error) {
11
+ const message = error instanceof BootstrapError ? error.message : (error && error.stack) || error;
12
+ process.stderr.write(`[iterate-harness] ${message}\n`);
13
+ process.exit(1);
14
+ }
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ /* `iterate-harness` — long-form entry point of the npm wrapper (same as `ih`). */
3
+
4
+ "use strict";
5
+
6
+ const { BootstrapError, runHarness } = require("../lib/bootstrap");
7
+
8
+ try {
9
+ runHarness(process.argv.slice(2));
10
+ } catch (error) {
11
+ const message = error instanceof BootstrapError ? error.message : (error && error.stack) || error;
12
+ process.stderr.write(`[iterate-harness] ${message}\n`);
13
+ process.exit(1);
14
+ }
@@ -0,0 +1,330 @@
1
+ /*
2
+ * Node-side bootstrap for the iterate-harness npm distribution wrapper.
3
+ *
4
+ * The harness itself is a Python package (the `ih` CLI). This module never
5
+ * re-implements harness logic — it only prepares a managed virtualenv and
6
+ * delegates every invocation to the real `ih` executable inside it:
7
+ *
8
+ * 1. Resolve a Python interpreter >= 3.10 (env override first).
9
+ * 2. Create/reuse the venv at ~/.iterate-harness-npm/venv.
10
+ * 3. pip-install the harness release tarball pinned to THIS npm package's
11
+ * version (lockstep: npm 1.6.0 installs harness v1.6.0).
12
+ * 4. Stamp the installed version; a mismatch (npm upgrade) triggers a
13
+ * re-install on the next run.
14
+ * 5. Spawn the venv's `ih` with argv/stdio/signals forwarded and the exit
15
+ * code passed through.
16
+ *
17
+ * Failures print an actionable message and exit 1 — never a raw stack trace.
18
+ */
19
+
20
+ "use strict";
21
+
22
+ const { spawn, spawnSync } = require("child_process");
23
+ const fs = require("fs");
24
+ const os = require("os");
25
+ const path = require("path");
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Constants (no magic strings below — everything is named here)
29
+ // ---------------------------------------------------------------------------
30
+
31
+ const RUNTIME_DIR_NAME = ".iterate-harness-npm";
32
+ const VENV_DIR_NAME = "venv";
33
+ const STAMP_FILE_NAME = "version.stamp";
34
+
35
+ const PYTHON_ENV_VAR = "ITERATE_HARNESS_PYTHON";
36
+ const HOME_ENV_VAR = "ITERATE_HARNESS_NPM_HOME";
37
+ const INSTALL_URL_ENV_VAR = "ITERATE_HARNESS_INSTALL_URL";
38
+ const SKIP_INSTALL_ENV_VAR = "ITERATE_HARNESS_SKIP_INSTALL";
39
+
40
+ const HARNESS_REPO_ARCHIVE_URL = "https://github.com/jingzhao-l/iterate-harness/archive/refs/tags";
41
+
42
+ const MIN_PYTHON_MAJOR = 3;
43
+ const MIN_PYTHON_MINOR = 10;
44
+
45
+ const PYTHON_VERSION_PATTERN = /Python\s+(\d+)\.(\d+)\.(\d+)/;
46
+ const IS_WINDOWS = process.platform === "win32";
47
+
48
+ class BootstrapError extends Error {}
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Pure helpers (unit-tested in test/bootstrap.test.js)
52
+ // ---------------------------------------------------------------------------
53
+
54
+ function releaseTarballUrl(version) {
55
+ return `${HARNESS_REPO_ARCHIVE_URL}/v${version}.tar.gz`;
56
+ }
57
+
58
+ function parsePythonVersion(output) {
59
+ const match = PYTHON_VERSION_PATTERN.exec(String(output));
60
+ if (!match) {
61
+ return null;
62
+ }
63
+ return {
64
+ major: Number.parseInt(match[1], 10),
65
+ minor: Number.parseInt(match[2], 10),
66
+ patch: Number.parseInt(match[3], 10),
67
+ };
68
+ }
69
+
70
+ function isSupportedPython(version) {
71
+ if (!version) {
72
+ return false;
73
+ }
74
+ return (
75
+ version.major > MIN_PYTHON_MAJOR ||
76
+ (version.major === MIN_PYTHON_MAJOR && version.minor >= MIN_PYTHON_MINOR)
77
+ );
78
+ }
79
+
80
+ function pythonCandidates(platform, env) {
81
+ const override = env ? env[PYTHON_ENV_VAR] : undefined;
82
+ const candidates = [];
83
+ if (override) {
84
+ candidates.push({ command: override, preArgs: [] });
85
+ }
86
+ if (platform === "win32") {
87
+ candidates.push({ command: "py", preArgs: ["-3"] });
88
+ candidates.push({ command: "python", preArgs: [] });
89
+ } else {
90
+ candidates.push({ command: "python3", preArgs: [] });
91
+ candidates.push({ command: "python", preArgs: [] });
92
+ }
93
+ return candidates;
94
+ }
95
+
96
+ function venvExecutablePaths(venvDir, platform) {
97
+ if (platform === "win32") {
98
+ return {
99
+ python: path.join(venvDir, "Scripts", "python.exe"),
100
+ ih: path.join(venvDir, "Scripts", "ih.exe"),
101
+ };
102
+ }
103
+ return {
104
+ python: path.join(venvDir, "bin", "python"),
105
+ ih: path.join(venvDir, "bin", "ih"),
106
+ };
107
+ }
108
+
109
+ function needsBootstrap(stampContent, expectedVersion) {
110
+ return String(stampContent || "").trim() !== String(expectedVersion || "").trim();
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Runtime resolution
115
+ // ---------------------------------------------------------------------------
116
+
117
+ function packageVersion() {
118
+ const manifestPath = path.resolve(__dirname, "..", "package.json");
119
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
120
+ return manifest.version;
121
+ }
122
+
123
+ function runtimeHomeDir(env) {
124
+ const override = env ? env[HOME_ENV_VAR] : undefined;
125
+ if (override) {
126
+ return override;
127
+ }
128
+ return path.join(os.homedir(), RUNTIME_DIR_NAME);
129
+ }
130
+
131
+ function detectPython(env) {
132
+ const errors = [];
133
+ for (const candidate of pythonCandidates(process.platform, env || process.env)) {
134
+ const probe = spawnSync(candidate.command, [...candidate.preArgs, "--version"], {
135
+ encoding: "utf8",
136
+ windowsHide: true,
137
+ });
138
+ if (probe.error) {
139
+ errors.push(`${candidate.command}: not found`);
140
+ continue;
141
+ }
142
+ const stdout = `${probe.stdout || ""}${probe.stderr || ""}`;
143
+ const version = parsePythonVersion(stdout);
144
+ if (!version) {
145
+ errors.push(`${candidate.command}: unreadable version output`);
146
+ continue;
147
+ }
148
+ if (!isSupportedPython(version)) {
149
+ errors.push(
150
+ `${candidate.command}: ${version.major}.${version.minor}.${version.patch} is older than 3.10`
151
+ );
152
+ continue;
153
+ }
154
+ return { ...candidate, version };
155
+ }
156
+ throw new BootstrapError(
157
+ [
158
+ "Python >= 3.10 not found (tried: " + errors.join("; ") + ").",
159
+ "",
160
+ "Install Python 3.10+ or point the wrapper at an existing interpreter:",
161
+ ` ${PYTHON_ENV_VAR}=/path/to/python3.12 npx iterate-harness --version`,
162
+ ].join("\n")
163
+ );
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Bootstrap (venv + pip install of the pinned release tarball)
168
+ // ---------------------------------------------------------------------------
169
+
170
+ const CERT_VERIFY_FAILURE_MARKER = "CERTIFICATE_VERIFY_FAILED";
171
+
172
+ function runStep(command, args, options) {
173
+ const result = spawnSync(command, args, {
174
+ stdio: ["ignore", "pipe", "pipe"],
175
+ windowsHide: true,
176
+ maxBuffer: 16 * 1024 * 1024,
177
+ ...options,
178
+ });
179
+ const capturedOutput = `${result.stdout || ""}${result.stderr || ""}`;
180
+ if (capturedOutput) {
181
+ process.stderr.write(capturedOutput);
182
+ }
183
+ if (result.error) {
184
+ throw new BootstrapError(`failed to run ${command}: ${result.error.message}`);
185
+ }
186
+ if (result.status !== 0) {
187
+ if (capturedOutput.includes(CERT_VERIFY_FAILURE_MARKER)) {
188
+ throw new BootstrapError(
189
+ [
190
+ `${command} ${args.join(" ")} exited with code ${result.status}`,
191
+ "",
192
+ "The download failed TLS certificate verification. This is a local",
193
+ "Python/pip trust-store issue, not a harness problem. Common fixes:",
194
+ " - macOS python.org installs: run",
195
+ " /Applications/Python 3.1x/Install Certificates.command",
196
+ " - Point pip at a CA bundle: export PIP_CERT=$(python3 -m certifi)",
197
+ ` - Or use another interpreter: ${PYTHON_ENV_VAR}=/opt/homebrew/bin/python3.12 npm i -g iterate-harness`,
198
+ ].join("\n")
199
+ );
200
+ }
201
+ throw new BootstrapError(`${command} ${args.join(" ")} exited with code ${result.status}`);
202
+ }
203
+ }
204
+
205
+ function installUrl(version, env) {
206
+ const override = env ? env[INSTALL_URL_ENV_VAR] : undefined;
207
+ if (override) {
208
+ return override;
209
+ }
210
+ return releaseTarballUrl(version);
211
+ }
212
+
213
+ function bootstrap(homeDir, version, env) {
214
+ fs.mkdirSync(homeDir, { recursive: true });
215
+
216
+ const venvDir = path.join(homeDir, VENV_DIR_NAME);
217
+ const executable = venvExecutablePaths(venvDir, process.platform);
218
+ const activateMarker = IS_WINDOWS
219
+ ? path.join(venvDir, "Scripts", "activate")
220
+ : path.join(venvDir, "bin", "activate");
221
+
222
+ const interpreter = detectPython(env);
223
+ const recreateVenv = fs.existsSync(venvDir) && !fs.existsSync(activateMarker);
224
+ if (recreateVenv) {
225
+ fs.rmSync(venvDir, { recursive: true, force: true });
226
+ }
227
+ if (!fs.existsSync(activateMarker)) {
228
+ process.stderr.write(`[iterate-harness] creating virtualenv at ${venvDir} ...\n`);
229
+ runStep(interpreter.command, [...interpreter.preArgs, "-m", "venv", venvDir]);
230
+ }
231
+
232
+ const url = installUrl(version, env);
233
+ process.stderr.write(`[iterate-harness] pip installing iterate-harness ${version} ...\n`);
234
+ runStep(executable.python, [
235
+ "-m",
236
+ "pip",
237
+ "install",
238
+ "--upgrade",
239
+ "--force-reinstall",
240
+ url,
241
+ ]);
242
+
243
+ const stampPath = path.join(homeDir, STAMP_FILE_NAME);
244
+ fs.writeFileSync(stampPath, `${version}\n`, "utf8");
245
+ }
246
+
247
+ function ensureRuntime(env) {
248
+ const environment = env || process.env;
249
+ const version = packageVersion();
250
+ const homeDir = runtimeHomeDir(environment);
251
+ const venvDir = path.join(homeDir, VENV_DIR_NAME);
252
+ const executable = venvExecutablePaths(venvDir, process.platform);
253
+ const stampPath = path.join(homeDir, STAMP_FILE_NAME);
254
+
255
+ const skipInstall = environment[SKIP_INSTALL_ENV_VAR] === "1";
256
+ const stampContent = fs.existsSync(stampPath)
257
+ ? fs.readFileSync(stampPath, "utf8")
258
+ : "";
259
+
260
+ if (!skipInstall && needsBootstrap(stampContent, version)) {
261
+ bootstrap(homeDir, version, environment);
262
+ }
263
+
264
+ if (!fs.existsSync(executable.ih)) {
265
+ throw new BootstrapError(
266
+ [
267
+ `ih is not installed yet at ${executable.ih}.`,
268
+ skipInstall
269
+ ? `Bootstrap was skipped (${SKIP_INSTALL_ENV_VAR}=1) — unset it and retry.`
270
+ : "The installation may have failed; remove the runtime dir and retry:",
271
+ ` rm -rf ${homeDir}`,
272
+ ].join("\n")
273
+ );
274
+ }
275
+ return executable.ih;
276
+ }
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // Delegation to the real ih executable
280
+ // ---------------------------------------------------------------------------
281
+
282
+ function runHarness(args, env) {
283
+ const target = ensureRuntime(env);
284
+ const child = spawn(target, args, {
285
+ stdio: "inherit",
286
+ windowsHide: false,
287
+ env: env || process.env,
288
+ });
289
+
290
+ const forwardSignal = (signal) => {
291
+ if (!child.killed) {
292
+ child.kill(signal);
293
+ }
294
+ };
295
+ process.on("SIGINT", () => forwardSignal("SIGINT"));
296
+ process.on("SIGTERM", () => forwardSignal("SIGTERM"));
297
+
298
+ child.on("error", (error) => {
299
+ process.stderr.write(`[iterate-harness] failed to launch ih: ${error.message}\n`);
300
+ process.exit(1);
301
+ });
302
+ child.on("close", (code, signal) => {
303
+ if (signal) {
304
+ process.kill(process.pid, signal);
305
+ return;
306
+ }
307
+ process.exit(code === null ? 1 : code);
308
+ });
309
+ }
310
+
311
+ module.exports = {
312
+ BootstrapError,
313
+ MIN_PYTHON_MAJOR,
314
+ MIN_PYTHON_MINOR,
315
+ PYTHON_ENV_VAR,
316
+ HOME_ENV_VAR,
317
+ INSTALL_URL_ENV_VAR,
318
+ SKIP_INSTALL_ENV_VAR,
319
+ detectPython,
320
+ ensureRuntime,
321
+ installUrl,
322
+ isSupportedPython,
323
+ needsBootstrap,
324
+ packageVersion,
325
+ parsePythonVersion,
326
+ pythonCandidates,
327
+ releaseTarballUrl,
328
+ runHarness,
329
+ venvExecutablePaths,
330
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "iterate-harness",
3
+ "version": "1.6.0",
4
+ "description": "npm distribution wrapper for iterate-harness — installs the Python harness (ih CLI) into a managed venv on first run and exposes it as a global command",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/jingzhao-l/iterate-harness.git"
10
+ },
11
+ "homepage": "https://github.com/jingzhao-l/iterate-harness#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/jingzhao-l/iterate-harness/issues"
14
+ },
15
+ "bin": {
16
+ "ih": "bin/ih.js",
17
+ "iterate-harness": "bin/iterate-harness.js"
18
+ },
19
+ "engines": {
20
+ "node": ">=16"
21
+ },
22
+ "files": [
23
+ "bin",
24
+ "lib",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "keywords": [
29
+ "iterate",
30
+ "code-review",
31
+ "agent-harness",
32
+ "cli",
33
+ "installer"
34
+ ],
35
+ "scripts": {
36
+ "test": "node --test test/bootstrap.test.js"
37
+ }
38
+ }