@mikenguyen69/harness 0.1.0-beta.1
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 +21 -0
- package/README.md +43 -0
- package/dist/build-identity.json +1 -0
- package/dist/cancellation.js +138 -0
- package/dist/cli.js +47 -0
- package/dist/dispatch.js +70 -0
- package/dist/help.js +47 -0
- package/dist/internal/agent/adapter/conformance.js +53 -0
- package/dist/internal/agent/adapter/hook.js +113 -0
- package/dist/internal/agent/adapter/install.js +30 -0
- package/dist/internal/agent/adapter/scope.js +55 -0
- package/dist/internal/agent/adapter/session.js +63 -0
- package/dist/internal/agent/adapter/settings.js +51 -0
- package/dist/internal/agent/cli.js +79 -0
- package/dist/internal/agent/evals/index.js +111 -0
- package/dist/internal/agent/index.js +15 -0
- package/dist/internal/agent/ledger-read.js +27 -0
- package/dist/internal/agent/routing/cli.js +104 -0
- package/dist/internal/agent/routing/index.js +123 -0
- package/dist/internal/agent/telemetry/index.js +97 -0
- package/dist/internal/orchestration/activity/index.js +111 -0
- package/dist/internal/orchestration/analyze/index.js +237 -0
- package/dist/internal/orchestration/cli.js +475 -0
- package/dist/internal/orchestration/config/index.js +66 -0
- package/dist/internal/orchestration/doctor/index.js +202 -0
- package/dist/internal/orchestration/forge/index.js +121 -0
- package/dist/internal/orchestration/git/index.js +181 -0
- package/dist/internal/orchestration/harness/cli.js +378 -0
- package/dist/internal/orchestration/harness/index.js +186 -0
- package/dist/internal/orchestration/harness/shim.js +182 -0
- package/dist/internal/orchestration/index.js +29 -0
- package/dist/internal/orchestration/loop/index.js +1107 -0
- package/dist/internal/orchestration/prompts/index.js +199 -0
- package/dist/internal/orchestration/report/index.js +125 -0
- package/dist/internal/orchestration/routes/index.js +85 -0
- package/dist/internal/orchestration/run/index.js +221 -0
- package/dist/internal/orchestration/runner/index.js +90 -0
- package/dist/internal/orchestration/runners/claude/index.js +246 -0
- package/dist/internal/orchestration/runners/claude/sdk.js +198 -0
- package/dist/internal/orchestration/runners/claude/wiring.js +98 -0
- package/dist/internal/orchestration/runners/codex/exec.js +114 -0
- package/dist/internal/orchestration/runners/codex/index.js +72 -0
- package/dist/internal/orchestration/runners/conformance/index.js +169 -0
- package/dist/internal/orchestration/runners/cursor/exec.js +531 -0
- package/dist/internal/orchestration/runners/cursor/index.js +184 -0
- package/dist/internal/orchestration/runners/stub/index.js +168 -0
- package/dist/internal/orchestration/runners/telemetry.js +36 -0
- package/dist/internal/orchestration/runstate/index.js +213 -0
- package/dist/internal/orchestration/runstate/publish.js +115 -0
- package/dist/internal/orchestration/worktree/index.js +89 -0
- package/dist/internal/system/adapters/index.js +19 -0
- package/dist/internal/system/agentops/index.js +16 -0
- package/dist/internal/system/agentops/scope.js +67 -0
- package/dist/internal/system/assets.js +45 -0
- package/dist/internal/system/board/cli.js +56 -0
- package/dist/internal/system/board/index.js +276 -0
- package/dist/internal/system/board/live.js +277 -0
- package/dist/internal/system/calibration/cli.js +45 -0
- package/dist/internal/system/calibration/index.js +141 -0
- package/dist/internal/system/cli.js +174 -0
- package/dist/internal/system/corpus/replay.js +79 -0
- package/dist/internal/system/digest/cli.js +23 -0
- package/dist/internal/system/digest/index.js +145 -0
- package/dist/internal/system/doors/classify.js +94 -0
- package/dist/internal/system/doors/cli.js +184 -0
- package/dist/internal/system/doors/index.js +249 -0
- package/dist/internal/system/doors/metrics.js +47 -0
- package/dist/internal/system/escapes/cli.js +45 -0
- package/dist/internal/system/escapes/index.js +81 -0
- package/dist/internal/system/explore/cli.js +77 -0
- package/dist/internal/system/explore/index.js +91 -0
- package/dist/internal/system/gates/cli.js +43 -0
- package/dist/internal/system/gates/detectors.js +55 -0
- package/dist/internal/system/gates/pre.js +51 -0
- package/dist/internal/system/hub/cli.js +51 -0
- package/dist/internal/system/hub/index.js +146 -0
- package/dist/internal/system/init/index.js +83 -0
- package/dist/internal/system/ledger/cli.js +54 -0
- package/dist/internal/system/ledger/index.js +176 -0
- package/dist/internal/system/manifest/index.js +207 -0
- package/dist/internal/system/provenance/index.js +13 -0
- package/dist/internal/system/report/index.js +15 -0
- package/dist/internal/system/runner/index.js +74 -0
- package/dist/internal/system/runstatus/cli.js +86 -0
- package/dist/internal/system/runstatus/index.js +168 -0
- package/dist/internal/system/sequencer/briefing.js +75 -0
- package/dist/internal/system/sequencer/cli.js +170 -0
- package/dist/internal/system/sequencer/index.js +156 -0
- package/dist/internal/system/spec/cli.js +110 -0
- package/dist/internal/system/spec/index.js +212 -0
- package/dist/internal/system/spec/materialize.js +111 -0
- package/dist/internal/system/spec/openspec-parse.js +83 -0
- package/dist/internal/system/spec/project.js +208 -0
- package/dist/internal/system/substrate/index.js +14 -0
- package/dist/internal/system/system/index.js +159 -0
- package/dist/internal/system/units/index.js +87 -0
- package/dist/internal/system/verify/cli.js +103 -0
- package/dist/internal/system/verify/declined-reasons.js +61 -0
- package/dist/internal/system/verify/index.js +188 -0
- package/dist/internal/system/verify/signing.js +79 -0
- package/dist/paths.js +50 -0
- package/dist/routes.js +93 -0
- package/dist/version.js +30 -0
- package/dist/win32-process-group.js +360 -0
- package/package.json +41 -0
- package/profiles/python/bindings.toml +59 -0
- package/profiles/terraform/bindings.toml +66 -0
- package/profiles/typescript/bindings.toml +64 -0
- package/profiles/typescript/smoke/README.md +8 -0
- package/schema/baseline.schema.json +26 -0
- package/schema/decision-classes.yaml +45 -0
- package/schema/envelope.schema.json +186 -0
- package/schema/ledger-event.schema.json +235 -0
- package/schema/manifest.schema.json +119 -0
- package/schema/routes.schema.json +55 -0
- package/schema/run-status.schema.json +162 -0
- package/schema/session-event.schema.json +74 -0
- package/schema/spec.schema.json +114 -0
- package/schema/system.schema.json +142 -0
- package/templates/ci/verify.yml +23 -0
- package/templates/target-kit/README.md +78 -0
- package/templates/target-kit/component.toml +20 -0
- package/templates/target-kit/orchestration.json +9 -0
- package/templates/target-kit/profiles/target/bindings.toml +6 -0
- package/templates/target-kit/routes.toml +5 -0
- package/templates/target-kit/specs/first-unit.yaml +11 -0
- package/templates/target-kit/system.toml +7 -0
package/dist/paths.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve absolute paths to the three internal product CLIs.
|
|
3
|
+
* Prefer staged dist/internal/<product>/cli.js; else workspace packages.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
export const packageRoot = join(moduleDir, "..");
|
|
10
|
+
function findPackageRoot(packageName, startDir) {
|
|
11
|
+
let dir = startDir;
|
|
12
|
+
while (true) {
|
|
13
|
+
const candidate = join(dir, "node_modules", packageName);
|
|
14
|
+
if (existsSync(join(candidate, "package.json")))
|
|
15
|
+
return candidate;
|
|
16
|
+
const parent = dirname(dir);
|
|
17
|
+
if (parent === dir)
|
|
18
|
+
break;
|
|
19
|
+
dir = parent;
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`package not found: ${packageName}`);
|
|
22
|
+
}
|
|
23
|
+
function resolveWorkspaceCli(packageName) {
|
|
24
|
+
const pkgRoot = findPackageRoot(packageName, packageRoot);
|
|
25
|
+
const cli = join(pkgRoot, "dist", "cli.js");
|
|
26
|
+
if (!existsSync(cli))
|
|
27
|
+
throw new Error(`CLI not built for ${packageName}: ${cli}`);
|
|
28
|
+
return cli;
|
|
29
|
+
}
|
|
30
|
+
function stagedClis() {
|
|
31
|
+
const system = join(packageRoot, "dist", "internal", "system", "cli.js");
|
|
32
|
+
const agent = join(packageRoot, "dist", "internal", "agent", "cli.js");
|
|
33
|
+
const orchestration = join(packageRoot, "dist", "internal", "orchestration", "cli.js");
|
|
34
|
+
if (existsSync(system) && existsSync(agent) && existsSync(orchestration)) {
|
|
35
|
+
return { system, agent, orchestration };
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
export function resolveInternalClis() {
|
|
40
|
+
return (stagedClis() ?? {
|
|
41
|
+
system: resolveWorkspaceCli("@harness/system"),
|
|
42
|
+
agent: resolveWorkspaceCli("@harness/agent"),
|
|
43
|
+
orchestration: resolveWorkspaceCli("@harness/orchestration"),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export function readPackageVersion(packageName) {
|
|
47
|
+
const pkg = JSON.parse(readFileSync(join(findPackageRoot(packageName, packageRoot), "package.json"), "utf8"));
|
|
48
|
+
return pkg.version;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=paths.js.map
|
package/dist/routes.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Facade tail after the leading `harness` token — longest match wins. */
|
|
2
|
+
const ROUTES = [
|
|
3
|
+
{ id: "agent:hook:pre-tool-use", product: "agent", argvPrefix: ["hook", "pre-tool-use"] },
|
|
4
|
+
{ id: "agent:session:started", product: "agent", argvPrefix: ["session", "started"] },
|
|
5
|
+
{ id: "agent:session:heartbeat", product: "agent", argvPrefix: ["session", "heartbeat"] },
|
|
6
|
+
{ id: "agent:session:gate_fired", product: "agent", argvPrefix: ["session", "gate_fired"] },
|
|
7
|
+
{ id: "agent:session:escalated", product: "agent", argvPrefix: ["session", "escalated"] },
|
|
8
|
+
{ id: "agent:session:ended", product: "agent", argvPrefix: ["session", "ended"] },
|
|
9
|
+
{ id: "system:gate:pre", product: "system", argvPrefix: ["gate", "--pre"] },
|
|
10
|
+
{ id: "system:spec:check", product: "system", argvPrefix: ["spec", "check"] },
|
|
11
|
+
{ id: "system:spec:materialize", product: "system", argvPrefix: ["spec", "materialize"] },
|
|
12
|
+
{ id: "system:door:list", product: "system", argvPrefix: ["door", "list"] },
|
|
13
|
+
{ id: "system:door:show", product: "system", argvPrefix: ["door", "show"] },
|
|
14
|
+
{ id: "system:door:approve", product: "system", argvPrefix: ["door", "approve"] },
|
|
15
|
+
{ id: "system:door:reject", product: "system", argvPrefix: ["door", "reject"] },
|
|
16
|
+
{ id: "system:ledger:append", product: "system", argvPrefix: ["ledger", "append"] },
|
|
17
|
+
{ id: "system:ledger:read", product: "system", argvPrefix: ["ledger", "read"] },
|
|
18
|
+
{ id: "system:ledger:verify-chain", product: "system", argvPrefix: ["ledger", "verify-chain"] },
|
|
19
|
+
{ id: "system:board:build", product: "system", argvPrefix: ["board", "build"] },
|
|
20
|
+
{ id: "system:board:serve", product: "system", argvPrefix: ["board", "serve"] },
|
|
21
|
+
{ id: "orchestration:doctor", product: "orchestration", argvPrefix: ["doctor"], injectOrchestrationBins: true },
|
|
22
|
+
{ id: "orchestration:run", product: "orchestration", argvPrefix: ["run"], injectOrchestrationBins: true },
|
|
23
|
+
{ id: "orchestration:resume", product: "orchestration", argvPrefix: ["resume"], injectOrchestrationBins: true },
|
|
24
|
+
{ id: "agent:escalate", product: "agent", argvPrefix: ["escalate"] },
|
|
25
|
+
{ id: "agent:route", product: "agent", argvPrefix: ["route"] },
|
|
26
|
+
{ id: "system:init", product: "system", argvPrefix: ["init"] },
|
|
27
|
+
{ id: "system:verify", product: "system", argvPrefix: ["verify"] },
|
|
28
|
+
{ id: "system:next", product: "system", argvPrefix: ["next"] },
|
|
29
|
+
{ id: "system:claim", product: "system", argvPrefix: ["claim"] },
|
|
30
|
+
{ id: "system:heartbeat", product: "system", argvPrefix: ["heartbeat"] },
|
|
31
|
+
{ id: "system:merge", product: "system", argvPrefix: ["merge"] },
|
|
32
|
+
{ id: "system:attest", product: "system", argvPrefix: ["attest"] },
|
|
33
|
+
{ id: "system:calibrate", product: "system", argvPrefix: ["calibrate"] },
|
|
34
|
+
{ id: "system:digest", product: "system", argvPrefix: ["digest"] },
|
|
35
|
+
].sort((a, b) => b.argvPrefix.length - a.argvPrefix.length);
|
|
36
|
+
/** Report subcommand disambiguation from command-map report_resolution. */
|
|
37
|
+
const REPORT_SUBCOMMANDS = new Map([
|
|
38
|
+
[
|
|
39
|
+
"run",
|
|
40
|
+
{
|
|
41
|
+
id: "orchestration:report",
|
|
42
|
+
product: "orchestration",
|
|
43
|
+
argvPrefix: ["report"],
|
|
44
|
+
injectOrchestrationBins: true,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
"calibration",
|
|
49
|
+
{
|
|
50
|
+
id: "system:report",
|
|
51
|
+
product: "system",
|
|
52
|
+
argvPrefix: ["report"],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
]);
|
|
56
|
+
function matchesPrefix(argv, prefix) {
|
|
57
|
+
if (argv.length < prefix.length)
|
|
58
|
+
return false;
|
|
59
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
60
|
+
if (argv[i] !== prefix[i])
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
export function resolveRoute(argv) {
|
|
66
|
+
if (argv.length === 0)
|
|
67
|
+
return null;
|
|
68
|
+
if (argv[0] === "report") {
|
|
69
|
+
const sub = argv[1];
|
|
70
|
+
if (sub && REPORT_SUBCOMMANDS.has(sub)) {
|
|
71
|
+
return REPORT_SUBCOMMANDS.get(sub);
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (argv[0] === "status") {
|
|
76
|
+
return "status-skeleton";
|
|
77
|
+
}
|
|
78
|
+
for (const route of ROUTES) {
|
|
79
|
+
if (matchesPrefix(argv, route.argvPrefix)) {
|
|
80
|
+
return route;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
export function buildInternalArgv(route, argv) {
|
|
86
|
+
if (argv[0] === "report" && (argv[1] === "run" || argv[1] === "calibration")) {
|
|
87
|
+
return [...route.argvPrefix, ...argv.slice(2)];
|
|
88
|
+
}
|
|
89
|
+
const rest = argv.slice(route.argvPrefix.length);
|
|
90
|
+
return [...route.argvPrefix, ...rest];
|
|
91
|
+
}
|
|
92
|
+
export { ROUTES, REPORT_SUBCOMMANDS };
|
|
93
|
+
//# sourceMappingURL=routes.js.map
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { packageRoot, readPackageVersion } from "./paths.js";
|
|
4
|
+
const facadePkg = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
5
|
+
/** Prefer staged build-identity.json (no node_modules); else workspace packages. */
|
|
6
|
+
function internalVersions() {
|
|
7
|
+
const staged = join(packageRoot, "dist", "build-identity.json");
|
|
8
|
+
if (existsSync(staged)) {
|
|
9
|
+
return JSON.parse(readFileSync(staged, "utf8"));
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
system: readPackageVersion("@harness/system"),
|
|
13
|
+
agent: readPackageVersion("@harness/agent"),
|
|
14
|
+
orchestration: readPackageVersion("@harness/orchestration"),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function formatVersion() {
|
|
18
|
+
const v = internalVersions();
|
|
19
|
+
return [
|
|
20
|
+
`${facadePkg.name} ${facadePkg.version}`,
|
|
21
|
+
`@harness/system ${v.system}`,
|
|
22
|
+
`@harness/agent ${v.agent}`,
|
|
23
|
+
`@harness/orchestration ${v.orchestration}`,
|
|
24
|
+
].join("\n") + "\n";
|
|
25
|
+
}
|
|
26
|
+
export function facadeVersion() {
|
|
27
|
+
return facadePkg.version;
|
|
28
|
+
}
|
|
29
|
+
export { facadePkg };
|
|
30
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows process-group spawn + CTRL_BREAK.
|
|
3
|
+
*
|
|
4
|
+
* Node's `spawn({ detached: true })` sets DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP,
|
|
5
|
+
* which hides a new console so GenerateConsoleCtrlEvent from the parent cannot
|
|
6
|
+
* reach the child. Node has no flag for CREATE_NEW_PROCESS_GROUP alone, so this
|
|
7
|
+
* module compiles a tiny helper (csc.exe, cached) that:
|
|
8
|
+
* - CreateProcessW with CREATE_NEW_PROCESS_GROUP (shared console)
|
|
9
|
+
* - assigns the child to a Win32 job with KILL_ON_JOB_CLOSE
|
|
10
|
+
* - GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, groupPid) on interrupt
|
|
11
|
+
*
|
|
12
|
+
* `process.kill(pid, "SIGBREAK")` is ENOSYS on Node 24 / Windows.
|
|
13
|
+
*/
|
|
14
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
export const WIN32_CTRL_BREAK_EVENT = 1;
|
|
20
|
+
/** Sidecar the `run` helper polls; written by {@link sendCtrlBreak}. */
|
|
21
|
+
export function win32BreakSignalPath(pid) {
|
|
22
|
+
return join(tmpdir(), `harness-win32-break-${pid}`);
|
|
23
|
+
}
|
|
24
|
+
const HELPER_SOURCE = `
|
|
25
|
+
using System;
|
|
26
|
+
using System.IO;
|
|
27
|
+
using System.Runtime.InteropServices;
|
|
28
|
+
using System.Text;
|
|
29
|
+
|
|
30
|
+
internal static class HarnessWin32Group {
|
|
31
|
+
const uint CREATE_NEW_PROCESS_GROUP = 0x00000200;
|
|
32
|
+
const uint CTRL_C_EVENT = 0;
|
|
33
|
+
const uint CTRL_BREAK_EVENT = 1;
|
|
34
|
+
const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000;
|
|
35
|
+
const int JobObjectExtendedLimitInformation = 9;
|
|
36
|
+
const uint WAIT_OBJECT_0 = 0;
|
|
37
|
+
const uint INFINITE = 0xFFFFFFFF;
|
|
38
|
+
|
|
39
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
40
|
+
struct STARTUPINFO {
|
|
41
|
+
public int cb;
|
|
42
|
+
public IntPtr lpReserved;
|
|
43
|
+
public IntPtr lpDesktop;
|
|
44
|
+
public IntPtr lpTitle;
|
|
45
|
+
public int dwX, dwY, dwXSize, dwYSize, dwXCountChars, dwYCountChars, dwFillAttribute, dwFlags;
|
|
46
|
+
public short wShowWindow;
|
|
47
|
+
public short cbReserved2;
|
|
48
|
+
public IntPtr lpReserved2;
|
|
49
|
+
public IntPtr hStdInput, hStdOutput, hStdError;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
53
|
+
struct PROCESS_INFORMATION {
|
|
54
|
+
public IntPtr hProcess;
|
|
55
|
+
public IntPtr hThread;
|
|
56
|
+
public uint dwProcessId;
|
|
57
|
+
public uint dwThreadId;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
61
|
+
struct JOBOBJECT_BASIC_LIMIT_INFORMATION {
|
|
62
|
+
public long PerProcessUserTimeLimit;
|
|
63
|
+
public long PerJobUserTimeLimit;
|
|
64
|
+
public uint LimitFlags;
|
|
65
|
+
public UIntPtr MinimumWorkingSetSize;
|
|
66
|
+
public UIntPtr MaximumWorkingSetSize;
|
|
67
|
+
public uint ActiveProcessLimit;
|
|
68
|
+
public UIntPtr Affinity;
|
|
69
|
+
public uint PriorityClass;
|
|
70
|
+
public uint SchedulingClass;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
74
|
+
struct IO_COUNTERS {
|
|
75
|
+
public ulong ReadOperationCount, WriteOperationCount, OtherOperationCount;
|
|
76
|
+
public ulong ReadTransferCount, WriteTransferCount, OtherTransferCount;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
80
|
+
struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
|
|
81
|
+
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
|
|
82
|
+
public IO_COUNTERS IoInfo;
|
|
83
|
+
public UIntPtr ProcessMemoryLimit, JobMemoryLimit, PeakProcessMemoryUsed, PeakJobMemoryUsed;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
87
|
+
static extern bool CreateProcessW(
|
|
88
|
+
string lpApplicationName,
|
|
89
|
+
StringBuilder lpCommandLine,
|
|
90
|
+
IntPtr lpProcessAttributes,
|
|
91
|
+
IntPtr lpThreadAttributes,
|
|
92
|
+
bool bInheritHandles,
|
|
93
|
+
uint dwCreationFlags,
|
|
94
|
+
IntPtr lpEnvironment,
|
|
95
|
+
string lpCurrentDirectory,
|
|
96
|
+
ref STARTUPINFO lpStartupInfo,
|
|
97
|
+
out PROCESS_INFORMATION lpProcessInformation);
|
|
98
|
+
|
|
99
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
100
|
+
static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId);
|
|
101
|
+
|
|
102
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
103
|
+
static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string lpName);
|
|
104
|
+
|
|
105
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
106
|
+
static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess);
|
|
107
|
+
|
|
108
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
109
|
+
static extern bool SetInformationJobObject(IntPtr hJob, int JobObjectInfoClass, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);
|
|
110
|
+
|
|
111
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
112
|
+
static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
|
113
|
+
|
|
114
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
115
|
+
static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
|
116
|
+
|
|
117
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
118
|
+
static extern bool CloseHandle(IntPtr hObject);
|
|
119
|
+
|
|
120
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
121
|
+
static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate HandlerRoutine, bool Add);
|
|
122
|
+
|
|
123
|
+
delegate bool ConsoleCtrlDelegate(uint dwCtrlType);
|
|
124
|
+
|
|
125
|
+
static uint childGroupPid;
|
|
126
|
+
static ConsoleCtrlDelegate ctrlHandler;
|
|
127
|
+
|
|
128
|
+
static int Main(string[] args) {
|
|
129
|
+
if (args == null || args.Length < 1) {
|
|
130
|
+
Console.Error.WriteLine("usage: harness-win32-group break <pid> | run --pidfile <p> --cwd <d> -- <cmd> [args]");
|
|
131
|
+
return 2;
|
|
132
|
+
}
|
|
133
|
+
if (args[0] == "break") {
|
|
134
|
+
if (args.Length < 2) return 2;
|
|
135
|
+
uint pid;
|
|
136
|
+
if (!uint.TryParse(args[1], out pid) || pid == 0) return 2;
|
|
137
|
+
return GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid) ? 0 : 1;
|
|
138
|
+
}
|
|
139
|
+
if (args[0] == "run") return Run(args);
|
|
140
|
+
return 2;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
static bool OnCtrl(uint type) {
|
|
144
|
+
if (childGroupPid != 0) {
|
|
145
|
+
GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, childGroupPid);
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
static int Run(string[] args) {
|
|
151
|
+
string pidfile = null;
|
|
152
|
+
string cwd = null;
|
|
153
|
+
int dash = -1;
|
|
154
|
+
for (int i = 1; i < args.Length; i++) {
|
|
155
|
+
if (args[i] == "--") { dash = i; break; }
|
|
156
|
+
if (args[i] == "--pidfile" && i + 1 < args.Length) { pidfile = args[++i]; continue; }
|
|
157
|
+
if (args[i] == "--cwd" && i + 1 < args.Length) { cwd = args[++i]; continue; }
|
|
158
|
+
}
|
|
159
|
+
if (dash < 0 || dash + 1 >= args.Length || string.IsNullOrEmpty(pidfile)) {
|
|
160
|
+
Console.Error.WriteLine("run: missing --pidfile or command");
|
|
161
|
+
return 2;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
StringBuilder cmd = new StringBuilder();
|
|
165
|
+
for (int i = dash + 1; i < args.Length; i++) {
|
|
166
|
+
AppendQuoted(cmd, args[i]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
ctrlHandler = new ConsoleCtrlDelegate(OnCtrl);
|
|
170
|
+
SetConsoleCtrlHandler(ctrlHandler, true);
|
|
171
|
+
|
|
172
|
+
IntPtr job = CreateJobObject(IntPtr.Zero, null);
|
|
173
|
+
if (job != IntPtr.Zero) {
|
|
174
|
+
EnableKillOnClose(job);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
STARTUPINFO si = new STARTUPINFO();
|
|
178
|
+
si.cb = Marshal.SizeOf(typeof(STARTUPINFO));
|
|
179
|
+
PROCESS_INFORMATION pi;
|
|
180
|
+
bool ok = CreateProcessW(
|
|
181
|
+
null,
|
|
182
|
+
cmd,
|
|
183
|
+
IntPtr.Zero,
|
|
184
|
+
IntPtr.Zero,
|
|
185
|
+
true,
|
|
186
|
+
CREATE_NEW_PROCESS_GROUP,
|
|
187
|
+
IntPtr.Zero,
|
|
188
|
+
string.IsNullOrEmpty(cwd) ? null : cwd,
|
|
189
|
+
ref si,
|
|
190
|
+
out pi);
|
|
191
|
+
if (!ok) {
|
|
192
|
+
Console.Error.WriteLine("CreateProcessW failed: " + Marshal.GetLastWin32Error());
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
childGroupPid = pi.dwProcessId;
|
|
197
|
+
try {
|
|
198
|
+
File.WriteAllText(pidfile, pi.dwProcessId.ToString());
|
|
199
|
+
} catch (Exception ex) {
|
|
200
|
+
Console.Error.WriteLine("pidfile: " + ex.Message);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (job != IntPtr.Zero) {
|
|
204
|
+
AssignProcessToJobObject(job, pi.hProcess);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
string breakPath = Path.Combine(Path.GetTempPath(), "harness-win32-break-" + pi.dwProcessId.ToString());
|
|
208
|
+
while (true) {
|
|
209
|
+
uint wr = WaitForSingleObject(pi.hProcess, 50);
|
|
210
|
+
if (wr == WAIT_OBJECT_0) break;
|
|
211
|
+
if (File.Exists(breakPath)) {
|
|
212
|
+
try { File.Delete(breakPath); } catch { }
|
|
213
|
+
GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pi.dwProcessId);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
uint code = 1;
|
|
218
|
+
GetExitCodeProcess(pi.hProcess, out code);
|
|
219
|
+
CloseHandle(pi.hThread);
|
|
220
|
+
CloseHandle(pi.hProcess);
|
|
221
|
+
if (job != IntPtr.Zero) CloseHandle(job);
|
|
222
|
+
return (int)code;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
static void EnableKillOnClose(IntPtr job) {
|
|
226
|
+
JOBOBJECT_EXTENDED_LIMIT_INFORMATION info = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
|
|
227
|
+
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
|
228
|
+
int size = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
|
|
229
|
+
IntPtr ptr = Marshal.AllocHGlobal(size);
|
|
230
|
+
try {
|
|
231
|
+
Marshal.StructureToPtr(info, ptr, false);
|
|
232
|
+
SetInformationJobObject(job, JobObjectExtendedLimitInformation, ptr, (uint)size);
|
|
233
|
+
} finally {
|
|
234
|
+
Marshal.FreeHGlobal(ptr);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
static void AppendQuoted(StringBuilder sb, string arg) {
|
|
239
|
+
if (sb.Length > 0) sb.Append(' ');
|
|
240
|
+
if (arg == null) arg = "";
|
|
241
|
+
bool quote = arg.Length == 0;
|
|
242
|
+
for (int i = 0; i < arg.Length; i++) {
|
|
243
|
+
char c = arg[i];
|
|
244
|
+
if (c == ' ' || c == '\\t' || c == '"') { quote = true; break; }
|
|
245
|
+
}
|
|
246
|
+
if (!quote) { sb.Append(arg); return; }
|
|
247
|
+
sb.Append('"');
|
|
248
|
+
int bs = 0;
|
|
249
|
+
for (int i = 0; i < arg.Length; i++) {
|
|
250
|
+
char c = arg[i];
|
|
251
|
+
if (c == '\\\\') { bs++; continue; }
|
|
252
|
+
if (c == '"') {
|
|
253
|
+
for (int j = 0; j < bs; j++) sb.Append('\\\\');
|
|
254
|
+
sb.Append('\\\\');
|
|
255
|
+
sb.Append('"');
|
|
256
|
+
bs = 0;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
for (int j = 0; j < bs; j++) sb.Append('\\\\');
|
|
260
|
+
sb.Append(c);
|
|
261
|
+
bs = 0;
|
|
262
|
+
}
|
|
263
|
+
for (int j = 0; j < bs * 2; j++) sb.Append('\\\\');
|
|
264
|
+
sb.Append('"');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
`;
|
|
268
|
+
function helperHash() {
|
|
269
|
+
return createHash("sha256").update(HELPER_SOURCE).digest("hex").slice(0, 12);
|
|
270
|
+
}
|
|
271
|
+
function findCsc() {
|
|
272
|
+
const windir = process.env.WINDIR ?? "C:\\Windows";
|
|
273
|
+
const candidates = [
|
|
274
|
+
join(windir, "Microsoft.NET", "Framework64", "v4.0.30319", "csc.exe"),
|
|
275
|
+
join(windir, "Microsoft.NET", "Framework", "v4.0.30319", "csc.exe"),
|
|
276
|
+
];
|
|
277
|
+
for (const c of candidates) {
|
|
278
|
+
if (existsSync(c))
|
|
279
|
+
return c;
|
|
280
|
+
}
|
|
281
|
+
const where = spawnSync("where.exe", ["csc.exe"], { encoding: "utf8", windowsHide: true });
|
|
282
|
+
const first = where.stdout?.split(/\r?\n/).map((s) => s.trim()).find((s) => s.length > 0);
|
|
283
|
+
if (first && existsSync(first))
|
|
284
|
+
return first;
|
|
285
|
+
throw new Error("csc.exe not found; cannot compile the Windows process-group helper");
|
|
286
|
+
}
|
|
287
|
+
let cachedHelper;
|
|
288
|
+
export function ensureWin32GroupHelper() {
|
|
289
|
+
if (cachedHelper && existsSync(cachedHelper))
|
|
290
|
+
return cachedHelper;
|
|
291
|
+
const dir = join(tmpdir(), "harness-win32-group");
|
|
292
|
+
mkdirSync(dir, { recursive: true });
|
|
293
|
+
const id = helperHash();
|
|
294
|
+
const exe = join(dir, `harness-win32-group-${id}.exe`);
|
|
295
|
+
if (existsSync(exe)) {
|
|
296
|
+
cachedHelper = exe;
|
|
297
|
+
return exe;
|
|
298
|
+
}
|
|
299
|
+
const cs = join(dir, `harness-win32-group-${id}.cs`);
|
|
300
|
+
writeFileSync(cs, HELPER_SOURCE, "utf8");
|
|
301
|
+
const csc = findCsc();
|
|
302
|
+
const compiled = spawnSync(csc, ["/nologo", "/optimize+", `/out:${exe}`, "/target:exe", cs], {
|
|
303
|
+
encoding: "utf8",
|
|
304
|
+
windowsHide: true,
|
|
305
|
+
});
|
|
306
|
+
if (!existsSync(exe)) {
|
|
307
|
+
throw new Error(`failed to compile Windows process-group helper with ${csc}: ${compiled.stderr || compiled.stdout || compiled.status}`);
|
|
308
|
+
}
|
|
309
|
+
cachedHelper = exe;
|
|
310
|
+
return exe;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Deliver CTRL_BREAK to a CREATE_NEW_PROCESS_GROUP leader that shares this
|
|
314
|
+
* console. Never TerminateProcess / taskkill.
|
|
315
|
+
*/
|
|
316
|
+
export function sendCtrlBreak(processGroupId) {
|
|
317
|
+
writeFileSync(win32BreakSignalPath(processGroupId), "break");
|
|
318
|
+
const helper = ensureWin32GroupHelper();
|
|
319
|
+
spawn(helper, ["break", String(processGroupId)], {
|
|
320
|
+
detached: false,
|
|
321
|
+
windowsHide: false,
|
|
322
|
+
stdio: "ignore",
|
|
323
|
+
}).on("error", () => undefined);
|
|
324
|
+
}
|
|
325
|
+
function waitForPidFile(path, helper, ms = 8_000) {
|
|
326
|
+
const start = Date.now();
|
|
327
|
+
const slot = new Int32Array(new SharedArrayBuffer(4));
|
|
328
|
+
while (Date.now() - start < ms) {
|
|
329
|
+
if (existsSync(path)) {
|
|
330
|
+
const n = Number(readFileSync(path, "utf8").trim());
|
|
331
|
+
if (Number.isInteger(n) && n > 0)
|
|
332
|
+
return n;
|
|
333
|
+
}
|
|
334
|
+
if (helper.exitCode !== null) {
|
|
335
|
+
throw new Error(`Windows process-group helper exited before pidfile (code ${helper.exitCode})`);
|
|
336
|
+
}
|
|
337
|
+
Atomics.wait(slot, 0, 0, 20);
|
|
338
|
+
}
|
|
339
|
+
throw new Error(`timed out waiting for Windows process-group pidfile at ${path}`);
|
|
340
|
+
}
|
|
341
|
+
export function spawnWindowsProcessGroup(command, args, options = {}) {
|
|
342
|
+
const helper = ensureWin32GroupHelper();
|
|
343
|
+
const id = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
344
|
+
const pidFile = join(tmpdir(), `harness-win32-pid-${id}`);
|
|
345
|
+
const cwd = typeof options.cwd === "string" ? options.cwd : process.cwd();
|
|
346
|
+
const child = spawn(helper, ["run", "--pidfile", pidFile, "--cwd", cwd, "--", command, ...args], {
|
|
347
|
+
...options,
|
|
348
|
+
detached: false,
|
|
349
|
+
windowsHide: false,
|
|
350
|
+
});
|
|
351
|
+
try {
|
|
352
|
+
child.groupPid = waitForPidFile(pidFile, child);
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
child.kill();
|
|
356
|
+
throw err;
|
|
357
|
+
}
|
|
358
|
+
return child;
|
|
359
|
+
}
|
|
360
|
+
//# sourceMappingURL=win32-process-group.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mikenguyen69/harness",
|
|
3
|
+
"version": "0.1.0-beta.1",
|
|
4
|
+
"description": "One installed `harness` command over the portfolio — routing facade, not a fourth control loop.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": "24.x"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"schema",
|
|
12
|
+
"profiles",
|
|
13
|
+
"templates",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/mikenguyen69/ai-harness.git",
|
|
21
|
+
"directory": null
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/mikenguyen69/ai-harness/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/mikenguyen69/ai-harness#readme",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"tag": "next"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.251",
|
|
32
|
+
"ajv": "^8.17.1",
|
|
33
|
+
"ajv-formats": "^3.0.1",
|
|
34
|
+
"picomatch": "^4.0.7",
|
|
35
|
+
"smol-toml": "^1.8.0",
|
|
36
|
+
"yaml": "^2.6.0"
|
|
37
|
+
},
|
|
38
|
+
"bin": {
|
|
39
|
+
"harness": "dist/cli.js"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Python profile — the 10-name vocabulary (ADR 0004) bound to a Python stack
|
|
2
|
+
# (ruff / mypy / pytest). Third stack instance; still zero src changes.
|
|
3
|
+
|
|
4
|
+
[harness]
|
|
5
|
+
profile_version = "0.0.1"
|
|
6
|
+
|
|
7
|
+
[gates.spec]
|
|
8
|
+
run = "harness gate spec"
|
|
9
|
+
severity = "blocking"
|
|
10
|
+
cost = "cheap"
|
|
11
|
+
|
|
12
|
+
[gates.fmt]
|
|
13
|
+
run = "ruff format --check ."
|
|
14
|
+
severity = "blocking"
|
|
15
|
+
cost = "cheap"
|
|
16
|
+
|
|
17
|
+
[gates.lint]
|
|
18
|
+
run = "ruff check ."
|
|
19
|
+
severity = "changed-lines"
|
|
20
|
+
cost = "cheap"
|
|
21
|
+
|
|
22
|
+
[gates.types]
|
|
23
|
+
run = "mypy --strict ."
|
|
24
|
+
severity = "blocking"
|
|
25
|
+
cost = "cheap"
|
|
26
|
+
|
|
27
|
+
[gates.test]
|
|
28
|
+
run = "pytest -q"
|
|
29
|
+
severity = "blocking"
|
|
30
|
+
cost = "cheap"
|
|
31
|
+
|
|
32
|
+
[gates."sec:deps"]
|
|
33
|
+
run = "pip-audit --strict"
|
|
34
|
+
severity = "changed-lines"
|
|
35
|
+
cost = "cheap"
|
|
36
|
+
snapshot_max_age_days = 7
|
|
37
|
+
|
|
38
|
+
[gates."sec:secrets"]
|
|
39
|
+
run = "gitleaks detect --no-banner"
|
|
40
|
+
severity = "blocking"
|
|
41
|
+
cost = "cheap"
|
|
42
|
+
|
|
43
|
+
[gates.arch]
|
|
44
|
+
run = "lint-imports" # import-linter contracts
|
|
45
|
+
severity = "advisory"
|
|
46
|
+
cost = "cheap"
|
|
47
|
+
|
|
48
|
+
[gates.contract]
|
|
49
|
+
declined = "no service boundary in the reference python profile; bind per repo (openapi-diff / schemathesis)"
|
|
50
|
+
|
|
51
|
+
[gates.size]
|
|
52
|
+
run = "harness gate size"
|
|
53
|
+
severity = "blocking"
|
|
54
|
+
cost = "cheap"
|
|
55
|
+
|
|
56
|
+
[gates."gen:fresh"]
|
|
57
|
+
run = "harness gate gen-fresh"
|
|
58
|
+
severity = "blocking"
|
|
59
|
+
cost = "cheap"
|