@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,199 @@
|
|
|
1
|
+
function criteriaBlock(b) {
|
|
2
|
+
return b.acceptanceCriteria
|
|
3
|
+
.map((c, i) => {
|
|
4
|
+
const how = c.verifier
|
|
5
|
+
? `verifier: ${c.verifier}`
|
|
6
|
+
: `manual: ${c.manual ?? "(unspecified)"}`;
|
|
7
|
+
return ` ${i + 1}. ${c.statement}\n (${how})`;
|
|
8
|
+
})
|
|
9
|
+
.join("\n");
|
|
10
|
+
}
|
|
11
|
+
function factsBlock(b) {
|
|
12
|
+
const f = b.mergeFacts;
|
|
13
|
+
const on = Object.entries({
|
|
14
|
+
door: f.door,
|
|
15
|
+
"adds-dependency": f.addsDependency,
|
|
16
|
+
"schema-change": f.schemaChange,
|
|
17
|
+
security: f.security,
|
|
18
|
+
"gate-external": f.gateExternal,
|
|
19
|
+
"artifact-producer-change": f.artifactProducerChange,
|
|
20
|
+
})
|
|
21
|
+
.filter(([, v]) => v)
|
|
22
|
+
.map(([k, v]) => (v === true ? k : `${k}=${String(v)}`));
|
|
23
|
+
return on.length ? on.join(", ") : "none declared";
|
|
24
|
+
}
|
|
25
|
+
function retryBlock(retry) {
|
|
26
|
+
if (!retry)
|
|
27
|
+
return "";
|
|
28
|
+
return `
|
|
29
|
+
|
|
30
|
+
## RETRY (attempt ${retry.attempt}) — the previous attempt failed at: ${retry.source}
|
|
31
|
+
|
|
32
|
+
The branch ALREADY carries the previous attempt(s)' committed work — it was not
|
|
33
|
+
thrown away. Orient before doing anything else:
|
|
34
|
+
1. \`git log --oneline <base-branch>..HEAD\` and \`git diff --stat <base-branch>\`
|
|
35
|
+
— see what is already implemented and committed.
|
|
36
|
+
2. Re-read only the acceptance criteria the feedback below implicates.
|
|
37
|
+
|
|
38
|
+
Then work IN PLACE on the same branch and fix EXACTLY what the feedback says.
|
|
39
|
+
Do NOT reset or start over, do NOT re-audit criteria that already pass, and do
|
|
40
|
+
NOT re-explore the whole codebase from scratch — a retry is a targeted fix, not
|
|
41
|
+
a fresh implementation.
|
|
42
|
+
|
|
43
|
+
\`\`\`
|
|
44
|
+
${retry.evidence.trim()}
|
|
45
|
+
\`\`\`
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Implementer: make the change and stay in scope. The orchestrator runs the
|
|
50
|
+
* authoritative harness verification after the session; letting an agent run
|
|
51
|
+
* a bare verify can append a second envelope with inferred, incorrect identity.
|
|
52
|
+
*/
|
|
53
|
+
export function renderImplementerPrompt(b, retry) {
|
|
54
|
+
return `You are the IMPLEMENTER for unit ${b.unitId}${b.component ? ` (component: ${b.component})` : ""}.
|
|
55
|
+
|
|
56
|
+
## You are inside an already-running harness orchestrator
|
|
57
|
+
This session was dispatched by \`orchestrate run\` (orchestration-harness). You are
|
|
58
|
+
a nested worker for ONE unit — not the DAG driver. The briefing below is the
|
|
59
|
+
source of truth for this turn.
|
|
60
|
+
|
|
61
|
+
Do NOT open, follow, or invoke external orchestrate / unit / loop skills (for
|
|
62
|
+
example \`~/.claude/skills/orchestrate\`, wfo \`unit\` CLI workflows, or Cursor
|
|
63
|
+
\`/orchestrate\`). Do NOT claim units, open review PRs, merge, or advance other
|
|
64
|
+
units — the outer orchestrator owns claim → verify → land → next.
|
|
65
|
+
|
|
66
|
+
If repo conventions are unclear, prefer \`AGENTS.md\` in this workspace over any
|
|
67
|
+
personal skill.
|
|
68
|
+
|
|
69
|
+
## Intent
|
|
70
|
+
${b.intent}
|
|
71
|
+
|
|
72
|
+
## Acceptance criteria — implement to ALL of these
|
|
73
|
+
${criteriaBlock(b)}
|
|
74
|
+
|
|
75
|
+
## Scope — you may ONLY touch these paths
|
|
76
|
+
${b.scope.map((s) => ` - ${s}`).join("\n")}
|
|
77
|
+
|
|
78
|
+
A write outside this scope halts the unit. If the scope is too narrow to satisfy
|
|
79
|
+
the intent, STOP and run \`agent-harness escalate --reason "<why>" --wants "widen scope"\`
|
|
80
|
+
rather than working around it.
|
|
81
|
+
|
|
82
|
+
## Branch
|
|
83
|
+
Work on \`${b.branch}\` (already checked out). Commit with a \`Unit: ${b.unitId}\` trailer.
|
|
84
|
+
|
|
85
|
+
## Declared merge facts (informational — the orchestrator passes these to \`harness merge\`)
|
|
86
|
+
${factsBlock(b)}
|
|
87
|
+
|
|
88
|
+
## Definition of done
|
|
89
|
+
Implement every acceptance criterion, run any relevant ordinary project tests or
|
|
90
|
+
build checks, and commit the result. Do NOT run \`harness verify\`, \`harness merge\`,
|
|
91
|
+
\`harness claim\`, \`harness next\`, or other harness state-changing commands. The
|
|
92
|
+
orchestrator runs authoritative verification after your session. Do not stop until
|
|
93
|
+
the project checks pass or you have escalated.${retryBlock(retry)}
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Verifier: review the rebased diff against the acceptance criteria. READ-ONLY —
|
|
98
|
+
* the runner denies all write tools; a verifier that writes voids its verdict.
|
|
99
|
+
* Ends with a single machine-readable verdict line.
|
|
100
|
+
*/
|
|
101
|
+
export function renderVerifierPrompt(b, diff, envelopeRef) {
|
|
102
|
+
return `You are the VERIFIER for unit ${b.unitId}. You are READ-ONLY: you have no
|
|
103
|
+
write tools. Do not attempt to modify the tree — inspect and judge only.
|
|
104
|
+
|
|
105
|
+
Do NOT start servers, daemons, watchers, or any process that does not exit on
|
|
106
|
+
its own (e.g. \`harness board serve\`, \`npm run dev\`). Judge behaviour from the
|
|
107
|
+
code and from the unit's own test suite (\`npm test\`, which exits). If a
|
|
108
|
+
criterion genuinely needs a long-running process, launch it with a hard
|
|
109
|
+
timeout and kill it before you finish — a process you leave running hangs the
|
|
110
|
+
orchestrator on this step.
|
|
111
|
+
|
|
112
|
+
## You are inside an already-running harness orchestrator
|
|
113
|
+
This session was dispatched by \`orchestrate run\`. Judge only this unit's diff
|
|
114
|
+
against the criteria below. Do NOT follow external orchestrate / unit / loop
|
|
115
|
+
skills, and do not drive the DAG, open PRs, or merge.
|
|
116
|
+
|
|
117
|
+
## Intent
|
|
118
|
+
${b.intent}
|
|
119
|
+
|
|
120
|
+
## Acceptance criteria — the review spec, judge the diff against EACH
|
|
121
|
+
${criteriaBlock(b)}
|
|
122
|
+
${envelopeRef ? `\n## Verify envelope\n${envelopeRef}\n` : ""}
|
|
123
|
+
## The change under review (rebased onto the base branch)
|
|
124
|
+
\`\`\`diff
|
|
125
|
+
${diff.trim() || "(empty diff)"}
|
|
126
|
+
\`\`\`
|
|
127
|
+
|
|
128
|
+
## Your task
|
|
129
|
+
For each acceptance criterion, decide whether the diff satisfies it and say why.
|
|
130
|
+
Then emit EXACTLY ONE final line, nothing after it:
|
|
131
|
+
|
|
132
|
+
VERDICT: APPROVE
|
|
133
|
+
or
|
|
134
|
+
VERDICT: REJECT — <comma-separated reasons, each tied to a criterion>
|
|
135
|
+
|
|
136
|
+
Reject if any criterion is unmet, if the change exceeds the stated scope, or if
|
|
137
|
+
the tests do not actually exercise the new behaviour.
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Terse follow-up for a RESUMED implementer session (cursor-agent --resume) —
|
|
142
|
+
* the session already holds the full briefing and its own prior work; it only
|
|
143
|
+
* needs the failure and a reminder to stay narrow.
|
|
144
|
+
*/
|
|
145
|
+
export function renderRetryFollowup(retry) {
|
|
146
|
+
return `RETRY (attempt ${retry.attempt}). Your previous attempt failed at: ${retry.source}.
|
|
147
|
+
|
|
148
|
+
The branch is unchanged and still carries your commits — do NOT reset. Fix
|
|
149
|
+
EXACTLY the following, in place, then stop. Do not re-audit criteria that
|
|
150
|
+
already pass, do not re-explore the codebase.
|
|
151
|
+
|
|
152
|
+
\`\`\`
|
|
153
|
+
${retry.evidence.trim()}
|
|
154
|
+
\`\`\`
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Follow-up for a RESUMED verifier session — it holds its prior criterion-by-
|
|
159
|
+
* criterion analysis; a new implement attempt has landed, so re-judge against
|
|
160
|
+
* the updated diff and re-emit the verdict line.
|
|
161
|
+
*/
|
|
162
|
+
export function renderReviewFollowup(diff) {
|
|
163
|
+
return `RE-REVIEW: a new implement attempt landed since your last verdict. The
|
|
164
|
+
updated diff (rebased onto the base branch) follows. Re-judge each acceptance
|
|
165
|
+
criterion — reuse your prior analysis for parts that did not change — then emit
|
|
166
|
+
EXACTLY ONE final line: \`VERDICT: APPROVE\` or \`VERDICT: REJECT — <reasons>\`.
|
|
167
|
+
|
|
168
|
+
\`\`\`diff
|
|
169
|
+
${diff.trim() || "(empty diff)"}
|
|
170
|
+
\`\`\`
|
|
171
|
+
`;
|
|
172
|
+
}
|
|
173
|
+
/** Parse the verifier's final `VERDICT:` line. Absent / malformed → reject. */
|
|
174
|
+
export function parseVerdict(text) {
|
|
175
|
+
// Strip leading markdown emphasis / quote / list / heading markers and
|
|
176
|
+
// surrounding `**`/`__`/backticks — agents routinely bold the verdict line
|
|
177
|
+
// (`**VERDICT: APPROVE**`), and a strict `startsWith("VERDICT:")` would then
|
|
178
|
+
// miss it and reject a genuine pass.
|
|
179
|
+
const line = text
|
|
180
|
+
.split("\n")
|
|
181
|
+
.map((l) => l.trim().replace(/^[>*_#`\s-]+/, "").replace(/[*_`]+$/, "").trim())
|
|
182
|
+
.filter((l) => l.toUpperCase().startsWith("VERDICT:"))
|
|
183
|
+
.at(-1);
|
|
184
|
+
if (!line) {
|
|
185
|
+
return { approved: false, reasons: ["no VERDICT line in the verifier output"] };
|
|
186
|
+
}
|
|
187
|
+
const body = line.slice(line.indexOf(":") + 1).trim();
|
|
188
|
+
if (/^APPROVE\b/i.test(body))
|
|
189
|
+
return { approved: true, reasons: [] };
|
|
190
|
+
const dash = body.search(/[—-]/);
|
|
191
|
+
const reasonText = dash >= 0 ? body.slice(dash + 1).trim() : "";
|
|
192
|
+
return {
|
|
193
|
+
approved: false,
|
|
194
|
+
reasons: reasonText
|
|
195
|
+
? reasonText.split(",").map((r) => r.trim()).filter(Boolean)
|
|
196
|
+
: ["verifier rejected without stated reasons"],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const esc = (s) => String(s).replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" })[c]);
|
|
2
|
+
function laneRow(l) {
|
|
3
|
+
const outcome = l.settled
|
|
4
|
+
? l.step === "land"
|
|
5
|
+
? "merged"
|
|
6
|
+
: "settled"
|
|
7
|
+
: "in-flight";
|
|
8
|
+
const prov = [
|
|
9
|
+
l.envelopeHash
|
|
10
|
+
? `<span class="dim">env</span> <code>${esc(l.envelopeHash.slice(0, 12))}…</code>`
|
|
11
|
+
: "",
|
|
12
|
+
l.doorId ? `<span class="chip warn">door ${esc(l.doorId)}</span>` : "",
|
|
13
|
+
]
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.join(" ") || `<span class="dim">—</span>`;
|
|
16
|
+
return `<tr>
|
|
17
|
+
<td><code>${esc(l.unitId)}</code></td>
|
|
18
|
+
<td>${esc(outcome)} <span class="dim">(${esc(l.step)})</span></td>
|
|
19
|
+
<td>${esc(l.attempt)}</td>
|
|
20
|
+
<td>$${l.cost.toFixed(4)}</td>
|
|
21
|
+
<td><span class="chip">${esc(l.runner)}</span> ${esc(l.model)}</td>
|
|
22
|
+
<td class="dim">${esc(l.emitter)}</td>
|
|
23
|
+
<td>${prov}</td>
|
|
24
|
+
</tr>`;
|
|
25
|
+
}
|
|
26
|
+
function findingRow(f) {
|
|
27
|
+
return `<tr>
|
|
28
|
+
<td><code>${esc(f.id)}</code></td>
|
|
29
|
+
<td><code>${esc(f.unitId)}</code></td>
|
|
30
|
+
<td><span class="chip ${f.severity === "action" ? "warn" : ""}">${esc(f.severity)}</span></td>
|
|
31
|
+
<td>${esc(f.evidence.length)}</td>
|
|
32
|
+
</tr>`;
|
|
33
|
+
}
|
|
34
|
+
/** Only rendered when `analysis` has something to show — a single-run report
|
|
35
|
+
* has no "insufficient data" state to represent, unlike a board panel. */
|
|
36
|
+
function analysisSection(analysis) {
|
|
37
|
+
if (!analysis || (analysis.units.length === 0 && analysis.findings.length === 0))
|
|
38
|
+
return "";
|
|
39
|
+
const table = analysis.findings.length
|
|
40
|
+
? `<table>
|
|
41
|
+
<thead><tr><th>rule</th><th>unit</th><th>severity</th><th>evidence</th></tr></thead>
|
|
42
|
+
<tbody>${analysis.findings.map(findingRow).join("")}</tbody>
|
|
43
|
+
</table>`
|
|
44
|
+
: `<div class="dim">no findings</div>`;
|
|
45
|
+
return `\n<h2>Analysis</h2>\n<div class="panel">${table}</div>\n`;
|
|
46
|
+
}
|
|
47
|
+
export function renderRunReport(state, analysis) {
|
|
48
|
+
const json = JSON.stringify(state, null, 2).replace(/</g, "\\u003c");
|
|
49
|
+
const totalCost = state.lanes.reduce((s, l) => s + l.cost, 0);
|
|
50
|
+
const dur = state.heartbeatAt && state.startedAt
|
|
51
|
+
? `${Math.max(0, Math.round((state.heartbeatAt - state.startedAt) / 1000))}s`
|
|
52
|
+
: "—";
|
|
53
|
+
const lanes = state.lanes.length
|
|
54
|
+
? `<table>
|
|
55
|
+
<thead><tr><th>unit</th><th>outcome</th><th>attempts</th><th>cost</th><th>runner / model</th><th>telemetry</th><th>provenance</th></tr></thead>
|
|
56
|
+
<tbody>${state.lanes.map(laneRow).join("")}</tbody>
|
|
57
|
+
</table>`
|
|
58
|
+
: `<div class="dim">no lanes ran</div>`;
|
|
59
|
+
const needsYou = state.needsYou.length
|
|
60
|
+
? `<ul>${state.needsYou
|
|
61
|
+
.map((n) => `<li><span class="chip warn">${esc(n.kind)}</span> ${esc(n.unitId ?? "")} — ${esc(n.message)}<br><code>${esc(n.unblock)}</code></li>`)
|
|
62
|
+
.join("")}</ul>`
|
|
63
|
+
: `<div class="dim">none — the run needs nothing from you</div>`;
|
|
64
|
+
const parked = state.parked.length
|
|
65
|
+
? state.parked.map((p) => `<span class="chip">${esc(p.unitId)} · ${esc(p.reason)}</span>`).join(" ")
|
|
66
|
+
: `<span class="dim">none</span>`;
|
|
67
|
+
const notes = state.notes.length
|
|
68
|
+
? `<ul>${state.notes.map((n) => `<li><code>${esc(n.unitId)}</code> — ${esc(n.note)}</li>`).join("")}</ul>`
|
|
69
|
+
: `<div class="dim">none</div>`;
|
|
70
|
+
return `<!doctype html>
|
|
71
|
+
<html lang="en"><head><meta charset="utf-8"><title>orchestrate run — ${esc(state.systemId || "run")}</title>
|
|
72
|
+
<style>
|
|
73
|
+
:root{color-scheme:light dark}
|
|
74
|
+
body{font:14px/1.5 ui-sans-serif,system-ui,sans-serif;margin:0;padding:24px;background:#0e1216;color:#e5e9ee}
|
|
75
|
+
h1{font-size:1.3rem;margin:0 0 4px} h2{font-size:.8rem;letter-spacing:.08em;text-transform:uppercase;color:#9aa4b1;margin:22px 0 10px}
|
|
76
|
+
.sub{color:#9aa4b1;margin-bottom:18px;font-size:.85rem}
|
|
77
|
+
.panel{border:1px solid #2a333d;border-radius:10px;background:#161c22;padding:14px 16px;margin-bottom:14px}
|
|
78
|
+
.counts{display:flex;flex-wrap:wrap;gap:8px}
|
|
79
|
+
.stat{border:1px solid #2a333d;border-radius:8px;background:#12181e;padding:8px 12px}
|
|
80
|
+
.stat b{display:block;font-size:1.2rem}
|
|
81
|
+
table{border-collapse:collapse;width:100%} th,td{text-align:left;padding:6px 10px;border-bottom:1px solid #222a33;font-size:.85rem}
|
|
82
|
+
th{color:#9aa4b1;font-weight:600;text-transform:uppercase;font-size:.7rem;letter-spacing:.06em}
|
|
83
|
+
code{font-family:ui-monospace,monospace;font-size:.82em;color:#5bb8d4}
|
|
84
|
+
.dim{color:#6b7583} .chip{display:inline-block;font-family:ui-monospace,monospace;font-size:.72rem;border:1px solid #3b4653;border-radius:99px;padding:1px 7px}
|
|
85
|
+
.chip.warn{border-color:#7a5b2e;color:#e0b062}
|
|
86
|
+
.outcome{font-size:1rem;padding:2px 10px;border-radius:6px}
|
|
87
|
+
.outcome.completed{background:#1c3323;color:#7fdca0} .outcome.halted{background:#3a2626;color:#e79a9a} .outcome.crashed{background:#3a2626;color:#e79a9a}
|
|
88
|
+
</style></head>
|
|
89
|
+
<body>
|
|
90
|
+
<h1>orchestrate run</h1>
|
|
91
|
+
<div class="sub">
|
|
92
|
+
system <code>${esc(state.systemId || "—")}</code> ·
|
|
93
|
+
run <code>${esc(state.runId || "—")}</code> ·
|
|
94
|
+
machine <code>${esc(state.machineId || "—")}</code> ·
|
|
95
|
+
spec <code>${esc(state.specRef || "—")}</code> ·
|
|
96
|
+
${dur} ·
|
|
97
|
+
<span class="outcome ${esc(state.outcome ?? "crashed")}">${esc(state.outcome ?? "crashed (no terminal frame)")}</span>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div class="panel counts">
|
|
101
|
+
<div class="stat"><b>${state.counts.merged}</b>merged / ${state.dagSize}</div>
|
|
102
|
+
<div class="stat"><b>${state.counts.escalated}</b>escalated</div>
|
|
103
|
+
<div class="stat"><b>${state.counts.parked}</b>parked</div>
|
|
104
|
+
<div class="stat"><b>${state.counts.inFlight}</b>in-flight</div>
|
|
105
|
+
<div class="stat"><b>${state.counts.needsYou}</b>needs you</div>
|
|
106
|
+
<div class="stat"><b>$${totalCost.toFixed(4)}</b>run cost</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<h2>Lanes</h2>
|
|
110
|
+
<div class="panel">${lanes}</div>
|
|
111
|
+
|
|
112
|
+
<h2>Needs you</h2>
|
|
113
|
+
<div class="panel">${needsYou}</div>
|
|
114
|
+
|
|
115
|
+
<h2>Parked</h2>
|
|
116
|
+
<div class="panel">${parked}</div>
|
|
117
|
+
|
|
118
|
+
<h2>Notes</h2>
|
|
119
|
+
<div class="panel">${notes}</div>
|
|
120
|
+
${analysisSection(analysis)}
|
|
121
|
+
<script id="runstate" type="application/json">${json}</script>
|
|
122
|
+
</body></html>
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* routes — reads the routing decision for a unit. Routing *policy* (which
|
|
3
|
+
* model / config / mode / runner a unit gets) lives in agent-harness's
|
|
4
|
+
* `resolveRoute` (routes.toml). This product only reads the decision and obeys
|
|
5
|
+
* it (mission: "Routing policy … is out of scope — permanently").
|
|
6
|
+
*
|
|
7
|
+
* The real source is `agent-harness route <unit-id> --profile <p> --json →
|
|
8
|
+
* {runner, model, config, mode, until}` (AH P7.0). Portfolio rule 1 forbids
|
|
9
|
+
* importing `resolveRoute` across `repos/`, so `CliRouteSource` shells the
|
|
10
|
+
* subcommand. The dry loop (M1) still runs against `FakeRouteSource`; M2 wires
|
|
11
|
+
* `CliRouteSource` into the real run.
|
|
12
|
+
*
|
|
13
|
+
* `agent-harness route`'s `mode` is `standard | propose-only`; this product's
|
|
14
|
+
* `Policy.mode` is `auto | propose-only` — `standard` maps to `auto`. Until AH
|
|
15
|
+
* P7.1 adds a `runner` field, the CLI always reports `runner: "claude"`.
|
|
16
|
+
*/
|
|
17
|
+
import { callProcess } from "../harness/cli.js";
|
|
18
|
+
const DEFAULT_POLICY = {
|
|
19
|
+
runner: "claude",
|
|
20
|
+
model: "claude-sonnet-5",
|
|
21
|
+
mode: "auto",
|
|
22
|
+
};
|
|
23
|
+
/** Map the CLI's `{runner, model, config, mode, until}` onto this product's `Policy`. */
|
|
24
|
+
export function mapRouteCliOutput(out) {
|
|
25
|
+
const policy = {
|
|
26
|
+
runner: out.runner || "claude",
|
|
27
|
+
model: out.model,
|
|
28
|
+
mode: out.mode === "propose-only" ? "propose-only" : "auto",
|
|
29
|
+
};
|
|
30
|
+
if (out.config !== undefined)
|
|
31
|
+
policy.config = { preset: out.config };
|
|
32
|
+
if (out.until !== undefined)
|
|
33
|
+
policy.until = out.until;
|
|
34
|
+
return policy;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Reads routing by shelling `agent-harness route` — the only legal consumption
|
|
38
|
+
* path (portfolio rule 1). One process per unit; the loop resolves each unit
|
|
39
|
+
* once at plan time.
|
|
40
|
+
*/
|
|
41
|
+
export class CliRouteSource {
|
|
42
|
+
opts;
|
|
43
|
+
constructor(opts) {
|
|
44
|
+
this.opts = opts;
|
|
45
|
+
}
|
|
46
|
+
async resolve(unitId, component) {
|
|
47
|
+
const profile = this.opts.profileForComponent(component);
|
|
48
|
+
if (!profile) {
|
|
49
|
+
throw new Error(`CliRouteSource: no profile for ${unitId} (component ${component ?? "?"}) — is it declared in system.toml?`);
|
|
50
|
+
}
|
|
51
|
+
const args = [
|
|
52
|
+
"route",
|
|
53
|
+
unitId,
|
|
54
|
+
"--profile",
|
|
55
|
+
profile,
|
|
56
|
+
"--json",
|
|
57
|
+
...(this.opts.routesPath ? ["--routes", this.opts.routesPath] : []),
|
|
58
|
+
...(this.opts.ledgerDir ? ["--ledger", this.opts.ledgerDir] : []),
|
|
59
|
+
];
|
|
60
|
+
const result = await (this.opts.processCall ?? callProcess)({
|
|
61
|
+
command: this.opts.bin,
|
|
62
|
+
args: [...(this.opts.binArgs ?? []), ...args],
|
|
63
|
+
cwd: this.opts.cwd,
|
|
64
|
+
});
|
|
65
|
+
if (result.code !== 0) {
|
|
66
|
+
throw new Error(`agent-harness route ${unitId} failed (exit ${result.code}): ${(result.stderr || result.stdout).trim()}`);
|
|
67
|
+
}
|
|
68
|
+
return mapRouteCliOutput(JSON.parse(result.stdout));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export class FakeRouteSource {
|
|
72
|
+
base;
|
|
73
|
+
overrides = new Map();
|
|
74
|
+
constructor(base = DEFAULT_POLICY) {
|
|
75
|
+
this.base = base;
|
|
76
|
+
}
|
|
77
|
+
set(unitId, policy) {
|
|
78
|
+
this.overrides.set(unitId, policy);
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
async resolve(unitId, _component) {
|
|
82
|
+
return { ...this.base, ...this.overrides.get(unitId) };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run — assembles a real `LoopDeps` from a driven repo + `orchestration.json`,
|
|
3
|
+
* wiring the CLI-backed implementations: `CliHarness` (harness), `CliRouteSource`
|
|
4
|
+
* (agent-harness route), `ShellGit`, `LocalForge` / `GithubForge`, and one
|
|
5
|
+
* `ClaudeRunner` per enabled `claude` runner. This is the seam between the pure
|
|
6
|
+
* loop and the outside world; `orchestrate run` is a thin shell over it.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { hostname } from "node:os";
|
|
10
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
11
|
+
import { parse as parseToml } from "smol-toml";
|
|
12
|
+
import { loadOrchestrationConfig } from "../config/index.js";
|
|
13
|
+
import { LocalForge, GithubForge } from "../forge/index.js";
|
|
14
|
+
import { ShellGit } from "../git/index.js";
|
|
15
|
+
import { CliHarness, resolveCommandSpec } from "../harness/cli.js";
|
|
16
|
+
import { ActivityLogWriter } from "../activity/index.js";
|
|
17
|
+
import { CliRouteSource } from "../routes/index.js";
|
|
18
|
+
import { RunLog } from "../runstate/index.js";
|
|
19
|
+
import { CliRunPublisher } from "../runstate/publish.js";
|
|
20
|
+
import { ClaudeRunner } from "../runners/claude/index.js";
|
|
21
|
+
import { CodexRunner } from "../runners/codex/index.js";
|
|
22
|
+
import { CursorRunner } from "../runners/cursor/index.js";
|
|
23
|
+
import { GitWorktreeManager } from "../worktree/index.js";
|
|
24
|
+
/** component id → absolute root + profile, parsed from Contract C. */
|
|
25
|
+
export function loadComponentDescriptors(systemTomlPath) {
|
|
26
|
+
const raw = parseToml(readFileSync(systemTomlPath, "utf8"));
|
|
27
|
+
const out = new Map();
|
|
28
|
+
for (const c of raw.component ?? []) {
|
|
29
|
+
if (typeof c.id !== "string" || typeof c.profile !== "string")
|
|
30
|
+
continue;
|
|
31
|
+
const declaredRoot = c.root ?? c.repo;
|
|
32
|
+
if (typeof declaredRoot !== "string")
|
|
33
|
+
continue;
|
|
34
|
+
out.set(c.id, {
|
|
35
|
+
id: c.id,
|
|
36
|
+
root: resolve(dirname(systemTomlPath), declaredRoot),
|
|
37
|
+
profile: c.profile,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/** Backward-compatible profile view used by routing. */
|
|
43
|
+
export function loadComponentProfiles(systemTomlPath) {
|
|
44
|
+
return new Map([...loadComponentDescriptors(systemTomlPath)].map(([id, descriptor]) => [id, descriptor.profile]));
|
|
45
|
+
}
|
|
46
|
+
export function assembleRun(opts) {
|
|
47
|
+
const repo = resolve(opts.repo);
|
|
48
|
+
const runId = `run-${Date.now().toString(36)}`;
|
|
49
|
+
const assembledAt = Date.now();
|
|
50
|
+
const systemId = basename(repo);
|
|
51
|
+
const configPath = opts.configPath ?? join(repo, "orchestration.json");
|
|
52
|
+
const config = loadOrchestrationConfig(configPath);
|
|
53
|
+
const harnessInput = opts.harnessBin ?? process.env.HARNESS_BIN ?? "harness";
|
|
54
|
+
const agentHarnessInput = opts.agentHarnessBin ?? process.env.AGENT_HARNESS_BIN ?? "agent-harness";
|
|
55
|
+
const harnessCommand = opts.harnessBinArgs
|
|
56
|
+
? { command: harnessInput, args: opts.harnessBinArgs }
|
|
57
|
+
: resolveCommandSpec(harnessInput, repo);
|
|
58
|
+
const agentHarnessCommand = opts.agentHarnessBinArgs
|
|
59
|
+
? { command: agentHarnessInput, args: opts.agentHarnessBinArgs }
|
|
60
|
+
: resolveCommandSpec(agentHarnessInput, repo);
|
|
61
|
+
const ledgerDir = opts.ledgerDir ?? join(repo, ".harness", "ledger");
|
|
62
|
+
const specsDir = opts.specsDir ?? join(repo, "specs");
|
|
63
|
+
// discover the driven project's system.toml — ring-2 target; absent = brownfield.
|
|
64
|
+
const systemTomlPath = join(repo, "system.toml");
|
|
65
|
+
const hasSystemToml = existsSync(systemTomlPath);
|
|
66
|
+
config.systemToml = hasSystemToml ? systemTomlPath : null;
|
|
67
|
+
const components = hasSystemToml
|
|
68
|
+
? loadComponentDescriptors(systemTomlPath)
|
|
69
|
+
: new Map();
|
|
70
|
+
const profiles = new Map([...components].map(([id, descriptor]) => [id, descriptor.profile]));
|
|
71
|
+
const harness = new CliHarness({
|
|
72
|
+
bin: harnessCommand.command,
|
|
73
|
+
binArgs: harnessCommand.args,
|
|
74
|
+
cwd: repo,
|
|
75
|
+
ledgerDir,
|
|
76
|
+
specsDir,
|
|
77
|
+
profilesDir: join(repo, "profiles"),
|
|
78
|
+
components,
|
|
79
|
+
systemId,
|
|
80
|
+
actorId: runId,
|
|
81
|
+
...(config.systemToml ? { systemToml: config.systemToml } : {}),
|
|
82
|
+
});
|
|
83
|
+
const routes = new CliRouteSource({
|
|
84
|
+
bin: agentHarnessCommand.command,
|
|
85
|
+
binArgs: agentHarnessCommand.args,
|
|
86
|
+
cwd: repo,
|
|
87
|
+
...(opts.routesPath ? { routesPath: opts.routesPath } : {}),
|
|
88
|
+
ledgerDir,
|
|
89
|
+
profileForComponent: (component) => component ? profiles.get(component) : undefined,
|
|
90
|
+
});
|
|
91
|
+
const git = new ShellGit({ cwd: repo });
|
|
92
|
+
const workspaces = new GitWorktreeManager({
|
|
93
|
+
controlRepo: repo,
|
|
94
|
+
root: opts.worktreeRoot ?? join(repo, ".harness", "worktrees"),
|
|
95
|
+
});
|
|
96
|
+
const forge = config.forge === "github" ? new GithubForge({ cwd: repo }) : new LocalForge(git);
|
|
97
|
+
const log = new RunLog();
|
|
98
|
+
// Retry feedback the loop recorded as `lane.retry` — thread the latest one
|
|
99
|
+
// back into the next implementer session so it fixes exactly what failed
|
|
100
|
+
// instead of re-exploring from the plain briefing (analyze/index.ts flagged
|
|
101
|
+
// this as unwired). The branch already carries the prior attempt's commits.
|
|
102
|
+
const retryFor = (unitId) => {
|
|
103
|
+
for (let i = log.all().length - 1; i >= 0; i--) {
|
|
104
|
+
const e = log.all()[i];
|
|
105
|
+
if (e.t === "lane.retry" && e.unitId === unitId) {
|
|
106
|
+
return {
|
|
107
|
+
attempt: e.attempt,
|
|
108
|
+
source: "verification / review of the previous attempt",
|
|
109
|
+
evidence: e.evidence,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
};
|
|
115
|
+
// Per-run session store: a retry / re-review resumes the prior cursor-agent
|
|
116
|
+
// conversation (same unit + kind, same model) instead of re-briefing from
|
|
117
|
+
// scratch. Ids are process-local — they do not survive an `orchestrate run`
|
|
118
|
+
// restart, and the fallback (fresh session) is always safe.
|
|
119
|
+
const sessions = new Map();
|
|
120
|
+
const sessionKey = (unitId, kind) => `${unitId}:${kind}`;
|
|
121
|
+
const resumeFor = (unitId, kind) => sessions.get(sessionKey(unitId, kind));
|
|
122
|
+
const recordSession = (unitId, kind, id, model) => {
|
|
123
|
+
sessions.set(sessionKey(unitId, kind), { id, model });
|
|
124
|
+
};
|
|
125
|
+
// one runner per implementation this installation enables. v0 only ships
|
|
126
|
+
// `claude`; a route to any other enabled name that has no impl here is caught
|
|
127
|
+
// in step() (config-error halt).
|
|
128
|
+
const runners = new Map();
|
|
129
|
+
for (const name of config.runnersEnabled) {
|
|
130
|
+
if (name === "claude") {
|
|
131
|
+
runners.set("claude", new ClaudeRunner({
|
|
132
|
+
cwd: repo,
|
|
133
|
+
harnessBin: harnessCommand.command,
|
|
134
|
+
harnessBinArgs: harnessCommand.args,
|
|
135
|
+
agentHarnessBin: agentHarnessCommand.command,
|
|
136
|
+
agentHarnessBinArgs: agentHarnessCommand.args,
|
|
137
|
+
ledgerDir,
|
|
138
|
+
systemId,
|
|
139
|
+
sessionPrefix: runId,
|
|
140
|
+
...(config.maxRunCost !== undefined
|
|
141
|
+
? { maxRunCost: config.maxRunCost }
|
|
142
|
+
: {}),
|
|
143
|
+
...(config.maxRunDuration !== undefined
|
|
144
|
+
? { deadlineAt: assembledAt + config.maxRunDuration }
|
|
145
|
+
: {}),
|
|
146
|
+
diffFor: (unitId) => git.diffText(`unit/${unitId}`, config.baseBranch),
|
|
147
|
+
retryFor,
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
else if (name === "codex") {
|
|
151
|
+
runners.set("codex", new CodexRunner({
|
|
152
|
+
cwd: repo,
|
|
153
|
+
harnessBin: harnessCommand.command,
|
|
154
|
+
...(config.maxRunDuration !== undefined
|
|
155
|
+
? { deadlineAt: assembledAt + config.maxRunDuration }
|
|
156
|
+
: {}),
|
|
157
|
+
diffFor: (unitId) => git.diffText(`unit/${unitId}`, config.baseBranch),
|
|
158
|
+
retryFor,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
else if (name === "cursor") {
|
|
162
|
+
runners.set("cursor", new CursorRunner({
|
|
163
|
+
resumeFor,
|
|
164
|
+
recordSession,
|
|
165
|
+
cwd: repo,
|
|
166
|
+
harnessBin: harnessCommand.command,
|
|
167
|
+
...(config.maxRunDuration !== undefined
|
|
168
|
+
? { deadlineAt: assembledAt + config.maxRunDuration }
|
|
169
|
+
: {}),
|
|
170
|
+
...(opts.watch ? { watchStderr: true } : {}),
|
|
171
|
+
diffFor: (unitId) => git.diffText(`unit/${unitId}`, config.baseBranch),
|
|
172
|
+
retryFor,
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const machineId = process.env.HARNESS_MACHINE_ID ?? safeHostname();
|
|
177
|
+
const deps = {
|
|
178
|
+
harness,
|
|
179
|
+
routes,
|
|
180
|
+
runners,
|
|
181
|
+
git,
|
|
182
|
+
workspaces,
|
|
183
|
+
forge,
|
|
184
|
+
config,
|
|
185
|
+
log,
|
|
186
|
+
runId,
|
|
187
|
+
machineId,
|
|
188
|
+
...(opts.onlyPrefix ? { onlyPrefix: opts.onlyPrefix } : {}),
|
|
189
|
+
};
|
|
190
|
+
// Capture is on by default and NOT coupled to `--spool` — whether the board
|
|
191
|
+
// is running has nothing to do with whether the operator wants a durable
|
|
192
|
+
// record afterwards (plan doc, "Default on/off" — settled).
|
|
193
|
+
if (opts.activityLog !== false) {
|
|
194
|
+
const runsRoot = opts.activityLogDir ?? join(repo, ".harness", "runs");
|
|
195
|
+
deps.activityLog = new ActivityLogWriter({
|
|
196
|
+
path: join(runsRoot, runId, "activity.jsonl"),
|
|
197
|
+
...(opts.resumesRunId ? { resumes: opts.resumesRunId } : {}),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
// D1 — publish the run-status feed only when a hub spool is given.
|
|
201
|
+
if (opts.spoolDir) {
|
|
202
|
+
deps.publisher = new CliRunPublisher({
|
|
203
|
+
harnessBin: harnessCommand.command,
|
|
204
|
+
harnessBinArgs: harnessCommand.args,
|
|
205
|
+
cwd: repo,
|
|
206
|
+
spoolDir: opts.spoolDir,
|
|
207
|
+
machineId,
|
|
208
|
+
log,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return { deps, runId, machineId, systemId, specRef: specsDir };
|
|
212
|
+
}
|
|
213
|
+
function safeHostname() {
|
|
214
|
+
try {
|
|
215
|
+
return hostname() || "local";
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return "local";
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=index.js.map
|