@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,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
|