@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,90 @@
1
+ export const ACTIVITY_LINE_MAX = 160;
2
+ export function clampActivityLine(line) {
3
+ return line.length <= ACTIVITY_LINE_MAX ? line : line.slice(0, ACTIVITY_LINE_MAX);
4
+ }
5
+ export class FakeRunner {
6
+ name;
7
+ caps;
8
+ defaultCost;
9
+ /** keyed `${unitId}:${kind}` (or `${unitId}:${kind}:${attempt}`) → outcome. */
10
+ script = new Map();
11
+ calls = [];
12
+ attempts = new Map();
13
+ constructor(opts = {}) {
14
+ this.name = opts.name ?? "fake";
15
+ this.caps = opts.capabilities ?? { scopeHook: true, sessionTelemetry: true };
16
+ this.defaultCost = opts.defaultCost ?? 1;
17
+ }
18
+ capabilities() {
19
+ return this.caps;
20
+ }
21
+ /** script an outcome. Attempt-scoped key (`unit:kind:2`) beats the loose key. */
22
+ on(unitId, kind, outcome, attempt) {
23
+ const key = attempt ? `${unitId}:${kind}:${attempt}` : `${unitId}:${kind}`;
24
+ this.script.set(key, outcome);
25
+ return this;
26
+ }
27
+ async run(briefing, _policy, kind, _workspace, _onActivity) {
28
+ const attemptKey = `${briefing.unitId}:${kind}`;
29
+ const attempt = (this.attempts.get(attemptKey) ?? 0) + 1;
30
+ this.attempts.set(attemptKey, attempt);
31
+ this.calls.push({ unitId: briefing.unitId, kind, attempt });
32
+ const outcome = this.script.get(`${briefing.unitId}:${kind}:${attempt}`) ??
33
+ this.script.get(`${briefing.unitId}:${kind}`) ??
34
+ { kind: "ok" };
35
+ const cost = ("cost" in outcome && outcome.cost) || this.defaultCost;
36
+ const inScopeFile = briefing.scope[0]?.replace(/\*+.*$/, "") + "file.ts";
37
+ switch (outcome.kind) {
38
+ case "ok":
39
+ // a verifier is read-only — it touches nothing and returns a verdict.
40
+ if (kind === "verifier") {
41
+ return {
42
+ ok: true,
43
+ filesTouched: outcome.filesTouched ?? [],
44
+ transcriptRef: `transcript://${briefing.unitId}/verifier/${attempt}`,
45
+ cost,
46
+ verdict: { approved: true, reasons: ["meets acceptance criteria"] },
47
+ };
48
+ }
49
+ return {
50
+ ok: true,
51
+ commitSha: `sha-${briefing.unitId}-implementer-${attempt}`,
52
+ filesTouched: outcome.filesTouched ?? [inScopeFile],
53
+ transcriptRef: `transcript://${briefing.unitId}/implementer/${attempt}`,
54
+ cost,
55
+ };
56
+ case "fail":
57
+ return {
58
+ ok: false,
59
+ filesTouched: outcome.filesTouched ?? [inScopeFile],
60
+ transcriptRef: `transcript://${briefing.unitId}/${kind}/${attempt}`,
61
+ cost,
62
+ };
63
+ case "escalate":
64
+ return {
65
+ ok: false,
66
+ filesTouched: [],
67
+ transcriptRef: `transcript://${briefing.unitId}/${kind}/${attempt}`,
68
+ cost,
69
+ escalation: { reason: outcome.reason, wants: outcome.wants },
70
+ };
71
+ case "reject":
72
+ return {
73
+ ok: true,
74
+ filesTouched: [],
75
+ transcriptRef: `transcript://${briefing.unitId}/${kind}/${attempt}`,
76
+ cost,
77
+ verdict: { approved: false, reasons: outcome.reasons },
78
+ };
79
+ case "scope-violation":
80
+ return {
81
+ ok: true,
82
+ commitSha: `sha-${briefing.unitId}-${kind}-${attempt}`,
83
+ filesTouched: outcome.filesTouched,
84
+ transcriptRef: `transcript://${briefing.unitId}/${kind}/${attempt}`,
85
+ cost,
86
+ };
87
+ }
88
+ }
89
+ }
90
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,246 @@
1
+ import { parseVerdict, renderImplementerPrompt, renderVerifierPrompt, } from "../../prompts/index.js";
2
+ import { runClaudeSession } from "./sdk.js";
3
+ import { claudeEnv, ensureAgentHarnessHook, emitClaudeSessionTelemetry, writeCurrentUnit, } from "./wiring.js";
4
+ /** write tools the implementer may use; the verifier gets NONE of these. */
5
+ const WRITE_TOOLS = ["Write", "Edit", "MultiEdit", "NotebookEdit"];
6
+ /** read-only tool set for verifier sessions. */
7
+ const VERIFIER_TOOLS = ["Read", "Grep", "Glob", "Bash"];
8
+ export class ClaudeRunner {
9
+ opts;
10
+ name = "claude";
11
+ sessionSequence = 0;
12
+ spentUsd = 0;
13
+ reservedUsd = 0;
14
+ constructor(opts) {
15
+ this.opts = opts;
16
+ }
17
+ capabilities() {
18
+ return { scopeHook: true, sessionTelemetry: true };
19
+ }
20
+ async run(briefing, policy, kind, workspace, onActivity) {
21
+ const cwd = workspace?.path ?? this.opts.cwd;
22
+ const limits = this.reserveLimits();
23
+ if (!limits) {
24
+ return {
25
+ ok: false,
26
+ filesTouched: [],
27
+ cost: 0,
28
+ telemetryError: "Claude session was not dispatched: run cost or duration budget exhausted",
29
+ };
30
+ }
31
+ const session = `${this.opts.sessionPrefix ?? "claude"}-${briefing.unitId}-${kind}-${++this.sessionSequence}`;
32
+ // wire agent-harness under the session (ring-0 scope + session.* telemetry).
33
+ writeCurrentUnit({
34
+ cwd,
35
+ briefing,
36
+ session,
37
+ harnessBin: this.opts.harnessBin,
38
+ agentHarnessBin: this.opts.agentHarnessBin,
39
+ });
40
+ ensureAgentHarnessHook({
41
+ cwd,
42
+ briefing,
43
+ session,
44
+ harnessBin: this.opts.harnessBin,
45
+ agentHarnessBin: this.opts.agentHarnessBin,
46
+ ...(this.opts.agentHarnessBinArgs?.length
47
+ ? { agentHarnessBinArgs: this.opts.agentHarnessBinArgs }
48
+ : {}),
49
+ });
50
+ const env = claudeEnv(this.opts.env ?? process.env, {
51
+ harnessBin: this.opts.harnessBin,
52
+ session,
53
+ unitId: briefing.unitId,
54
+ ...(this.opts.harnessBinArgs?.length ? { harnessBinArgs: this.opts.harnessBinArgs } : {}),
55
+ ...(this.opts.ledgerDir ? { ledgerDir: this.opts.ledgerDir } : {}),
56
+ ...(this.opts.systemId ? { systemId: this.opts.systemId } : {}),
57
+ });
58
+ const emit = async (phase, body) => {
59
+ if (this.opts.emitTelemetry)
60
+ return this.opts.emitTelemetry({ phase, body });
61
+ return emitClaudeSessionTelemetry({
62
+ agentHarnessBin: this.opts.agentHarnessBin,
63
+ ...(this.opts.agentHarnessBinArgs?.length
64
+ ? { agentHarnessBinArgs: this.opts.agentHarnessBinArgs }
65
+ : {}),
66
+ phase,
67
+ body,
68
+ cwd,
69
+ env,
70
+ });
71
+ };
72
+ const common = {
73
+ session_id: session,
74
+ unit_id: briefing.unitId,
75
+ component_id: briefing.component ?? "unknown",
76
+ model: policy.model,
77
+ runner: this.name,
78
+ role: kind,
79
+ emitter: "adapter",
80
+ };
81
+ try {
82
+ await emit("started", common);
83
+ }
84
+ catch (error) {
85
+ return {
86
+ ok: false,
87
+ sessionId: session,
88
+ filesTouched: [],
89
+ cost: 0,
90
+ telemetryError: error.message,
91
+ };
92
+ }
93
+ let result;
94
+ try {
95
+ result = kind === "verifier"
96
+ ? await this.runVerifier(briefing, policy, env, limits, cwd, onActivity)
97
+ : await this.runImplementer(briefing, policy, env, limits, cwd, onActivity);
98
+ result.sessionId = session;
99
+ if (result.escalation) {
100
+ await emit("escalated", { ...common, escalation: result.escalation });
101
+ }
102
+ }
103
+ catch (error) {
104
+ result = {
105
+ ok: false,
106
+ sessionId: session,
107
+ filesTouched: [],
108
+ cost: 0,
109
+ telemetryError: error.message,
110
+ };
111
+ }
112
+ this.releaseBudget(limits.reservedUsd, result.cost);
113
+ try {
114
+ await emit("ended", {
115
+ ...common,
116
+ tokens: result.tokens ?? { in: 0, out: 0 },
117
+ cost_usd: result.cost,
118
+ transcript_ref: result.transcriptRef ?? null,
119
+ ended: {
120
+ outcome: result.escalation ? "escalated" : result.ok ? "handed-off" : "abandoned",
121
+ },
122
+ });
123
+ }
124
+ catch (error) {
125
+ result.ok = false;
126
+ result.telemetryError = error.message;
127
+ }
128
+ return result;
129
+ }
130
+ async runImplementer(briefing, policy, env, limits, cwd, onActivity) {
131
+ const prompt = renderImplementerPrompt(briefing, this.opts.retryFor?.(briefing.unitId, "implementer"));
132
+ const activitySink = onActivity
133
+ ? (u) => onActivity({
134
+ line: u.line,
135
+ turn: u.turn,
136
+ tokens: u.tokens,
137
+ ...(u.kind ? { kind: u.kind } : {}),
138
+ ...(u.tool ? { tool: u.tool } : {}),
139
+ ...(u.target ? { target: u.target } : {}),
140
+ })
141
+ : undefined;
142
+ const r = await runClaudeSession({
143
+ prompt,
144
+ model: policy.model,
145
+ cwd,
146
+ permissionMode: "acceptEdits",
147
+ allowedTools: [...WRITE_TOOLS, "Read", "Grep", "Glob", "Bash"],
148
+ env,
149
+ settingSources: ["project"],
150
+ ...(this.opts.maxTurns ? { maxTurns: this.opts.maxTurns } : {}),
151
+ ...(limits.maxBudgetUsd !== undefined
152
+ ? { maxBudgetUsd: limits.maxBudgetUsd }
153
+ : {}),
154
+ ...(limits.abortSignal ? { abortSignal: limits.abortSignal } : {}),
155
+ ...(this.opts.queryImpl ? { queryImpl: this.opts.queryImpl } : {}),
156
+ ...(activitySink ? { onActivity: activitySink } : {}),
157
+ });
158
+ const result = {
159
+ ok: r.ok && !r.permissionDenied,
160
+ filesTouched: [], // the loop cross-checks with `git diff` (M2.5 + M3)
161
+ cost: r.totalCostUsd,
162
+ tokens: r.tokens,
163
+ ...(r.sessionId ? { transcriptRef: `claude-session://${r.sessionId}` } : {}),
164
+ };
165
+ // a denied write is a scope violation the ring-0 hook caught in-session.
166
+ if (r.permissionDenied) {
167
+ result.escalation = {
168
+ reason: "the ring-0 scope hook denied a write outside the declared scope",
169
+ wants: "widen scope or split the unit",
170
+ };
171
+ }
172
+ return result;
173
+ }
174
+ async runVerifier(briefing, policy, env, limits, cwd, onActivity) {
175
+ const diff = (await this.opts.diffFor?.(briefing.unitId)) ?? "";
176
+ const prompt = renderVerifierPrompt(briefing, diff);
177
+ const activitySink = onActivity
178
+ ? (u) => onActivity({
179
+ line: u.line,
180
+ turn: u.turn,
181
+ tokens: u.tokens,
182
+ ...(u.kind ? { kind: u.kind } : {}),
183
+ ...(u.tool ? { tool: u.tool } : {}),
184
+ ...(u.target ? { target: u.target } : {}),
185
+ })
186
+ : undefined;
187
+ const r = await runClaudeSession({
188
+ prompt,
189
+ model: policy.model,
190
+ cwd,
191
+ // read-only: NO write tools at all, and never auto-accept an edit.
192
+ permissionMode: "default",
193
+ allowedTools: VERIFIER_TOOLS,
194
+ disallowedTools: WRITE_TOOLS,
195
+ env,
196
+ settingSources: ["project"],
197
+ ...(this.opts.maxTurns ? { maxTurns: this.opts.maxTurns } : {}),
198
+ ...(limits.maxBudgetUsd !== undefined
199
+ ? { maxBudgetUsd: limits.maxBudgetUsd }
200
+ : {}),
201
+ ...(limits.abortSignal ? { abortSignal: limits.abortSignal } : {}),
202
+ ...(this.opts.queryImpl ? { queryImpl: this.opts.queryImpl } : {}),
203
+ ...(activitySink ? { onActivity: activitySink } : {}),
204
+ });
205
+ const verdict = parseVerdict(r.resultText);
206
+ return {
207
+ ok: r.ok,
208
+ filesTouched: [], // a verifier must touch nothing; the loop's post-hoc diff is the backstop
209
+ cost: r.totalCostUsd,
210
+ tokens: r.tokens,
211
+ ...(r.sessionId ? { transcriptRef: `claude-session://${r.sessionId}` } : {}),
212
+ verdict,
213
+ };
214
+ }
215
+ reserveLimits() {
216
+ let reservedUsd = 0;
217
+ let maxBudgetUsd;
218
+ if (this.opts.maxRunCost !== undefined) {
219
+ const available = this.opts.maxRunCost - this.spentUsd - this.reservedUsd;
220
+ if (available <= 0)
221
+ return null;
222
+ reservedUsd = available;
223
+ maxBudgetUsd = available;
224
+ this.reservedUsd += reservedUsd;
225
+ }
226
+ let abortSignal;
227
+ if (this.opts.deadlineAt !== undefined) {
228
+ const remaining = this.opts.deadlineAt - Date.now();
229
+ if (remaining <= 0) {
230
+ this.reservedUsd -= reservedUsd;
231
+ return null;
232
+ }
233
+ abortSignal = AbortSignal.timeout(remaining);
234
+ }
235
+ return {
236
+ reservedUsd,
237
+ ...(maxBudgetUsd !== undefined ? { maxBudgetUsd } : {}),
238
+ ...(abortSignal ? { abortSignal } : {}),
239
+ };
240
+ }
241
+ releaseBudget(reservedUsd, actualUsd) {
242
+ this.reservedUsd = Math.max(0, this.reservedUsd - reservedUsd);
243
+ this.spentUsd += actualUsd;
244
+ }
245
+ }
246
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,198 @@
1
+ const LINE_CLAMP = 160;
2
+ const ARG_CLAMP = 80;
3
+ async function loadRealQuery() {
4
+ const mod = await import("@anthropic-ai/claude-agent-sdk");
5
+ return mod.query;
6
+ }
7
+ export async function runClaudeSession(params) {
8
+ const query = params.queryImpl ?? (await loadRealQuery());
9
+ const options = {
10
+ model: params.model,
11
+ cwd: params.cwd,
12
+ permissionMode: params.permissionMode,
13
+ env: params.env,
14
+ ...(params.allowedTools ? { allowedTools: params.allowedTools } : {}),
15
+ ...(params.disallowedTools ? { disallowedTools: params.disallowedTools } : {}),
16
+ ...(params.settingSources ? { settingSources: params.settingSources } : {}),
17
+ ...(params.maxTurns ? { maxTurns: params.maxTurns } : {}),
18
+ ...(params.maxBudgetUsd !== undefined
19
+ ? { maxBudgetUsd: params.maxBudgetUsd }
20
+ : {}),
21
+ ...(params.abortSignal
22
+ ? { abortController: abortControllerFor(params.abortSignal) }
23
+ : {}),
24
+ };
25
+ const result = {
26
+ ok: false,
27
+ resultText: "",
28
+ totalCostUsd: 0,
29
+ sessionId: null,
30
+ numTurns: 0,
31
+ tokens: { in: 0, out: 0 },
32
+ permissionDenied: false,
33
+ };
34
+ let turn = 0;
35
+ let latestTokens = { in: 0, out: 0 };
36
+ const toolNames = new Map();
37
+ const emit = (line, kind, tool, target) => {
38
+ if (!params.onActivity)
39
+ return;
40
+ try {
41
+ params.onActivity({
42
+ line: clamp(line, LINE_CLAMP),
43
+ turn,
44
+ tokens: { ...latestTokens },
45
+ kind,
46
+ ...(tool ? { tool } : {}),
47
+ ...(target ? { target } : {}),
48
+ });
49
+ }
50
+ catch {
51
+ // broken sink must not change the session result
52
+ }
53
+ };
54
+ try {
55
+ for await (const msg of query({ prompt: params.prompt, options })) {
56
+ const m = msg;
57
+ const usageTokens = extractUsageTokens(m);
58
+ if (usageTokens)
59
+ latestTokens = usageTokens;
60
+ if (m.type === "assistant") {
61
+ turn += 1;
62
+ const blocks = contentBlocks(m);
63
+ let emitted = false;
64
+ for (const block of blocks) {
65
+ if (block.type === "tool_use") {
66
+ const name = typeof block.name === "string" && block.name ? block.name : "tool";
67
+ if (typeof block.id === "string")
68
+ toolNames.set(block.id, name);
69
+ const arg = clamp(primaryArgument(block.input), ARG_CLAMP);
70
+ emit(`${name}: ${arg}`, "tool", name, arg || undefined);
71
+ emitted = true;
72
+ }
73
+ else if (block.type === "text") {
74
+ const excerpt = clamp(typeof block.text === "string" ? block.text : "", ARG_CLAMP);
75
+ emit(excerpt ? `assistant: ${excerpt}` : "assistant", "text");
76
+ emitted = true;
77
+ }
78
+ }
79
+ if (!emitted)
80
+ emit("assistant", "text");
81
+ }
82
+ else if (m.type === "user") {
83
+ for (const block of contentBlocks(m)) {
84
+ if (block.type !== "tool_result")
85
+ continue;
86
+ const id = typeof block.tool_use_id === "string" ? block.tool_use_id : "";
87
+ const name = toolNames.get(id) ?? "tool";
88
+ emit(block.is_error === true ? `${name} -> error` : `${name} -> ok`, "tool-result", name);
89
+ }
90
+ }
91
+ else if (m.type === "result") {
92
+ result.totalCostUsd = numberOr(m.total_cost_usd, result.totalCostUsd);
93
+ result.numTurns = numberOr(m.num_turns, result.numTurns);
94
+ const usage = m.usage;
95
+ if (usage) {
96
+ result.tokens = {
97
+ in: numberOr(usage.input_tokens, result.tokens.in),
98
+ out: numberOr(usage.output_tokens, result.tokens.out),
99
+ };
100
+ latestTokens = { ...result.tokens };
101
+ }
102
+ if (typeof m.session_id === "string")
103
+ result.sessionId = m.session_id;
104
+ if (typeof m.result === "string")
105
+ result.resultText = m.result;
106
+ const denials = m.permission_denials;
107
+ if (Array.isArray(denials) && denials.length > 0)
108
+ result.permissionDenied = true;
109
+ if (m.subtype === "success") {
110
+ result.ok = true;
111
+ }
112
+ else {
113
+ result.ok = false;
114
+ result.error =
115
+ (Array.isArray(m.errors) && m.errors.join("; ")) ||
116
+ (typeof m.subtype === "string" ? m.subtype : "error");
117
+ }
118
+ }
119
+ }
120
+ }
121
+ catch (e) {
122
+ result.ok = false;
123
+ result.error = e.message;
124
+ }
125
+ return result;
126
+ }
127
+ function contentBlocks(msg) {
128
+ const nested = msg.message;
129
+ const raw = nested?.content ?? msg.content;
130
+ if (!Array.isArray(raw))
131
+ return [];
132
+ return raw.filter((b) => !!b && typeof b === "object" && !Array.isArray(b));
133
+ }
134
+ /** Prefer well-known tool input keys, else the first string value. */
135
+ function primaryArgument(input) {
136
+ if (input == null)
137
+ return "";
138
+ if (typeof input === "string")
139
+ return input;
140
+ if (typeof input !== "object" || Array.isArray(input)) {
141
+ try {
142
+ return JSON.stringify(input);
143
+ }
144
+ catch {
145
+ return "";
146
+ }
147
+ }
148
+ const obj = input;
149
+ const preferred = [
150
+ "file_path",
151
+ "path",
152
+ "command",
153
+ "pattern",
154
+ "query",
155
+ "url",
156
+ "prompt",
157
+ ];
158
+ for (const key of preferred) {
159
+ const v = obj[key];
160
+ if (typeof v === "string" && v.length > 0)
161
+ return v;
162
+ }
163
+ for (const v of Object.values(obj)) {
164
+ if (typeof v === "string" && v.length > 0)
165
+ return v;
166
+ }
167
+ try {
168
+ return JSON.stringify(input);
169
+ }
170
+ catch {
171
+ return "";
172
+ }
173
+ }
174
+ function extractUsageTokens(msg) {
175
+ const nested = msg.message;
176
+ const usage = (nested?.usage ?? msg.usage);
177
+ if (!usage)
178
+ return null;
179
+ return {
180
+ in: numberOr(usage.input_tokens, 0),
181
+ out: numberOr(usage.output_tokens, 0),
182
+ };
183
+ }
184
+ function clamp(s, max) {
185
+ return s.length <= max ? s : s.slice(0, max);
186
+ }
187
+ function numberOr(v, fallback) {
188
+ return typeof v === "number" && Number.isFinite(v) ? v : fallback;
189
+ }
190
+ function abortControllerFor(signal) {
191
+ const ac = new AbortController();
192
+ if (signal.aborted)
193
+ ac.abort();
194
+ else
195
+ signal.addEventListener("abort", () => ac.abort(), { once: true });
196
+ return ac;
197
+ }
198
+ //# sourceMappingURL=sdk.js.map
@@ -0,0 +1,98 @@
1
+ /**
2
+ * wiring — puts an agent-harness adapter under a Claude session so it gets
3
+ * ring-0 scope enforcement + first-hand `session.*` telemetry (v0-plan M2
4
+ * "Session wiring (Claude)"):
5
+ * - write `.harness/current-unit.json` (the hook reads the unit + scope from here)
6
+ * - ensure the `harness hook pre-tool-use` PreToolUse entry in
7
+ * `.claude/settings.json` (no internal binary path, no AGENT_HARNESS_BIN).
8
+ * Session-time ensure of the same public command; the named owner of
9
+ * init-composes-hook-install is system-harness `init` → agent-harness
10
+ * `adapter/install`.
11
+ * - set `HARNESS_BIN` / `HARNESS_SESSION_ID` in the session env (process env
12
+ * for the adapter to reach the system CLI — not a user-facing install step)
13
+ *
14
+ * All writes land in the session `cwd` (the driven project's checkout). Pure
15
+ * except for those three fs effects; each is idempotent.
16
+ */
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18
+ import { dirname, join } from "node:path";
19
+ import { callProcess } from "../../harness/cli.js";
20
+ /** Public facade hook command — never an absolute internal CLI path. */
21
+ export const HOOK_COMMAND = "harness hook pre-tool-use";
22
+ /** `.harness/current-unit.json` — the shape `agent-harness`'s `loadUnitContext` reads. */
23
+ export function writeCurrentUnit(opts) {
24
+ const path = join(opts.cwd, ".harness", "current-unit.json");
25
+ mkdirSync(dirname(path), { recursive: true });
26
+ writeFileSync(path, JSON.stringify({
27
+ unit_id: opts.briefing.unitId,
28
+ component_id: opts.briefing.component ?? null,
29
+ scope: opts.briefing.scope,
30
+ session_id: opts.session,
31
+ }, null, 2) + "\n");
32
+ return path;
33
+ }
34
+ /**
35
+ * Merge the PreToolUse hooks block into `.claude/settings.json`.
36
+ * Idempotent: if a PreToolUse entry already runs `harness hook pre-tool-use`
37
+ * (or the legacy `hook pre-tool-use` form), nothing changes.
38
+ */
39
+ export function ensureAgentHarnessHook(opts) {
40
+ const path = join(opts.cwd, ".claude", "settings.json");
41
+ const snippet = {
42
+ PreToolUse: [
43
+ {
44
+ matcher: "Write|Edit|MultiEdit|NotebookEdit",
45
+ hooks: [{ type: "command", command: HOOK_COMMAND }],
46
+ },
47
+ ],
48
+ };
49
+ let settings = {};
50
+ if (existsSync(path)) {
51
+ try {
52
+ settings = JSON.parse(readFileSync(path, "utf8"));
53
+ }
54
+ catch {
55
+ settings = {};
56
+ }
57
+ }
58
+ const hooks = (settings.hooks ??= {});
59
+ const pre = (hooks.PreToolUse ??= []);
60
+ const already = JSON.stringify(pre).includes("harness hook pre-tool-use") ||
61
+ JSON.stringify(pre).includes("hook pre-tool-use");
62
+ if (!already && Array.isArray(snippet.PreToolUse)) {
63
+ pre.push(...snippet.PreToolUse);
64
+ }
65
+ mkdirSync(dirname(path), { recursive: true });
66
+ writeFileSync(path, JSON.stringify(settings, null, 2) + "\n");
67
+ return path;
68
+ }
69
+ /** The session env: the base env plus HARNESS_BIN + HARNESS_SESSION_ID. */
70
+ export function claudeEnv(base, opts) {
71
+ return {
72
+ ...base,
73
+ HARNESS_BIN: opts.harnessBin,
74
+ ...(opts.harnessBinArgs?.length
75
+ ? { HARNESS_BIN_ARGS: JSON.stringify(opts.harnessBinArgs) }
76
+ : {}),
77
+ HARNESS_SESSION_ID: opts.session,
78
+ HARNESS_UNIT_ID: opts.unitId,
79
+ ...(opts.ledgerDir ? { HARNESS_LEDGER_DIR: opts.ledgerDir } : {}),
80
+ ...(opts.systemId ? { HARNESS_SYSTEM: opts.systemId } : {}),
81
+ };
82
+ }
83
+ /** Invoke the adapter's public session surface; any failure is authoritative. */
84
+ export function emitClaudeSessionTelemetry(opts) {
85
+ return callProcess({
86
+ command: opts.agentHarnessBin,
87
+ args: [...(opts.agentHarnessBinArgs ?? []), "session", opts.phase],
88
+ cwd: opts.cwd,
89
+ env: opts.env,
90
+ stdin: JSON.stringify(opts.body),
91
+ }).then((result) => {
92
+ if (result.code !== 0) {
93
+ const detail = (result.stderr || result.stdout).trim();
94
+ throw new Error(`agent-harness session ${opts.phase} failed (${result.code}): ${detail}`);
95
+ }
96
+ });
97
+ }
98
+ //# sourceMappingURL=wiring.js.map