@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
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* orchestrate — the execution-loop entrypoint.
|
|
4
|
+
*
|
|
5
|
+
* orchestrate run [--repo <path>] [--config <path>]
|
|
6
|
+
* drive the spec DAG in a real checkout — CliHarness + CliRouteSource +
|
|
7
|
+
* ShellGit + LocalForge/GithubForge + ClaudeRunner, per orchestration.json.
|
|
8
|
+
* orchestrate demo
|
|
9
|
+
* the M1 dry loop against a fake DAG — prints RunState.
|
|
10
|
+
*
|
|
11
|
+
* `orchestrate report` (end-of-run HTML from the final RunState — D2) is M5.
|
|
12
|
+
*/
|
|
13
|
+
import { FakeHarness } from "./harness/index.js";
|
|
14
|
+
import { FakeRouteSource } from "./routes/index.js";
|
|
15
|
+
import { FakeRunner } from "./runner/index.js";
|
|
16
|
+
import { RunLog } from "./runstate/index.js";
|
|
17
|
+
import { readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { join, resolve } from "node:path";
|
|
19
|
+
import { DEFAULT_CONFIG, FakeGit, resumeAfterDoor, runLoop, startupSweep, } from "./loop/index.js";
|
|
20
|
+
import { renderRunReport } from "./report/index.js";
|
|
21
|
+
import { assembleRun } from "./run/index.js";
|
|
22
|
+
import { runDoctor } from "./doctor/index.js";
|
|
23
|
+
import { resolveCommandSpec } from "./harness/cli.js";
|
|
24
|
+
import { StubRunner } from "./runners/stub/index.js";
|
|
25
|
+
import { readdirSync } from "node:fs";
|
|
26
|
+
import { analyzeRun, parseActivityLog } from "./analyze/index.js";
|
|
27
|
+
const USAGE = `orchestrate — drive a spec DAG to merged with an agent fleet
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
orchestrate run [--repo <path>] [--config <path>] [--routes <path>]
|
|
31
|
+
[--only <id-prefix>] [--spool <hub-dir>] [--report <path>]
|
|
32
|
+
[--watch] [--activity-log <dir>] [--no-activity-log] [--no-ledger]
|
|
33
|
+
real run against a checkout (default --repo: cwd);
|
|
34
|
+
--only limits claims (harness next --only); --spool feeds the board (D1);
|
|
35
|
+
--watch mirrors Cursor lane activity to stderr as well as the spool;
|
|
36
|
+
activity capture is ON by default (<repo>/.harness/runs/<run-id>/activity.jsonl);
|
|
37
|
+
analysis runs automatically at the end and appends one run.analyzed ledger
|
|
38
|
+
event unless --no-ledger; --no-activity-log disables capture entirely
|
|
39
|
+
orchestrate doctor [--repo <path>] --routes <path> [--spool <path>] [--json]
|
|
40
|
+
read-only preflight; --allow-dirty makes dirt a warning
|
|
41
|
+
orchestrate resume --unit <id> [--repo <path>] [--routes <path>]
|
|
42
|
+
[--spool <hub-dir>] [--report <path>] [--watch]
|
|
43
|
+
[--activity-log <dir>] [--no-activity-log] [--no-ledger] [--resumes <run-id>]
|
|
44
|
+
after a signed door approval, reverify/review/land the unit;
|
|
45
|
+
--resumes links the new run's activity log back to the run it continues
|
|
46
|
+
orchestrate sweep [--repo <path>] [--routes <path>] [--json]
|
|
47
|
+
startup crash-window check (stranded in-review, ledger-ahead-of-git)
|
|
48
|
+
orchestrate demo [--report <path>]
|
|
49
|
+
the M1 dry loop against a fake DAG, prints RunState
|
|
50
|
+
orchestrate analyze [--run <id> | --last | --log <path>] [--repo <path>]
|
|
51
|
+
[--out <path>] [--json] [--no-ledger]
|
|
52
|
+
synthesize a captured activity.jsonl into findings (P1);
|
|
53
|
+
--run/--last resolve under <repo>/.harness/runs/, or pass --log directly;
|
|
54
|
+
writes summary.json next to the log unless --out is given
|
|
55
|
+
orchestrate report --state <run-state.json> [--out <path>]
|
|
56
|
+
render the end-of-run HTML from a saved RunState (D2)
|
|
57
|
+
orchestrate --help this text
|
|
58
|
+
|
|
59
|
+
Env: HARNESS_MACHINE_ID, ORCHESTRATE_STUB_RUNNER, ORCHESTRATE_STUB_CONFIG.
|
|
60
|
+
Development-only escape hatch (not for installed packages): HARNESS_BIN, AGENT_HARNESS_BIN.
|
|
61
|
+
`;
|
|
62
|
+
function exitCodeForRunState(state) {
|
|
63
|
+
if (state.outcome === "completed")
|
|
64
|
+
return 0;
|
|
65
|
+
if (state.needsYou.some((entry) => entry.kind === "one-way-door"))
|
|
66
|
+
return 2;
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
function exitCodeForSweepHalts(halts) {
|
|
70
|
+
return halts.length === 0 ? 0 : 1;
|
|
71
|
+
}
|
|
72
|
+
function readStubEnvConfig() {
|
|
73
|
+
if (process.env.ORCHESTRATE_STUB_RUNNER !== "1")
|
|
74
|
+
return null;
|
|
75
|
+
const path = process.env.ORCHESTRATE_STUB_CONFIG;
|
|
76
|
+
if (!path)
|
|
77
|
+
return {};
|
|
78
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
79
|
+
}
|
|
80
|
+
function buildStubRunner(repo, config, stubConfig) {
|
|
81
|
+
const afterImplement = (unitId) => {
|
|
82
|
+
const action = stubConfig.afterImplement?.[unitId];
|
|
83
|
+
if (action === "ledger-unwritable") {
|
|
84
|
+
const ledgerDir = join(repo, ".harness", "ledger");
|
|
85
|
+
renameSync(ledgerDir, `${ledgerDir}-saved`);
|
|
86
|
+
writeFileSync(ledgerDir, "fault injection");
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const stub = new StubRunner({
|
|
90
|
+
cwd: repo,
|
|
91
|
+
baseBranch: config.baseBranch,
|
|
92
|
+
...(Object.keys(stubConfig.afterImplement ?? {}).length ? { afterImplement } : {}),
|
|
93
|
+
});
|
|
94
|
+
for (const [unitId, mode] of Object.entries(stubConfig.scripts ?? {})) {
|
|
95
|
+
if (mode === "out-of-scope")
|
|
96
|
+
stub.scriptOutOfScope(unitId);
|
|
97
|
+
else if (mode === "fail")
|
|
98
|
+
stub.scriptFail(unitId);
|
|
99
|
+
else if (mode === "escalate")
|
|
100
|
+
stub.scriptEscalate(unitId);
|
|
101
|
+
}
|
|
102
|
+
return stub;
|
|
103
|
+
}
|
|
104
|
+
/** E2E-only: swap real runners for StubRunner after assembleRun. */
|
|
105
|
+
function applyStubRunners(deps, repo) {
|
|
106
|
+
const stubConfig = readStubEnvConfig();
|
|
107
|
+
if (stubConfig === null)
|
|
108
|
+
return;
|
|
109
|
+
const stub = buildStubRunner(repo, deps.config, stubConfig);
|
|
110
|
+
for (const name of deps.config.runnersEnabled)
|
|
111
|
+
deps.runners.set(name, stub);
|
|
112
|
+
if (stubConfig.retryBudget !== undefined)
|
|
113
|
+
deps.config.retryBudget = stubConfig.retryBudget;
|
|
114
|
+
}
|
|
115
|
+
function flag(name) {
|
|
116
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
117
|
+
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : undefined;
|
|
118
|
+
}
|
|
119
|
+
function summarise(state, analysis) {
|
|
120
|
+
process.stdout.write(JSON.stringify(state, null, 2) + "\n");
|
|
121
|
+
process.stdout.write(`\noutcome=${state.outcome} merged=${state.counts.merged}/${state.dagSize} ` +
|
|
122
|
+
`parked=${state.counts.parked} escalated=${state.counts.escalated} ` +
|
|
123
|
+
`needsYou=${state.counts.needsYou}\n`);
|
|
124
|
+
for (const n of state.needsYou) {
|
|
125
|
+
process.stdout.write(` needs-you [${n.kind}] ${n.unitId ?? ""}: ${n.unblock}\n`);
|
|
126
|
+
}
|
|
127
|
+
const reportPath = flag("report");
|
|
128
|
+
if (reportPath) {
|
|
129
|
+
writeFileSync(reportPath, renderRunReport(state, analysis ?? undefined));
|
|
130
|
+
process.stdout.write(`\nreport → ${reportPath}\n`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** the `run.analyzed` body — metrics and rule ids only, no free text (P5.3: nothing for redaction to strip). */
|
|
134
|
+
function runAnalyzedBody(summary) {
|
|
135
|
+
return {
|
|
136
|
+
run_id: summary.runId,
|
|
137
|
+
units: summary.units.map((u) => ({
|
|
138
|
+
unit_id: u.unitId,
|
|
139
|
+
turns_total: u.turnsTotal,
|
|
140
|
+
tool_failures: u.toolFailures,
|
|
141
|
+
attempts: u.attempts,
|
|
142
|
+
wall_ms: u.wallMs,
|
|
143
|
+
agent_ms: u.agentMs,
|
|
144
|
+
...(u.costUsd !== undefined ? { cost_usd: u.costUsd } : {}),
|
|
145
|
+
runner: u.runner,
|
|
146
|
+
model: u.model,
|
|
147
|
+
})),
|
|
148
|
+
findings: summary.findings.map((f) => ({
|
|
149
|
+
rule: f.id,
|
|
150
|
+
unit_id: f.unitId,
|
|
151
|
+
severity: f.severity,
|
|
152
|
+
count: f.evidence.length,
|
|
153
|
+
})),
|
|
154
|
+
log_ref: summary.logRef,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async function appendRunAnalyzed(harness, runId, summary) {
|
|
158
|
+
// run-scoped event — CliHarness.ledgerAppend always folds `unitId` into
|
|
159
|
+
// `body.unit_id`; there is no single natural unit for a whole run, so `runId`
|
|
160
|
+
// doubles for both (and, since `session` is unset, for `actor.id` too —
|
|
161
|
+
// assembleRun already sets actorId to runId). Redundant with body.run_id,
|
|
162
|
+
// but harmless — not a copy-paste bug.
|
|
163
|
+
await harness.ledgerAppend({ type: "run.analyzed", unitId: runId, body: runAnalyzedBody(summary) });
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* P2 — runs automatically at the end of `orchestrate run`/`resume`. A no-op
|
|
167
|
+
* when capture was disabled (`--no-activity-log`) so a missing summary.json
|
|
168
|
+
* isn't a silent surprise — we tell the operator why. Appends one bounded
|
|
169
|
+
* `run.analyzed` ledger event unless `--no-ledger` is passed.
|
|
170
|
+
*/
|
|
171
|
+
async function runAutoAnalyze(deps, repo, runId, state) {
|
|
172
|
+
if (!deps.activityLog) {
|
|
173
|
+
process.stderr.write("orchestrate: activity capture was disabled — skipping analysis\n");
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const events = deps.activityLog.events(); // in-process — no re-read of the file needed
|
|
177
|
+
const costByUnit = new Map(state.lanes.map((l) => [l.unitId, l.cost]));
|
|
178
|
+
const logRef = `runs/${runId}/activity.jsonl`;
|
|
179
|
+
const summary = analyzeRun(events, { logRef, costByUnit });
|
|
180
|
+
const summaryPath = join(repo, ".harness", "runs", runId, "summary.json");
|
|
181
|
+
writeFileSync(summaryPath, JSON.stringify(summary, null, 2));
|
|
182
|
+
if (!hasFlag("no-ledger")) {
|
|
183
|
+
try {
|
|
184
|
+
await appendRunAnalyzed(deps.harness, runId, summary);
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
process.stderr.write(`orchestrate: run.analyzed ledger append failed: ${e.message}\n`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return summary;
|
|
191
|
+
}
|
|
192
|
+
function report() {
|
|
193
|
+
const statePath = flag("state");
|
|
194
|
+
if (!statePath) {
|
|
195
|
+
process.stderr.write("orchestrate report --state <run-state.json> [--out <path>]\n");
|
|
196
|
+
return 2;
|
|
197
|
+
}
|
|
198
|
+
let state;
|
|
199
|
+
try {
|
|
200
|
+
state = JSON.parse(readFileSync(statePath, "utf8"));
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
process.stderr.write(`orchestrate report: cannot read ${statePath}: ${e.message}\n`);
|
|
204
|
+
return 2;
|
|
205
|
+
}
|
|
206
|
+
const out = flag("out") ?? "run-report.html";
|
|
207
|
+
writeFileSync(out, renderRunReport(state));
|
|
208
|
+
process.stdout.write(`report → ${out}\n`);
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
async function demo() {
|
|
212
|
+
const harness = new FakeHarness([
|
|
213
|
+
{ id: "a", scope: ["src/a/**"], mergeFacts: { door: "two-way" } },
|
|
214
|
+
{ id: "b", deps: ["a"], scope: ["src/b/**"], mergeFacts: { door: "two-way" } },
|
|
215
|
+
{ id: "spike", mode: "explore", scope: ["src/spike/**"] },
|
|
216
|
+
]);
|
|
217
|
+
const runners = new Map([["claude", new FakeRunner({ name: "claude" })]]);
|
|
218
|
+
const deps = {
|
|
219
|
+
harness,
|
|
220
|
+
routes: new FakeRouteSource(),
|
|
221
|
+
runners,
|
|
222
|
+
git: new FakeGit(),
|
|
223
|
+
config: { ...DEFAULT_CONFIG, runnersEnabled: ["claude"] },
|
|
224
|
+
log: new RunLog(),
|
|
225
|
+
runId: "demo-run",
|
|
226
|
+
machineId: "local",
|
|
227
|
+
};
|
|
228
|
+
const state = await runLoop(deps, {
|
|
229
|
+
specRef: "specs/demo.yaml",
|
|
230
|
+
systemId: "demo",
|
|
231
|
+
dagSize: 3,
|
|
232
|
+
});
|
|
233
|
+
summarise(state);
|
|
234
|
+
return state.outcome === "completed" ? 0 : 1;
|
|
235
|
+
}
|
|
236
|
+
function hasFlag(name) {
|
|
237
|
+
return process.argv.includes(`--${name}`);
|
|
238
|
+
}
|
|
239
|
+
async function realRun() {
|
|
240
|
+
const configPath = flag("config");
|
|
241
|
+
const routesPath = flag("routes");
|
|
242
|
+
const spoolDir = flag("spool");
|
|
243
|
+
const onlyPrefix = flag("only");
|
|
244
|
+
const watch = hasFlag("watch");
|
|
245
|
+
const activityLogDir = flag("activity-log");
|
|
246
|
+
const noActivityLog = hasFlag("no-activity-log");
|
|
247
|
+
let assembled;
|
|
248
|
+
try {
|
|
249
|
+
assembled = assembleRun({
|
|
250
|
+
repo: flag("repo") ?? process.cwd(),
|
|
251
|
+
...(configPath ? { configPath } : {}),
|
|
252
|
+
...(routesPath ? { routesPath } : {}),
|
|
253
|
+
...(onlyPrefix ? { onlyPrefix } : {}),
|
|
254
|
+
...(spoolDir ? { spoolDir } : {}),
|
|
255
|
+
...(watch ? { watch } : {}),
|
|
256
|
+
...(activityLogDir ? { activityLogDir } : {}),
|
|
257
|
+
...(noActivityLog ? { activityLog: false } : {}),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
catch (e) {
|
|
261
|
+
process.stderr.write(`orchestrate run: ${e.message}\n`);
|
|
262
|
+
return 2;
|
|
263
|
+
}
|
|
264
|
+
const repo = resolve(flag("repo") ?? process.cwd());
|
|
265
|
+
applyStubRunners(assembled.deps, repo);
|
|
266
|
+
const { deps, runId, systemId, specRef } = assembled;
|
|
267
|
+
const dagSize = (await deps.harness.units()).length;
|
|
268
|
+
const state = await runLoop(deps, { specRef, systemId, dagSize });
|
|
269
|
+
const analysis = await runAutoAnalyze(deps, repo, runId, state);
|
|
270
|
+
summarise(state, analysis);
|
|
271
|
+
return exitCodeForRunState(state);
|
|
272
|
+
}
|
|
273
|
+
async function resume() {
|
|
274
|
+
const unitId = flag("unit");
|
|
275
|
+
if (!unitId) {
|
|
276
|
+
process.stderr.write("orchestrate resume --unit <id> [--repo <path>] [--routes <path>]\n");
|
|
277
|
+
return 2;
|
|
278
|
+
}
|
|
279
|
+
const configPath = flag("config");
|
|
280
|
+
const routesPath = flag("routes");
|
|
281
|
+
const spoolDir = flag("spool");
|
|
282
|
+
const onlyPrefix = flag("only");
|
|
283
|
+
const watch = hasFlag("watch");
|
|
284
|
+
const activityLogDir = flag("activity-log");
|
|
285
|
+
const noActivityLog = hasFlag("no-activity-log");
|
|
286
|
+
const resumesRunId = flag("resumes");
|
|
287
|
+
let assembled;
|
|
288
|
+
try {
|
|
289
|
+
assembled = assembleRun({
|
|
290
|
+
repo: flag("repo") ?? process.cwd(),
|
|
291
|
+
...(configPath ? { configPath } : {}),
|
|
292
|
+
...(routesPath ? { routesPath } : {}),
|
|
293
|
+
...(onlyPrefix ? { onlyPrefix } : {}),
|
|
294
|
+
...(spoolDir ? { spoolDir } : {}),
|
|
295
|
+
...(watch ? { watch } : {}),
|
|
296
|
+
...(activityLogDir ? { activityLogDir } : {}),
|
|
297
|
+
...(noActivityLog ? { activityLog: false } : {}),
|
|
298
|
+
...(resumesRunId ? { resumesRunId } : {}),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
process.stderr.write(`orchestrate resume: ${error.message}\n`);
|
|
303
|
+
return 2;
|
|
304
|
+
}
|
|
305
|
+
const repo = resolve(flag("repo") ?? process.cwd());
|
|
306
|
+
applyStubRunners(assembled.deps, repo);
|
|
307
|
+
const dagSize = (await assembled.deps.harness.units()).length;
|
|
308
|
+
const result = await resumeAfterDoor(unitId, assembled.deps);
|
|
309
|
+
if (result.outcome !== "merged") {
|
|
310
|
+
process.stderr.write(result.outcome === "halted"
|
|
311
|
+
? `${result.halt.message}\n ${result.halt.unblock}\n`
|
|
312
|
+
: `${unitId} was not resumed (${result.outcome})\n`);
|
|
313
|
+
return 1;
|
|
314
|
+
}
|
|
315
|
+
const state = await runLoop(assembled.deps, {
|
|
316
|
+
specRef: assembled.specRef,
|
|
317
|
+
systemId: assembled.systemId,
|
|
318
|
+
dagSize,
|
|
319
|
+
});
|
|
320
|
+
const analysis = await runAutoAnalyze(assembled.deps, repo, assembled.runId, state);
|
|
321
|
+
summarise(state, analysis);
|
|
322
|
+
return exitCodeForRunState(state);
|
|
323
|
+
}
|
|
324
|
+
async function sweep() {
|
|
325
|
+
const configPath = flag("config");
|
|
326
|
+
const routesPath = flag("routes");
|
|
327
|
+
const onlyPrefix = flag("only");
|
|
328
|
+
let assembled;
|
|
329
|
+
try {
|
|
330
|
+
assembled = assembleRun({
|
|
331
|
+
repo: flag("repo") ?? process.cwd(),
|
|
332
|
+
...(configPath ? { configPath } : {}),
|
|
333
|
+
...(routesPath ? { routesPath } : {}),
|
|
334
|
+
...(onlyPrefix ? { onlyPrefix } : {}),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
process.stderr.write(`orchestrate sweep: ${error.message}\n`);
|
|
339
|
+
return 2;
|
|
340
|
+
}
|
|
341
|
+
applyStubRunners(assembled.deps, resolve(flag("repo") ?? process.cwd()));
|
|
342
|
+
const halts = await startupSweep(assembled.deps);
|
|
343
|
+
if (process.argv.includes("--json")) {
|
|
344
|
+
process.stdout.write(JSON.stringify(halts, null, 2) + "\n");
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
for (const halt of halts) {
|
|
348
|
+
process.stderr.write(` ⛔ ${halt.kind} ${halt.unitId ?? ""}: ${halt.message}\n`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return exitCodeForSweepHalts(halts);
|
|
352
|
+
}
|
|
353
|
+
/** run ids are `run-${Date.now().toString(36)}` — sorts chronologically as a string. */
|
|
354
|
+
function findLastRunId(runsRoot) {
|
|
355
|
+
let entries;
|
|
356
|
+
try {
|
|
357
|
+
entries = readdirSync(runsRoot);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
return entries.filter((e) => e.startsWith("run-")).sort().at(-1);
|
|
363
|
+
}
|
|
364
|
+
async function analyze() {
|
|
365
|
+
const repo = resolve(flag("repo") ?? process.cwd());
|
|
366
|
+
const runsRoot = join(repo, ".harness", "runs");
|
|
367
|
+
let logPath = flag("log");
|
|
368
|
+
let runId = flag("run");
|
|
369
|
+
if (!logPath) {
|
|
370
|
+
if (hasFlag("last"))
|
|
371
|
+
runId = findLastRunId(runsRoot);
|
|
372
|
+
if (!runId) {
|
|
373
|
+
process.stderr.write("orchestrate analyze: pass --run <id>, --last, or --log <path>\n");
|
|
374
|
+
return 2;
|
|
375
|
+
}
|
|
376
|
+
logPath = join(runsRoot, runId, "activity.jsonl");
|
|
377
|
+
}
|
|
378
|
+
let events;
|
|
379
|
+
try {
|
|
380
|
+
events = parseActivityLog(readFileSync(logPath, "utf8"));
|
|
381
|
+
}
|
|
382
|
+
catch (e) {
|
|
383
|
+
process.stderr.write(`orchestrate analyze: cannot read ${logPath}: ${e.message}\n`);
|
|
384
|
+
return 2;
|
|
385
|
+
}
|
|
386
|
+
const logRef = runId ? `runs/${runId}/activity.jsonl` : logPath;
|
|
387
|
+
const summary = analyzeRun(events, { logRef });
|
|
388
|
+
const outPath = flag("out") ?? (runId ? join(runsRoot, runId, "summary.json") : undefined);
|
|
389
|
+
if (outPath)
|
|
390
|
+
writeFileSync(outPath, JSON.stringify(summary, null, 2));
|
|
391
|
+
if (hasFlag("json")) {
|
|
392
|
+
process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
process.stdout.write(`run ${summary.runId || "(unknown)"} — ${summary.units.length} unit(s), ${summary.findings.length} finding(s)\n`);
|
|
396
|
+
for (const f of summary.findings) {
|
|
397
|
+
process.stdout.write(` [${f.severity}] ${f.id} ${f.unitId}: ${f.message} (${f.evidence.length} evidence line(s))\n`);
|
|
398
|
+
}
|
|
399
|
+
if (outPath)
|
|
400
|
+
process.stdout.write(`\nsummary → ${outPath}\n`);
|
|
401
|
+
}
|
|
402
|
+
if (!hasFlag("no-ledger") && runId) {
|
|
403
|
+
try {
|
|
404
|
+
const assembled = assembleRun({ repo, activityLog: false });
|
|
405
|
+
await appendRunAnalyzed(assembled.deps.harness, runId, summary);
|
|
406
|
+
}
|
|
407
|
+
catch (e) {
|
|
408
|
+
process.stderr.write(`orchestrate analyze: run.analyzed ledger append failed: ${e.message}\n`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return 0;
|
|
412
|
+
}
|
|
413
|
+
async function doctor() {
|
|
414
|
+
const repo = flag("repo") ?? process.cwd();
|
|
415
|
+
const routesPath = flag("routes") ?? `${repo}/routes.toml`;
|
|
416
|
+
try {
|
|
417
|
+
const report = await runDoctor({
|
|
418
|
+
repo,
|
|
419
|
+
...(flag("config") ? { configPath: flag("config") } : {}),
|
|
420
|
+
routesPath,
|
|
421
|
+
...(flag("spool") ? { spoolDir: flag("spool") } : {}),
|
|
422
|
+
allowDirty: process.argv.includes("--allow-dirty"),
|
|
423
|
+
harness: resolveCommandSpec(process.env.HARNESS_BIN ?? "harness", repo),
|
|
424
|
+
agentHarness: resolveCommandSpec(process.env.AGENT_HARNESS_BIN ?? "agent-harness", repo),
|
|
425
|
+
});
|
|
426
|
+
if (process.argv.includes("--json")) {
|
|
427
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
for (const check of report.checks) {
|
|
431
|
+
const mark = check.status === "pass" ? "✓" : check.status === "warn" ? "⚠" : "✗";
|
|
432
|
+
process.stdout.write(` ${mark} ${check.id}: ${check.detail}\n`);
|
|
433
|
+
if (check.fix)
|
|
434
|
+
process.stdout.write(` fix: ${check.fix}\n`);
|
|
435
|
+
}
|
|
436
|
+
process.stdout.write(`\ndoctor: ${report.status}\n`);
|
|
437
|
+
}
|
|
438
|
+
return report.status === "ready" ? 0 : 1;
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
process.stderr.write(`orchestrate doctor: ${error.message}\n`);
|
|
442
|
+
return 2;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const cmd = process.argv[2];
|
|
446
|
+
if (cmd === "--help" || cmd === "-h" || cmd === undefined) {
|
|
447
|
+
process.stdout.write(USAGE);
|
|
448
|
+
process.exit(0);
|
|
449
|
+
}
|
|
450
|
+
else if (cmd === "demo") {
|
|
451
|
+
demo().then((c) => process.exit(c));
|
|
452
|
+
}
|
|
453
|
+
else if (cmd === "run") {
|
|
454
|
+
realRun().then((c) => process.exit(c));
|
|
455
|
+
}
|
|
456
|
+
else if (cmd === "doctor") {
|
|
457
|
+
doctor().then((c) => process.exit(c));
|
|
458
|
+
}
|
|
459
|
+
else if (cmd === "resume") {
|
|
460
|
+
resume().then((c) => process.exit(c));
|
|
461
|
+
}
|
|
462
|
+
else if (cmd === "sweep") {
|
|
463
|
+
sweep().then((c) => process.exit(c));
|
|
464
|
+
}
|
|
465
|
+
else if (cmd === "analyze") {
|
|
466
|
+
analyze().then((c) => process.exit(c));
|
|
467
|
+
}
|
|
468
|
+
else if (cmd === "report") {
|
|
469
|
+
process.exit(report());
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
process.stderr.write(`unknown command "${cmd}"\n\n${USAGE}`);
|
|
473
|
+
process.exit(2);
|
|
474
|
+
}
|
|
475
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config — `orchestration.json`, the run policy file (successor to the
|
|
3
|
+
* prototype's `drive.json`). It holds ONLY what is genuinely orchestration:
|
|
4
|
+
* concurrency, retries, run budgets, forge, and which runner implementations
|
|
5
|
+
* this installation has. Model/runner *selection* is agent-harness's
|
|
6
|
+
* `resolveRoute` — `drive`'s `models.rules` matcher is deliberately not ported.
|
|
7
|
+
*
|
|
8
|
+
* { forge, baseBranch, maxParallel, retryBudget,
|
|
9
|
+
* maxRunCost?, maxRunDuration?, runners: { enabled: [...] } }
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { DEFAULT_CONFIG } from "../loop/index.js";
|
|
13
|
+
export class ConfigError extends Error {
|
|
14
|
+
name = "ConfigError";
|
|
15
|
+
}
|
|
16
|
+
export function parseOrchestrationConfig(raw, source = "orchestration.json") {
|
|
17
|
+
if (raw === null || typeof raw !== "object") {
|
|
18
|
+
throw new ConfigError(`${source}: expected a JSON object`);
|
|
19
|
+
}
|
|
20
|
+
const f = raw;
|
|
21
|
+
if (f.forge !== undefined && f.forge !== "local" && f.forge !== "github") {
|
|
22
|
+
throw new ConfigError(`${source}: forge must be "local" or "github"`);
|
|
23
|
+
}
|
|
24
|
+
for (const k of ["maxParallel", "retryBudget", "maxRunCost", "maxRunDuration"]) {
|
|
25
|
+
if (f[k] !== undefined && (typeof f[k] !== "number" || f[k] < 0)) {
|
|
26
|
+
throw new ConfigError(`${source}: ${k} must be a non-negative number`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const enabled = f.runners?.enabled;
|
|
30
|
+
if (enabled !== undefined && (!Array.isArray(enabled) || enabled.some((e) => typeof e !== "string"))) {
|
|
31
|
+
throw new ConfigError(`${source}: runners.enabled must be an array of strings`);
|
|
32
|
+
}
|
|
33
|
+
const cfg = {
|
|
34
|
+
forge: f.forge ?? DEFAULT_CONFIG.forge,
|
|
35
|
+
baseBranch: f.baseBranch ?? DEFAULT_CONFIG.baseBranch,
|
|
36
|
+
maxParallel: f.maxParallel ?? DEFAULT_CONFIG.maxParallel,
|
|
37
|
+
retryBudget: f.retryBudget ?? DEFAULT_CONFIG.retryBudget,
|
|
38
|
+
systemToml: f.systemToml ?? DEFAULT_CONFIG.systemToml,
|
|
39
|
+
runnersEnabled: enabled ?? DEFAULT_CONFIG.runnersEnabled,
|
|
40
|
+
};
|
|
41
|
+
// optional run-level circuit breakers — absent means uncapped (an explicit
|
|
42
|
+
// operator choice, never a silent default).
|
|
43
|
+
if (f.maxRunCost !== undefined)
|
|
44
|
+
cfg.maxRunCost = f.maxRunCost;
|
|
45
|
+
if (f.maxRunDuration !== undefined)
|
|
46
|
+
cfg.maxRunDuration = f.maxRunDuration;
|
|
47
|
+
return cfg;
|
|
48
|
+
}
|
|
49
|
+
export function loadOrchestrationConfig(path) {
|
|
50
|
+
let text;
|
|
51
|
+
try {
|
|
52
|
+
text = readFileSync(path, "utf8");
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new ConfigError(`orchestration.json not found at ${path}`);
|
|
56
|
+
}
|
|
57
|
+
let raw;
|
|
58
|
+
try {
|
|
59
|
+
raw = JSON.parse(text);
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
throw new ConfigError(`${path}: invalid JSON — ${e.message}`);
|
|
63
|
+
}
|
|
64
|
+
return parseOrchestrationConfig(raw, path);
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=index.js.map
|