@basein/runner 0.3.17 → 0.3.18
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/dist/bin/bir-scenario.d.ts +5 -1
- package/dist/bin/bir-scenario.js +155 -5
- package/dist/bin/bir.js +2 -2
- package/dist/bin/scenario-edit.d.ts +7 -0
- package/dist/bin/scenario-edit.js +9 -2
- package/dist/bin/scenario-liveness.d.ts +12 -1
- package/dist/bin/scenario-liveness.js +8 -4
- package/dist/bin/setup.js +3 -3
- package/dist/control/client.d.ts +5 -0
- package/dist/control/client.js +7 -0
- package/dist/control/ensure-hook.d.ts +14 -3
- package/dist/control/ensure-hook.js +18 -5
- package/dist/control/server.d.ts +165 -2
- package/dist/control/server.js +719 -21
- package/dist/control/transcript.d.ts +19 -0
- package/dist/control/transcript.js +57 -0
- package/dist/proxy/intercept.js +13 -0
- package/dist/proxy/session.d.ts +23 -0
- package/dist/proxy/session.js +71 -3
- package/dist/record/recorder.d.ts +146 -0
- package/dist/record/recorder.js +9 -0
- package/dist/record/remote-recorder.d.ts +59 -1
- package/dist/record/remote-recorder.js +297 -20
- package/dist/record/switchable-recorder.js +6 -0
- package/dist/replay/bundle.d.ts +8 -0
- package/dist/replay/bundle.js +20 -6
- package/dist/replay/controller.d.ts +33 -1
- package/dist/replay/controller.js +137 -12
- package/dist/replay/derive.d.ts +15 -0
- package/dist/replay/derive.js +5 -0
- package/dist/replay/plan.d.ts +90 -1
- package/dist/replay/plan.js +232 -10
- package/dist/replay/source-run.d.ts +2 -2
- package/dist/replay/source-run.js +3 -3
- package/dist/replay/targets.d.ts +40 -1
- package/dist/replay/targets.js +105 -3
- package/dist/replay/types.d.ts +6 -5
- package/package.json +1 -1
|
@@ -68,7 +68,11 @@
|
|
|
68
68
|
* WHICH TOOLS ARE OFFERED. `run_scenario`, always — unless the runner is
|
|
69
69
|
* switched off (`bir off`), when nothing is: an inert runner adds nothing to
|
|
70
70
|
* the model's context, and a call from a list taken before the switch starts
|
|
71
|
-
* nothing and asks nobody.
|
|
71
|
+
* nothing and asks nobody. `report_wrong_result` comes with it: the agent that
|
|
72
|
+
* reads a replay's results is the one who can tell they do not answer the
|
|
73
|
+
* request, and says so in one call that holds the plan and undoes nothing
|
|
74
|
+
* (the service's review of calcscenarioautocorrect.md, item 19); the control
|
|
75
|
+
* server knows which session asks, and so which plan. The rest come with scenario editing, which
|
|
72
76
|
* `bir setup` turns on (a person's `bir scenario editing off` is kept): the
|
|
73
77
|
* reading tools — also with `BIR_SCENARIO_TOOLS=read` in this server's
|
|
74
78
|
* environment — and the tools that CHANGE a scenario (D2). A project wired by
|
package/dist/bin/bir-scenario.js
CHANGED
|
@@ -68,7 +68,11 @@
|
|
|
68
68
|
* WHICH TOOLS ARE OFFERED. `run_scenario`, always — unless the runner is
|
|
69
69
|
* switched off (`bir off`), when nothing is: an inert runner adds nothing to
|
|
70
70
|
* the model's context, and a call from a list taken before the switch starts
|
|
71
|
-
* nothing and asks nobody.
|
|
71
|
+
* nothing and asks nobody. `report_wrong_result` comes with it: the agent that
|
|
72
|
+
* reads a replay's results is the one who can tell they do not answer the
|
|
73
|
+
* request, and says so in one call that holds the plan and undoes nothing
|
|
74
|
+
* (the service's review of calcscenarioautocorrect.md, item 19); the control
|
|
75
|
+
* server knows which session asks, and so which plan. The rest come with scenario editing, which
|
|
72
76
|
* `bir setup` turns on (a person's `bir scenario editing off` is kept): the
|
|
73
77
|
* reading tools — also with `BIR_SCENARIO_TOOLS=read` in this server's
|
|
74
78
|
* environment — and the tools that CHANGE a scenario (D2). A project wired by
|
|
@@ -184,6 +188,39 @@ const TOOL = {
|
|
|
184
188
|
"never otherwise.",
|
|
185
189
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
186
190
|
};
|
|
191
|
+
/**
|
|
192
|
+
* The agent's way to say a replay was wrong (the service's review of
|
|
193
|
+
* calcscenarioautocorrect.md, item 19). A plan that took the wrong half of an
|
|
194
|
+
* "if … otherwise" was caught only when the agent said so in its answer, or
|
|
195
|
+
* did the task again — and the results it was handed told it not to check.
|
|
196
|
+
* Offered wherever `run_scenario` is: the agent that reads a replay's results
|
|
197
|
+
* is the one who can tell they are wrong. It holds the plan on the service and
|
|
198
|
+
* undoes nothing. Must match `REPORT_TOOL_NAME`'s suffix in the replay
|
|
199
|
+
* controller.
|
|
200
|
+
*/
|
|
201
|
+
const REPORT_TOOL_NAME = "report_wrong_result";
|
|
202
|
+
/** What the agent may say, in characters; the control server and the service cut it there too. */
|
|
203
|
+
const REPORT_REASON_MAX = 2_000;
|
|
204
|
+
/** The whole report's request: the control server waits up to 10 s for the service. */
|
|
205
|
+
const REPORT_TIMEOUT_MS = 15_000;
|
|
206
|
+
const REPORT_TOOL = {
|
|
207
|
+
name: REPORT_TOOL_NAME,
|
|
208
|
+
description: "Call this when the results of a replayed scenario (run_scenario, or a plan bir ran for you) do not answer " +
|
|
209
|
+
"the user's request — for example, the wrong branch of an if/otherwise was taken, or the wrong item was acted " +
|
|
210
|
+
"on. Say why in one or two sentences. It does not undo anything.",
|
|
211
|
+
inputSchema: {
|
|
212
|
+
type: "object",
|
|
213
|
+
properties: {
|
|
214
|
+
reason: {
|
|
215
|
+
type: "string",
|
|
216
|
+
maxLength: REPORT_REASON_MAX,
|
|
217
|
+
description: "Why the results do not answer the request, in one or two sentences.",
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
required: ["reason"],
|
|
221
|
+
additionalProperties: false,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
187
224
|
/**
|
|
188
225
|
* Ask the control server to run the armed plan. Never throws. The reasons it
|
|
189
226
|
* returns are for the audit log; {@link toolResult} decides what the model reads.
|
|
@@ -213,8 +250,14 @@ async function runArmedPlan(cwd, flight, toolUseId) {
|
|
|
213
250
|
"content-type": "application/json",
|
|
214
251
|
authorization: `Bearer ${found.token}`,
|
|
215
252
|
},
|
|
216
|
-
// `ppid`: the host that started this server and the session's proxies
|
|
217
|
-
|
|
253
|
+
// `ppid`: the host that started this server and the session's proxies
|
|
254
|
+
// (WHOSE PROXIES above). `reports`: this server offers
|
|
255
|
+
// `report_wrong_result`, so the results may say the agent can use it.
|
|
256
|
+
body: JSON.stringify({
|
|
257
|
+
...(toolUseId ? { toolUseId } : {}),
|
|
258
|
+
ppid: process.ppid,
|
|
259
|
+
reports: listTools(cwd).some((t) => t.name === REPORT_TOOL_NAME),
|
|
260
|
+
}),
|
|
218
261
|
signal: controller.signal,
|
|
219
262
|
});
|
|
220
263
|
const raw = await res.text();
|
|
@@ -333,6 +376,60 @@ function toolResult(result) {
|
|
|
333
376
|
return said(ALREADY_RUNNING_TEXT);
|
|
334
377
|
return said(UNAVAILABLE_TEXT);
|
|
335
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* Tell the control server the agent said a replay was wrong (review item 19):
|
|
381
|
+
* it knows the session — by the host's id for this call, as for
|
|
382
|
+
* `run_scenario` — and so the plan, and it reports it to the service. Never
|
|
383
|
+
* throws; every failure is an answer.
|
|
384
|
+
*/
|
|
385
|
+
async function reportWrongResult(cwd, flight, reason, toolUseId) {
|
|
386
|
+
const found = await resolveControl(cwd, DISCOVERY_GRACE_MS, 50);
|
|
387
|
+
if (!found?.url || !found.token)
|
|
388
|
+
return { ok: false, why: "no_plan" };
|
|
389
|
+
if (flight.cancelled)
|
|
390
|
+
return { ok: false, why: "cancelled" };
|
|
391
|
+
const controller = new AbortController();
|
|
392
|
+
flight.abort = controller;
|
|
393
|
+
const timer = setTimeout(() => controller.abort(), REPORT_TIMEOUT_MS);
|
|
394
|
+
timer.unref?.();
|
|
395
|
+
try {
|
|
396
|
+
const res = await fetch(`${found.url.replace(/\/+$/, "")}/scenario/dispute`, {
|
|
397
|
+
method: "POST",
|
|
398
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${found.token}` },
|
|
399
|
+
body: JSON.stringify({ reason, ...(toolUseId ? { toolUseId } : {}), ppid: process.ppid }),
|
|
400
|
+
signal: controller.signal,
|
|
401
|
+
});
|
|
402
|
+
const raw = await res.text();
|
|
403
|
+
// An older recorder has no such route (404): it cannot take the report.
|
|
404
|
+
if (!res.ok)
|
|
405
|
+
return { ok: false, why: res.status === 404 ? "unsupported" : `HTTP ${res.status}` };
|
|
406
|
+
const body = JSON.parse(raw);
|
|
407
|
+
return body && typeof body === "object" && !Array.isArray(body) ? body : { ok: false };
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
return { ok: false, why: flight.cancelled ? "cancelled" : errText(err) };
|
|
411
|
+
}
|
|
412
|
+
finally {
|
|
413
|
+
clearTimeout(timer);
|
|
414
|
+
flight.abort = undefined;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* The one sentence the agent reads back. Never an error: the report is a side
|
|
419
|
+
* note to its answer, and whatever happened to it, the agent's next move is to
|
|
420
|
+
* tell the user.
|
|
421
|
+
*/
|
|
422
|
+
function reportAnswer(result) {
|
|
423
|
+
if (result.ok === true) {
|
|
424
|
+
return said(result.held === true
|
|
425
|
+
? "Reported. bir will not use this plan again until it is checked."
|
|
426
|
+
: "Reported. Now tell the user what is wrong with these results.");
|
|
427
|
+
}
|
|
428
|
+
if (result.why === "no_plan") {
|
|
429
|
+
return said("Nothing was reported: no scenario was replayed for this session's latest request.");
|
|
430
|
+
}
|
|
431
|
+
return said("The report could not be sent right now. Tell the user what is wrong with these results.");
|
|
432
|
+
}
|
|
336
433
|
// ── the scenario tools: `bir scenario …`, spawned ─────────────────────────────
|
|
337
434
|
/** How long a `bir` that may change a scenario may take. A check replays the chain on the service. */
|
|
338
435
|
const BIR_TIMEOUT_MS = 120_000;
|
|
@@ -720,7 +817,11 @@ function readToolsOptedIn() {
|
|
|
720
817
|
const value = (process.env.BIR_SCENARIO_TOOLS ?? "").trim().toLowerCase();
|
|
721
818
|
return value === "read" || value === "1" || value === "on" || value === "true";
|
|
722
819
|
}
|
|
723
|
-
/**
|
|
820
|
+
/**
|
|
821
|
+
* The tools to list for `cwd`, read now: the switches may have moved since the
|
|
822
|
+
* last list. `report_wrong_result` comes with `run_scenario`, editing on or off:
|
|
823
|
+
* whoever reads a replay's results may need to say they are wrong.
|
|
824
|
+
*/
|
|
724
825
|
function listTools(cwd) {
|
|
725
826
|
if (runnerOff(cwd))
|
|
726
827
|
return [];
|
|
@@ -728,6 +829,7 @@ function listTools(cwd) {
|
|
|
728
829
|
const reading = editing || readToolsOptedIn();
|
|
729
830
|
return [
|
|
730
831
|
TOOL,
|
|
832
|
+
REPORT_TOOL,
|
|
731
833
|
...SCENARIO_TOOLS.filter((t) => (t.changes ? editing : reading)).map(({ name, description, inputSchema }) => ({
|
|
732
834
|
name,
|
|
733
835
|
description,
|
|
@@ -968,7 +1070,8 @@ function main() {
|
|
|
968
1070
|
cwd,
|
|
969
1071
|
state: () => {
|
|
970
1072
|
const off = runnerOff(cwd);
|
|
971
|
-
|
|
1073
|
+
const tools = off ? [] : listTools(cwd);
|
|
1074
|
+
return { tools: tools.length, off, report: tools.some((t) => t.name === REPORT_TOOL_NAME) };
|
|
972
1075
|
},
|
|
973
1076
|
});
|
|
974
1077
|
/**
|
|
@@ -1035,6 +1138,27 @@ function main() {
|
|
|
1035
1138
|
}
|
|
1036
1139
|
reply({ jsonrpc: "2.0", id, result: answer });
|
|
1037
1140
|
};
|
|
1141
|
+
/** `report_wrong_result`: tell the control server, and answer exactly once — unless cancelled. */
|
|
1142
|
+
const answerReport = async (id, flight, reason, toolUseId) => {
|
|
1143
|
+
let result;
|
|
1144
|
+
try {
|
|
1145
|
+
result = await reportWrongResult(cwd, flight, reason, toolUseId);
|
|
1146
|
+
}
|
|
1147
|
+
catch (err) {
|
|
1148
|
+
result = { ok: false, why: errText(err) };
|
|
1149
|
+
}
|
|
1150
|
+
if (inFlight.get(id) === flight)
|
|
1151
|
+
inFlight.delete(id);
|
|
1152
|
+
if (flight.cancelled || closing)
|
|
1153
|
+
return;
|
|
1154
|
+
logLine("scenario.reported", {
|
|
1155
|
+
ok: result.ok === true,
|
|
1156
|
+
held: result.held === true || undefined,
|
|
1157
|
+
why: typeof result.why === "string" ? result.why.slice(0, 200) : undefined,
|
|
1158
|
+
bound: toolUseId !== undefined || undefined,
|
|
1159
|
+
});
|
|
1160
|
+
reply({ jsonrpc: "2.0", id, result: reportAnswer(result) });
|
|
1161
|
+
};
|
|
1038
1162
|
/** A scenario tool: run `bir`, and answer exactly once — unless cancelled. */
|
|
1039
1163
|
const answerTool = async (id, tool, args, flight) => {
|
|
1040
1164
|
let result;
|
|
@@ -1136,6 +1260,32 @@ function main() {
|
|
|
1136
1260
|
void answerTool(id, scenarioTool, paramsObject(params.arguments), flight);
|
|
1137
1261
|
return;
|
|
1138
1262
|
}
|
|
1263
|
+
if (name === REPORT_TOOL_NAME) {
|
|
1264
|
+
// Off: nothing ran a plan, so there is nothing to report — said
|
|
1265
|
+
// plainly, as the reading tools say it.
|
|
1266
|
+
if (runnerOff(cwd)) {
|
|
1267
|
+
logLine("scenario.reported", { ok: false, why: "runner is off" });
|
|
1268
|
+
reply({
|
|
1269
|
+
jsonrpc: "2.0",
|
|
1270
|
+
id,
|
|
1271
|
+
result: said("BaseInstRunner is switched off here, so there is nothing to report."),
|
|
1272
|
+
});
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
const reason = paramsObject(params.arguments).reason;
|
|
1276
|
+
if (typeof reason !== "string" || reason.trim() === "") {
|
|
1277
|
+
reply({
|
|
1278
|
+
jsonrpc: "2.0",
|
|
1279
|
+
id,
|
|
1280
|
+
result: { content: [{ type: "text", text: "`reason` is required: say in a sentence or two why the results are wrong." }], isError: true },
|
|
1281
|
+
});
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
const flight = { tool: REPORT_TOOL_NAME, cancelled: false };
|
|
1285
|
+
inFlight.set(id, flight);
|
|
1286
|
+
void answerReport(id, flight, reason.trim().slice(0, REPORT_REASON_MAX), toolUseIdOf(params));
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1139
1289
|
if (name !== TOOL_NAME) {
|
|
1140
1290
|
reply(errorResponse(id, JsonRpcErrorCode.MethodNotFound, `unknown tool: ${params.name}`));
|
|
1141
1291
|
return;
|
package/dist/bin/bir.js
CHANGED
|
@@ -3269,8 +3269,8 @@ async function doctor(args) {
|
|
|
3269
3269
|
// lets Claude Code run them without asking; said only when that is not so.
|
|
3270
3270
|
if (scenarioEntry && isScenarioServer(scenarioEntry.config)) {
|
|
3271
3271
|
if (!editingEnabled(sidecar, cwd)) {
|
|
3272
|
-
notes.push("the bir MCP server offers run_scenario only: scenario editing is off here —
|
|
3273
|
-
"adds investigate and the scenario tools (restart Claude Code after)");
|
|
3272
|
+
notes.push("the bir MCP server offers run_scenario and report_wrong_result only: scenario editing is off here — " +
|
|
3273
|
+
"`bir scenario editing on` adds investigate and the scenario tools (restart Claude Code after)");
|
|
3274
3274
|
}
|
|
3275
3275
|
else if (![claudeUserSettingsPath(), paths.settings, paths.settingsLocal].some((file) => allowRules(file).some((rule) => rule === BIR_TOOLS_RULE || rule === `${BIR_TOOLS_RULE}__*`))) {
|
|
3276
3276
|
notes.push("Claude Code asks before investigate and the scenario tools run here — `bir setup` allows every bir tool " +
|
|
@@ -222,6 +222,13 @@ export declare const SCENARIO_EDIT_SUBCOMMANDS: ReadonlySet<string>;
|
|
|
222
222
|
export declare const READ_TOOLS: readonly ["scenario_show", "scenario_edits", "investigate"];
|
|
223
223
|
/** The ones that change a scenario: offered only after `bir scenario editing on` (D2). */
|
|
224
224
|
export declare const EDIT_TOOLS: readonly ["scenario_check", "scenario_edit", "scenario_undo"];
|
|
225
|
+
/**
|
|
226
|
+
* The ones every session with the `bir` server has, editing on or off: the
|
|
227
|
+
* delivery of a direct plan, and the agent's way to say a replay's results do
|
|
228
|
+
* not answer the request (the service's review of calcscenarioautocorrect.md,
|
|
229
|
+
* item 19). `bir setup`'s allow rule, `mcp__bir`, covers every one.
|
|
230
|
+
*/
|
|
231
|
+
export declare const BASE_TOOLS: readonly ["run_scenario", "report_wrong_result"];
|
|
225
232
|
export declare function scenarioEditCommand(args: ScenarioEditArgs, deps: ScenarioEditDeps): Promise<number>;
|
|
226
233
|
/**
|
|
227
234
|
* A logic file's text. UTF-8, with a byte-order mark dropped — and UTF-16 with
|
|
@@ -59,6 +59,13 @@ export const SCENARIO_EDIT_SUBCOMMANDS = new Set([
|
|
|
59
59
|
export const READ_TOOLS = ["scenario_show", "scenario_edits", "investigate"];
|
|
60
60
|
/** The ones that change a scenario: offered only after `bir scenario editing on` (D2). */
|
|
61
61
|
export const EDIT_TOOLS = ["scenario_check", "scenario_edit", "scenario_undo"];
|
|
62
|
+
/**
|
|
63
|
+
* The ones every session with the `bir` server has, editing on or off: the
|
|
64
|
+
* delivery of a direct plan, and the agent's way to say a replay's results do
|
|
65
|
+
* not answer the request (the service's review of calcscenarioautocorrect.md,
|
|
66
|
+
* item 19). `bir setup`'s allow rule, `mcp__bir`, covers every one.
|
|
67
|
+
*/
|
|
68
|
+
export const BASE_TOOLS = ["run_scenario", "report_wrong_result"];
|
|
62
69
|
const USAGE = {
|
|
63
70
|
show: "usage: bir scenario show <runId|scnId> [--step <n>]",
|
|
64
71
|
check: "usage: bir scenario check <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->] [--unfreeze] [--json]",
|
|
@@ -1067,11 +1074,11 @@ function describeEditing(out, sidecar, cwd) {
|
|
|
1067
1074
|
// working on a scenario, and every other session would pay for their
|
|
1068
1075
|
// definitions on every request without ever calling them.
|
|
1069
1076
|
if (on) {
|
|
1070
|
-
out(` the bir MCP server offers
|
|
1077
|
+
out(` the bir MCP server offers ${[...BASE_TOOLS, ...READ_TOOLS].join(", ")} (they read)`);
|
|
1071
1078
|
out(` and ${EDIT_TOOLS.join(", ")} (they change a scenario)`);
|
|
1072
1079
|
}
|
|
1073
1080
|
else {
|
|
1074
|
-
out(
|
|
1081
|
+
out(` the bir MCP server offers ${BASE_TOOLS.join(", ")}`);
|
|
1075
1082
|
out(` \`bir scenario editing on\` adds ${EDIT_TOOLS.join(", ")} (they change a scenario)`);
|
|
1076
1083
|
out(` and ${READ_TOOLS.join(", ")} (they read)`);
|
|
1077
1084
|
out(" (BIR_SCENARIO_TOOLS=read in the server's environment offers the reading ones alone)");
|
|
@@ -75,6 +75,13 @@ export interface HelloBody {
|
|
|
75
75
|
/** How many tools this server lists now (0 when off). */
|
|
76
76
|
tools: number;
|
|
77
77
|
off: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* It lists `report_wrong_result` (runner 0.3.18 on; the service's review of
|
|
80
|
+
* calcscenarioautocorrect.md, item 19): a replay's results may then tell the
|
|
81
|
+
* agent it can say they are wrong. Sent only when true; an older recorder
|
|
82
|
+
* ignores it.
|
|
83
|
+
*/
|
|
84
|
+
report?: boolean;
|
|
78
85
|
}
|
|
79
86
|
/** A discovery record as written, its pid not yet checked. */
|
|
80
87
|
export interface RecorderRecord {
|
|
@@ -100,10 +107,14 @@ export declare function launchRevive(cwd: string): Promise<void>;
|
|
|
100
107
|
export interface LivenessOptions {
|
|
101
108
|
/** The project this server runs for. */
|
|
102
109
|
cwd: string;
|
|
103
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* What this server lists now. Called for every hello. `report`: the list
|
|
112
|
+
* holds `report_wrong_result` (review item 19).
|
|
113
|
+
*/
|
|
104
114
|
state: () => {
|
|
105
115
|
tools: number;
|
|
106
116
|
off: boolean;
|
|
117
|
+
report?: boolean;
|
|
107
118
|
};
|
|
108
119
|
now?: () => number;
|
|
109
120
|
random?: () => number;
|
|
@@ -314,10 +314,14 @@ export class ScenarioLiveness {
|
|
|
314
314
|
state() {
|
|
315
315
|
try {
|
|
316
316
|
const s = this.opts.state();
|
|
317
|
-
return {
|
|
317
|
+
return {
|
|
318
|
+
tools: Number.isInteger(s.tools) && s.tools >= 0 ? s.tools : 0,
|
|
319
|
+
off: s.off === true,
|
|
320
|
+
report: s.report === true && s.off !== true,
|
|
321
|
+
};
|
|
318
322
|
}
|
|
319
323
|
catch {
|
|
320
|
-
return { tools: 0, off: false };
|
|
324
|
+
return { tools: 0, off: false, report: false };
|
|
321
325
|
}
|
|
322
326
|
}
|
|
323
327
|
async sendHello(why) {
|
|
@@ -330,8 +334,8 @@ export class ScenarioLiveness {
|
|
|
330
334
|
const target = this.target();
|
|
331
335
|
if (!target)
|
|
332
336
|
return;
|
|
333
|
-
const { tools, off } = this.state();
|
|
334
|
-
const body = { pid: process.pid, version: packageVersion(), tools, off };
|
|
337
|
+
const { tools, off, report } = this.state();
|
|
338
|
+
const body = { pid: process.pid, version: packageVersion(), tools, off, ...(report ? { report } : {}) };
|
|
335
339
|
this.tried = target.identity;
|
|
336
340
|
this.lastHelloAt = this.now();
|
|
337
341
|
let reach;
|
package/dist/bin/setup.js
CHANGED
|
@@ -43,7 +43,7 @@ import { normalizePath } from "../control/paths.js";
|
|
|
43
43
|
import { allowRules, BIR_TOOLS_RULE, claudeCodePaths, claudeCodeSetUp } from "../config/adapters/claude-code.js";
|
|
44
44
|
import { allocateReplacementPort, editingEnabled, isScenarioServer, isWrapped, LOOPBACK_NO_PROXY, probeFreePort, proxyConfigured, projectRecord, readSidecar, SCENARIO_SERVER_KEY, setProjectRecord, settleReplaySwitch, writeSidecar, allocateFreeProjectPort, } from "../config/generate.js";
|
|
45
45
|
import { resolveServers } from "../config/resolve.js";
|
|
46
|
-
import { EDIT_TOOLS, READ_TOOLS } from "./scenario-edit.js";
|
|
46
|
+
import { BASE_TOOLS, EDIT_TOOLS, READ_TOOLS } from "./scenario-edit.js";
|
|
47
47
|
import { errText } from "../util/log.js";
|
|
48
48
|
import { offSource } from "../util/off-switch.js";
|
|
49
49
|
import { packageVersion } from "../util/version.js";
|
|
@@ -508,8 +508,8 @@ export async function runSetup(args, deps) {
|
|
|
508
508
|
if (resolveServers(project).some((s) => s.name === SCENARIO_SERVER_KEY && isScenarioServer(s.config))) {
|
|
509
509
|
const asks = !allowRules(claudeCodePaths(project).settingsLocal).includes(BIR_TOOLS_RULE);
|
|
510
510
|
out(editingEnabled(readSidecar(), project)
|
|
511
|
-
? ` tools ${[
|
|
512
|
-
:
|
|
511
|
+
? ` tools ${[...BASE_TOOLS, ...READ_TOOLS, ...EDIT_TOOLS].join(", ")} — ${asks ? "Claude Code asks before each" : "Claude Code runs them without asking, except a forced change"}`
|
|
512
|
+
: ` tools ${BASE_TOOLS.join(" and ")} only — scenario editing is off here (\`bir scenario editing on\` adds investigate and the scenario tools)`);
|
|
513
513
|
}
|
|
514
514
|
if (ensured) {
|
|
515
515
|
const { info, health: h } = ensured.status;
|
package/dist/control/client.d.ts
CHANGED
|
@@ -68,6 +68,11 @@ export declare class ControlClient {
|
|
|
68
68
|
}): Promise<RegisterResult | undefined>;
|
|
69
69
|
/** Report one completed MCP call. Resolves false when the send was dropped. */
|
|
70
70
|
report(step: ProxyStepReport): Promise<boolean>;
|
|
71
|
+
/**
|
|
72
|
+
* The tool list of the server this proxy wraps, as it answered `tools/list`
|
|
73
|
+
* (the service's autocorrect.md AC12). Resolves false when the send was dropped.
|
|
74
|
+
*/
|
|
75
|
+
tools(serverName: string, tools: readonly unknown[]): Promise<boolean>;
|
|
71
76
|
health(): Promise<Record<string, unknown> | undefined>;
|
|
72
77
|
/**
|
|
73
78
|
* Ask the control server to shut down. Resolves true when it agreed; the
|
package/dist/control/client.js
CHANGED
|
@@ -26,6 +26,13 @@ export class ControlClient {
|
|
|
26
26
|
const body = this.identity ? { ...step, proxyId: this.identity.proxyId } : step;
|
|
27
27
|
return (await this.post("/proxy/step", body)) !== undefined;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The tool list of the server this proxy wraps, as it answered `tools/list`
|
|
31
|
+
* (the service's autocorrect.md AC12). Resolves false when the send was dropped.
|
|
32
|
+
*/
|
|
33
|
+
async tools(serverName, tools) {
|
|
34
|
+
return (await this.post("/proxy/tools", { ...this.identity, serverName, tools })) !== undefined;
|
|
35
|
+
}
|
|
29
36
|
async health() {
|
|
30
37
|
return (await this.request("GET", "/health"));
|
|
31
38
|
}
|
|
@@ -49,9 +49,14 @@
|
|
|
49
49
|
* only with enough of the budget left.
|
|
50
50
|
*
|
|
51
51
|
* STDOUT IS CONTEXT. For SessionStart, Claude Code adds whatever a command hook
|
|
52
|
-
* prints on stdout to the model's context
|
|
53
|
-
*
|
|
54
|
-
* is the answer,
|
|
52
|
+
* prints on stdout to the model's context — unless it is one JSON object, which
|
|
53
|
+
* it reads as the hook's answer instead. So nothing is printed unless the
|
|
54
|
+
* control server answered with something, and then it is the answer, as one
|
|
55
|
+
* line of JSON and nothing else. The control server answers `{}`, or — when
|
|
56
|
+
* plans of the account wait for their owner (the service's review of
|
|
57
|
+
* calcscenarioautocorrect.md, item 8) — `{ "systemMessage": "bir: …" }`, a
|
|
58
|
+
* line Claude Code shows the person and never the model. Characters past ASCII
|
|
59
|
+
* are written as `\u` escapes, so a console code page cannot garble the line.
|
|
55
60
|
*/
|
|
56
61
|
import { type EnsureOptions } from "./daemon.js";
|
|
57
62
|
/**
|
|
@@ -93,6 +98,12 @@ export interface EnsureHookOptions extends EnsureOptions {
|
|
|
93
98
|
*/
|
|
94
99
|
overrideCheck?: ((projectCwd: string) => StaleOverrides) | false;
|
|
95
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* `value` as one line of JSON whose every character past ASCII is a `\u`
|
|
103
|
+
* escape: the same JSON to any reader, and nothing a console's code page can
|
|
104
|
+
* turn into something else on the way to Claude Code.
|
|
105
|
+
*/
|
|
106
|
+
export declare function asciiJson(value: unknown): string;
|
|
96
107
|
/**
|
|
97
108
|
* Run the hook for `cwd`. Resolves the exit code, which is always 0.
|
|
98
109
|
*/
|
|
@@ -49,9 +49,14 @@
|
|
|
49
49
|
* only with enough of the budget left.
|
|
50
50
|
*
|
|
51
51
|
* STDOUT IS CONTEXT. For SessionStart, Claude Code adds whatever a command hook
|
|
52
|
-
* prints on stdout to the model's context
|
|
53
|
-
*
|
|
54
|
-
* is the answer,
|
|
52
|
+
* prints on stdout to the model's context — unless it is one JSON object, which
|
|
53
|
+
* it reads as the hook's answer instead. So nothing is printed unless the
|
|
54
|
+
* control server answered with something, and then it is the answer, as one
|
|
55
|
+
* line of JSON and nothing else. The control server answers `{}`, or — when
|
|
56
|
+
* plans of the account wait for their owner (the service's review of
|
|
57
|
+
* calcscenarioautocorrect.md, item 8) — `{ "systemMessage": "bir: …" }`, a
|
|
58
|
+
* line Claude Code shows the person and never the model. Characters past ASCII
|
|
59
|
+
* are written as `\u` escapes, so a console code page cannot garble the line.
|
|
55
60
|
*/
|
|
56
61
|
import { appendFileSync } from "node:fs";
|
|
57
62
|
import { Console } from "node:console";
|
|
@@ -152,6 +157,14 @@ function reportStaleOverrides(projectCwd, logFile, msLeft, check) {
|
|
|
152
157
|
/* a check that failed leaves things as they were; `bir doctor` names what is stale */
|
|
153
158
|
}
|
|
154
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* `value` as one line of JSON whose every character past ASCII is a `\u`
|
|
162
|
+
* escape: the same JSON to any reader, and nothing a console's code page can
|
|
163
|
+
* turn into something else on the way to Claude Code.
|
|
164
|
+
*/
|
|
165
|
+
export function asciiJson(value) {
|
|
166
|
+
return JSON.stringify(value).replace(/[\u007f-]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
167
|
+
}
|
|
155
168
|
/** True when this process's own environment switches the runner off (`BIR_OFF=1` or `true`). */
|
|
156
169
|
function offInThisEnvironment() {
|
|
157
170
|
const value = (process.env.BIR_OFF ?? "").trim().toLowerCase();
|
|
@@ -228,8 +241,8 @@ export async function runEnsureHook(cwd, opts = {}) {
|
|
|
228
241
|
signal: timeoutSignal(Math.max(300, Math.min(RELAY_TIMEOUT_MS, left()))),
|
|
229
242
|
});
|
|
230
243
|
const answer = (await res.json().catch(() => ({})));
|
|
231
|
-
if (res.ok && answer && typeof answer === "object" && Object.keys(answer).length > 0) {
|
|
232
|
-
stdout.write(
|
|
244
|
+
if (res.ok && answer && typeof answer === "object" && !Array.isArray(answer) && Object.keys(answer).length > 0) {
|
|
245
|
+
stdout.write(asciiJson(answer));
|
|
233
246
|
}
|
|
234
247
|
}
|
|
235
248
|
catch (err) {
|