@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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +43 -0
  3. package/dist/build-identity.json +1 -0
  4. package/dist/cancellation.js +138 -0
  5. package/dist/cli.js +47 -0
  6. package/dist/dispatch.js +70 -0
  7. package/dist/help.js +47 -0
  8. package/dist/internal/agent/adapter/conformance.js +53 -0
  9. package/dist/internal/agent/adapter/hook.js +113 -0
  10. package/dist/internal/agent/adapter/install.js +30 -0
  11. package/dist/internal/agent/adapter/scope.js +55 -0
  12. package/dist/internal/agent/adapter/session.js +63 -0
  13. package/dist/internal/agent/adapter/settings.js +51 -0
  14. package/dist/internal/agent/cli.js +79 -0
  15. package/dist/internal/agent/evals/index.js +111 -0
  16. package/dist/internal/agent/index.js +15 -0
  17. package/dist/internal/agent/ledger-read.js +27 -0
  18. package/dist/internal/agent/routing/cli.js +104 -0
  19. package/dist/internal/agent/routing/index.js +123 -0
  20. package/dist/internal/agent/telemetry/index.js +97 -0
  21. package/dist/internal/orchestration/activity/index.js +111 -0
  22. package/dist/internal/orchestration/analyze/index.js +237 -0
  23. package/dist/internal/orchestration/cli.js +475 -0
  24. package/dist/internal/orchestration/config/index.js +66 -0
  25. package/dist/internal/orchestration/doctor/index.js +202 -0
  26. package/dist/internal/orchestration/forge/index.js +121 -0
  27. package/dist/internal/orchestration/git/index.js +181 -0
  28. package/dist/internal/orchestration/harness/cli.js +378 -0
  29. package/dist/internal/orchestration/harness/index.js +186 -0
  30. package/dist/internal/orchestration/harness/shim.js +182 -0
  31. package/dist/internal/orchestration/index.js +29 -0
  32. package/dist/internal/orchestration/loop/index.js +1107 -0
  33. package/dist/internal/orchestration/prompts/index.js +199 -0
  34. package/dist/internal/orchestration/report/index.js +125 -0
  35. package/dist/internal/orchestration/routes/index.js +85 -0
  36. package/dist/internal/orchestration/run/index.js +221 -0
  37. package/dist/internal/orchestration/runner/index.js +90 -0
  38. package/dist/internal/orchestration/runners/claude/index.js +246 -0
  39. package/dist/internal/orchestration/runners/claude/sdk.js +198 -0
  40. package/dist/internal/orchestration/runners/claude/wiring.js +98 -0
  41. package/dist/internal/orchestration/runners/codex/exec.js +114 -0
  42. package/dist/internal/orchestration/runners/codex/index.js +72 -0
  43. package/dist/internal/orchestration/runners/conformance/index.js +169 -0
  44. package/dist/internal/orchestration/runners/cursor/exec.js +531 -0
  45. package/dist/internal/orchestration/runners/cursor/index.js +184 -0
  46. package/dist/internal/orchestration/runners/stub/index.js +168 -0
  47. package/dist/internal/orchestration/runners/telemetry.js +36 -0
  48. package/dist/internal/orchestration/runstate/index.js +213 -0
  49. package/dist/internal/orchestration/runstate/publish.js +115 -0
  50. package/dist/internal/orchestration/worktree/index.js +89 -0
  51. package/dist/internal/system/adapters/index.js +19 -0
  52. package/dist/internal/system/agentops/index.js +16 -0
  53. package/dist/internal/system/agentops/scope.js +67 -0
  54. package/dist/internal/system/assets.js +45 -0
  55. package/dist/internal/system/board/cli.js +56 -0
  56. package/dist/internal/system/board/index.js +276 -0
  57. package/dist/internal/system/board/live.js +277 -0
  58. package/dist/internal/system/calibration/cli.js +45 -0
  59. package/dist/internal/system/calibration/index.js +141 -0
  60. package/dist/internal/system/cli.js +174 -0
  61. package/dist/internal/system/corpus/replay.js +79 -0
  62. package/dist/internal/system/digest/cli.js +23 -0
  63. package/dist/internal/system/digest/index.js +145 -0
  64. package/dist/internal/system/doors/classify.js +94 -0
  65. package/dist/internal/system/doors/cli.js +184 -0
  66. package/dist/internal/system/doors/index.js +249 -0
  67. package/dist/internal/system/doors/metrics.js +47 -0
  68. package/dist/internal/system/escapes/cli.js +45 -0
  69. package/dist/internal/system/escapes/index.js +81 -0
  70. package/dist/internal/system/explore/cli.js +77 -0
  71. package/dist/internal/system/explore/index.js +91 -0
  72. package/dist/internal/system/gates/cli.js +43 -0
  73. package/dist/internal/system/gates/detectors.js +55 -0
  74. package/dist/internal/system/gates/pre.js +51 -0
  75. package/dist/internal/system/hub/cli.js +51 -0
  76. package/dist/internal/system/hub/index.js +146 -0
  77. package/dist/internal/system/init/index.js +83 -0
  78. package/dist/internal/system/ledger/cli.js +54 -0
  79. package/dist/internal/system/ledger/index.js +176 -0
  80. package/dist/internal/system/manifest/index.js +207 -0
  81. package/dist/internal/system/provenance/index.js +13 -0
  82. package/dist/internal/system/report/index.js +15 -0
  83. package/dist/internal/system/runner/index.js +74 -0
  84. package/dist/internal/system/runstatus/cli.js +86 -0
  85. package/dist/internal/system/runstatus/index.js +168 -0
  86. package/dist/internal/system/sequencer/briefing.js +75 -0
  87. package/dist/internal/system/sequencer/cli.js +170 -0
  88. package/dist/internal/system/sequencer/index.js +156 -0
  89. package/dist/internal/system/spec/cli.js +110 -0
  90. package/dist/internal/system/spec/index.js +212 -0
  91. package/dist/internal/system/spec/materialize.js +111 -0
  92. package/dist/internal/system/spec/openspec-parse.js +83 -0
  93. package/dist/internal/system/spec/project.js +208 -0
  94. package/dist/internal/system/substrate/index.js +14 -0
  95. package/dist/internal/system/system/index.js +159 -0
  96. package/dist/internal/system/units/index.js +87 -0
  97. package/dist/internal/system/verify/cli.js +103 -0
  98. package/dist/internal/system/verify/declined-reasons.js +61 -0
  99. package/dist/internal/system/verify/index.js +188 -0
  100. package/dist/internal/system/verify/signing.js +79 -0
  101. package/dist/paths.js +50 -0
  102. package/dist/routes.js +93 -0
  103. package/dist/version.js +30 -0
  104. package/dist/win32-process-group.js +360 -0
  105. package/package.json +41 -0
  106. package/profiles/python/bindings.toml +59 -0
  107. package/profiles/terraform/bindings.toml +66 -0
  108. package/profiles/typescript/bindings.toml +64 -0
  109. package/profiles/typescript/smoke/README.md +8 -0
  110. package/schema/baseline.schema.json +26 -0
  111. package/schema/decision-classes.yaml +45 -0
  112. package/schema/envelope.schema.json +186 -0
  113. package/schema/ledger-event.schema.json +235 -0
  114. package/schema/manifest.schema.json +119 -0
  115. package/schema/routes.schema.json +55 -0
  116. package/schema/run-status.schema.json +162 -0
  117. package/schema/session-event.schema.json +74 -0
  118. package/schema/spec.schema.json +114 -0
  119. package/schema/system.schema.json +142 -0
  120. package/templates/ci/verify.yml +23 -0
  121. package/templates/target-kit/README.md +78 -0
  122. package/templates/target-kit/component.toml +20 -0
  123. package/templates/target-kit/orchestration.json +9 -0
  124. package/templates/target-kit/profiles/target/bindings.toml +6 -0
  125. package/templates/target-kit/routes.toml +5 -0
  126. package/templates/target-kit/specs/first-unit.yaml +11 -0
  127. 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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[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