@wrenai/genbi 0.0.5 → 0.0.6
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/README.md +1 -1
- package/dist-server/harness/route/codex-ask.d.ts +1 -1
- package/dist-server/harness/route/codex-ask.js +5 -46
- package/dist-server/harness/route/dispatched.d.ts +20 -0
- package/dist-server/harness/route/dispatched.js +122 -24
- package/dist-server/harness/route/host-query.d.ts +93 -0
- package/dist-server/harness/route/{codex-host-query.js → host-query.js} +120 -54
- package/dist-server/harness/route/route.js +3 -0
- package/dist-server/harness/route/types.d.ts +18 -1
- package/dist-server/harness/route/{codex-wren-proxy.js → wren-host-proxy.js} +6 -5
- package/dist-server/harness/setup/runner.d.ts +15 -3
- package/dist-server/harness/setup/runner.js +234 -72
- package/dist-server/harness/tools/setup-native.d.ts +7 -0
- package/dist-server/harness/tools/setup-native.js +7 -4
- package/dist-server/server/app.js +13 -4
- package/dist-server/server/bin.js +8 -0
- package/dist-server/server/fold.d.ts +9 -8
- package/dist-server/server/fold.js +17 -12
- package/dist-server/server/host-evidence-grounding.d.ts +21 -0
- package/dist-server/server/{codex-local-grounding.js → host-evidence-grounding.js} +15 -14
- package/dist-server/server/native-artifacts.d.ts +93 -4
- package/dist-server/server/native-artifacts.js +153 -4
- package/dist-server/server/turn.js +4 -1
- package/package.json +2 -2
- package/dist-server/harness/route/codex-host-query.d.ts +0 -52
- package/dist-server/server/codex-local-grounding.d.ts +0 -18
- /package/dist-server/harness/route/{codex-wren-proxy.d.ts → wren-host-proxy.d.ts} +0 -0
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { accessSync, existsSync, constants as fsConstants } from "node:fs";
|
|
2
3
|
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import { createServer } from "node:net";
|
|
4
5
|
import os from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
6
8
|
import { COMPONENT_LIMITS } from "../components/runner.js";
|
|
7
9
|
import { GovernedWrenError } from "../components/wren-access.js";
|
|
8
10
|
/**
|
|
9
|
-
* Per-turn
|
|
10
|
-
* the total caps what the host keeps in memory for one
|
|
11
|
-
* Rows default to 1000 like `run_sql`. Like `run_sql`, the host
|
|
12
|
-
* report truncation; the cap is 9999 so the probe stays within
|
|
11
|
+
* Per-scope bounds (one Ask turn, or one native session). Count, request and per-result bytes
|
|
12
|
+
* reuse the component runner's limits; the total caps what the host keeps in memory for one scope
|
|
13
|
+
* until its answer is grounded. Rows default to 1000 like `run_sql`. Like `run_sql`, the host
|
|
14
|
+
* probes one row past the limit to report truncation; the cap is 9999 so the probe stays within
|
|
15
|
+
* governed-stdio's 10000-row bound.
|
|
13
16
|
*/
|
|
14
|
-
export const
|
|
17
|
+
export const HOST_QUERY_LIMITS = Object.freeze({
|
|
15
18
|
queries: COMPONENT_LIMITS.calls,
|
|
16
19
|
requestBytes: COMPONENT_LIMITS.requestBytes,
|
|
17
20
|
resultBytes: COMPONENT_LIMITS.resultBytes,
|
|
@@ -20,58 +23,35 @@ export const CODEX_HOST_QUERY_LIMITS = Object.freeze({
|
|
|
20
23
|
maxRows: 9_999,
|
|
21
24
|
timeoutMs: COMPONENT_LIMITS.timeoutMs,
|
|
22
25
|
});
|
|
23
|
-
function requestFrame(value) {
|
|
24
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
25
|
-
return undefined;
|
|
26
|
-
const record = value;
|
|
27
|
-
if (typeof record.id !== "number" || !Number.isSafeInteger(record.id))
|
|
28
|
-
return undefined;
|
|
29
|
-
return { token: record.token, id: record.id, sql: record.sql, limit: record.limit };
|
|
30
|
-
}
|
|
31
26
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* governed access (`wren governed-stdio`), gets a turn-unique host `query_id`, and is recorded
|
|
35
|
-
* as an observation of this turn only.
|
|
27
|
+
* Every query runs through governed access (`wren governed-stdio`), gets a scope-unique host
|
|
28
|
+
* `query_id` (`q<N>-<tag>`), and is recorded as an observation of this scope only.
|
|
36
29
|
*/
|
|
37
|
-
export
|
|
38
|
-
|
|
39
|
-
throw new Error("codex:local host query service requires unix sockets");
|
|
40
|
-
const directory = await mkdtemp(path.join(os.tmpdir(), "genbi-hq-"));
|
|
41
|
-
const token = Buffer.from(randomBytes(32).toString("base64url"), "utf8");
|
|
42
|
-
const turnTag = randomBytes(4).toString("hex");
|
|
43
|
-
const socketPath = path.join(directory, "s");
|
|
44
|
-
const credentialFile = path.join(directory, "credential.json");
|
|
30
|
+
export function createHostQueryRecorder(options) {
|
|
31
|
+
const tag = randomBytes(4).toString("hex");
|
|
45
32
|
const controller = new AbortController();
|
|
46
33
|
const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
47
34
|
const recorded = [];
|
|
48
|
-
const sockets = new Set();
|
|
49
35
|
let attempts = 0;
|
|
50
36
|
let recordedBytes = 0;
|
|
51
37
|
let closed = false;
|
|
52
38
|
let access;
|
|
53
|
-
const authorized = (candidate) => {
|
|
54
|
-
if (typeof candidate !== "string")
|
|
55
|
-
return false;
|
|
56
|
-
const presented = Buffer.from(candidate, "utf8");
|
|
57
|
-
return presented.length === token.length && timingSafeEqual(presented, token);
|
|
58
|
-
};
|
|
59
39
|
const execute = async (sql, limit) => {
|
|
60
40
|
if (closed || signal.aborted)
|
|
61
41
|
return { error: "transport" };
|
|
62
|
-
if (typeof sql !== "string" || !sql.trim() || Buffer.byteLength(sql) >
|
|
42
|
+
if (typeof sql !== "string" || !sql.trim() || Buffer.byteLength(sql) > HOST_QUERY_LIMITS.requestBytes)
|
|
63
43
|
return { error: "invalid_request" };
|
|
64
44
|
if (limit !== undefined && limit !== null && (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1))
|
|
65
45
|
return { error: "invalid_request" };
|
|
66
|
-
if (attempts >=
|
|
46
|
+
if (attempts >= HOST_QUERY_LIMITS.queries)
|
|
67
47
|
return { error: "budget_exhausted" };
|
|
68
48
|
attempts += 1;
|
|
69
|
-
const rows = Math.min(
|
|
49
|
+
const rows = Math.min(HOST_QUERY_LIMITS.maxRows, typeof limit === "number" ? limit : HOST_QUERY_LIMITS.defaultRows);
|
|
70
50
|
let result;
|
|
71
51
|
try {
|
|
72
52
|
access ??= options.openAccess(signal);
|
|
73
53
|
const governed = await access;
|
|
74
|
-
result = await governed.query({ sql, limit: rows + 1 }, AbortSignal.any([signal, AbortSignal.timeout(
|
|
54
|
+
result = await governed.query({ sql, limit: rows + 1 }, AbortSignal.any([signal, AbortSignal.timeout(HOST_QUERY_LIMITS.timeoutMs)]));
|
|
75
55
|
}
|
|
76
56
|
catch (error) {
|
|
77
57
|
return { error: error instanceof GovernedWrenError ? error.errorClass : "transport" };
|
|
@@ -88,17 +68,65 @@ export async function openCodexHostQueryService(options) {
|
|
|
88
68
|
const truncated = probed.length > rows;
|
|
89
69
|
const shown = truncated ? probed.slice(0, rows) : probed;
|
|
90
70
|
// The id is assigned here, once, so the observation and the model's copy carry the same id. The
|
|
91
|
-
// tag makes ids unique per
|
|
92
|
-
const output = { ...result, rows: shown, row_count: shown.length, truncated, query_id: `q${attempts}-${
|
|
71
|
+
// tag makes ids unique per scope: an id copied from another turn or session never names this one's query.
|
|
72
|
+
const output = { ...result, rows: shown, row_count: shown.length, truncated, query_id: `q${attempts}-${tag}` };
|
|
93
73
|
const bytes = Buffer.byteLength(JSON.stringify(output));
|
|
94
|
-
if (bytes >
|
|
74
|
+
if (bytes > HOST_QUERY_LIMITS.resultBytes)
|
|
95
75
|
return { error: "result_too_large" };
|
|
96
|
-
if (recordedBytes + bytes >
|
|
76
|
+
if (recordedBytes + bytes > HOST_QUERY_LIMITS.totalResultBytes)
|
|
97
77
|
return { error: "budget_exhausted" };
|
|
98
78
|
recordedBytes += bytes;
|
|
99
79
|
recorded.push(Object.freeze({ tool: "run_sql", input: Object.freeze({ sql, limit: rows }), output: structuredClone(output) }));
|
|
100
80
|
return { result: output };
|
|
101
81
|
};
|
|
82
|
+
let closing;
|
|
83
|
+
const close = () => {
|
|
84
|
+
closing ??= (async () => {
|
|
85
|
+
closed = true;
|
|
86
|
+
recorded.length = 0;
|
|
87
|
+
controller.abort();
|
|
88
|
+
try {
|
|
89
|
+
if (access)
|
|
90
|
+
await (await access).close();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Access that never opened, or already closed, has nothing left to release.
|
|
94
|
+
}
|
|
95
|
+
})();
|
|
96
|
+
return closing;
|
|
97
|
+
};
|
|
98
|
+
options.signal?.addEventListener("abort", () => { void close(); }, { once: true });
|
|
99
|
+
return { execute, observations: () => Object.freeze([...recorded]), close };
|
|
100
|
+
}
|
|
101
|
+
function requestFrame(value) {
|
|
102
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
103
|
+
return undefined;
|
|
104
|
+
const record = value;
|
|
105
|
+
if (typeof record.id !== "number" || !Number.isSafeInteger(record.id))
|
|
106
|
+
return undefined;
|
|
107
|
+
return { token: record.token, id: record.id, sql: record.sql, limit: record.limit };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The host side of a subscription turn's `run_sql` (codex:local and the Claude agent-sdk Ask):
|
|
111
|
+
* a per-turn unix socket in a fresh 0700 directory that answers only requests carrying this
|
|
112
|
+
* turn's random token, in front of one {@link createHostQueryRecorder} for the turn.
|
|
113
|
+
*/
|
|
114
|
+
export async function openHostQueryService(options) {
|
|
115
|
+
if (process.platform === "win32")
|
|
116
|
+
throw new Error("the host query service requires unix sockets");
|
|
117
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "genbi-hq-"));
|
|
118
|
+
const token = Buffer.from(randomBytes(32).toString("base64url"), "utf8");
|
|
119
|
+
const socketPath = path.join(directory, "s");
|
|
120
|
+
const credentialFile = path.join(directory, "credential.json");
|
|
121
|
+
const recorder = createHostQueryRecorder(options);
|
|
122
|
+
const sockets = new Set();
|
|
123
|
+
let closed = false;
|
|
124
|
+
const authorized = (candidate) => {
|
|
125
|
+
if (typeof candidate !== "string")
|
|
126
|
+
return false;
|
|
127
|
+
const presented = Buffer.from(candidate, "utf8");
|
|
128
|
+
return presented.length === token.length && timingSafeEqual(presented, token);
|
|
129
|
+
};
|
|
102
130
|
const serve = (socket) => {
|
|
103
131
|
if (closed) {
|
|
104
132
|
socket.destroy();
|
|
@@ -112,7 +140,7 @@ export async function openCodexHostQueryService(options) {
|
|
|
112
140
|
socket.setEncoding("utf8");
|
|
113
141
|
socket.on("data", (chunk) => {
|
|
114
142
|
buffer += chunk;
|
|
115
|
-
if (buffer.length > 2 *
|
|
143
|
+
if (buffer.length > 2 * HOST_QUERY_LIMITS.requestBytes) {
|
|
116
144
|
socket.destroy();
|
|
117
145
|
return;
|
|
118
146
|
}
|
|
@@ -138,7 +166,7 @@ export async function openCodexHostQueryService(options) {
|
|
|
138
166
|
return;
|
|
139
167
|
}
|
|
140
168
|
tail = tail.then(async () => {
|
|
141
|
-
const outcome = await execute(sql, limit);
|
|
169
|
+
const outcome = await recorder.execute(sql, limit);
|
|
142
170
|
if (socket.destroyed)
|
|
143
171
|
return;
|
|
144
172
|
socket.write(`${JSON.stringify("result" in outcome ? { id, result: outcome.result } : { id, error: { class: outcome.error } })}\n`);
|
|
@@ -157,6 +185,7 @@ export async function openCodexHostQueryService(options) {
|
|
|
157
185
|
}
|
|
158
186
|
catch (error) {
|
|
159
187
|
server.close();
|
|
188
|
+
await recorder.close();
|
|
160
189
|
await rm(directory, { recursive: true, force: true });
|
|
161
190
|
throw error;
|
|
162
191
|
}
|
|
@@ -164,17 +193,11 @@ export async function openCodexHostQueryService(options) {
|
|
|
164
193
|
const close = () => {
|
|
165
194
|
closing ??= (async () => {
|
|
166
195
|
closed = true;
|
|
167
|
-
recorded.length = 0;
|
|
168
|
-
controller.abort();
|
|
169
196
|
for (const socket of sockets)
|
|
170
197
|
socket.destroy();
|
|
171
198
|
await new Promise((resolve) => server.close(() => resolve()));
|
|
172
199
|
try {
|
|
173
|
-
|
|
174
|
-
await (await access).close();
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
// Access that never opened, or already closed, has nothing left to release.
|
|
200
|
+
await recorder.close();
|
|
178
201
|
}
|
|
179
202
|
finally {
|
|
180
203
|
await rm(directory, { recursive: true, force: true });
|
|
@@ -183,9 +206,52 @@ export async function openCodexHostQueryService(options) {
|
|
|
183
206
|
return closing;
|
|
184
207
|
};
|
|
185
208
|
options.signal?.addEventListener("abort", () => { void close(); }, { once: true });
|
|
186
|
-
return {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
209
|
+
return { directory, credentialFile, observations: recorder.observations, close };
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The argv after the proxy command: the proxy script, this turn's credential file, then the
|
|
213
|
+
* `wren serve mcp` command it fronts. Shared by codex:local's `--server-arg`s and the agent-sdk
|
|
214
|
+
* host MCP config so the two cannot drift.
|
|
215
|
+
*/
|
|
216
|
+
export function wrenHostProxyArgs(options) {
|
|
217
|
+
return [
|
|
218
|
+
...options.proxy.prefixArgs,
|
|
219
|
+
"--credential-file",
|
|
220
|
+
options.credentialFile,
|
|
221
|
+
"--",
|
|
222
|
+
options.mcpServer.command,
|
|
223
|
+
...options.mcpServer.prefixArgs,
|
|
224
|
+
"serve",
|
|
225
|
+
"mcp",
|
|
226
|
+
"--project",
|
|
227
|
+
options.userProject,
|
|
228
|
+
"--quiet",
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
/** The built proxy next to this module, else its source (Node strips its erasable types). */
|
|
232
|
+
export function defaultWrenHostProxy() {
|
|
233
|
+
const built = fileURLToPath(new URL("./wren-host-proxy.js", import.meta.url));
|
|
234
|
+
const script = existsSync(built) ? built : fileURLToPath(new URL("./wren-host-proxy.ts", import.meta.url));
|
|
235
|
+
return { command: process.execPath, prefixArgs: [script] };
|
|
236
|
+
}
|
|
237
|
+
/** Governed access spawns one executable with fixed arguments; a prefixed command cannot be bound. */
|
|
238
|
+
export function singleWrenExecutable(cli) {
|
|
239
|
+
if (cli.prefixArgs.length > 0)
|
|
240
|
+
throw new Error("host queries need a standalone wren executable");
|
|
241
|
+
return cli.command;
|
|
242
|
+
}
|
|
243
|
+
export function resolveExecutableOnPath(name) {
|
|
244
|
+
for (const directory of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
245
|
+
if (!directory)
|
|
246
|
+
continue;
|
|
247
|
+
const candidate = path.join(directory, name);
|
|
248
|
+
try {
|
|
249
|
+
accessSync(candidate, fsConstants.X_OK);
|
|
250
|
+
return candidate;
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// Keep searching.
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
throw new Error(`could not find the "${name}" executable on PATH`);
|
|
191
257
|
}
|
|
@@ -86,6 +86,9 @@ export async function route(options) {
|
|
|
86
86
|
...(options.agentId !== undefined ? { agentId: options.agentId } : {}),
|
|
87
87
|
...(options.chatTimeoutMs !== undefined ? { chatTimeoutMs: options.chatTimeoutMs } : {}),
|
|
88
88
|
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
89
|
+
// An Ask turn: its data queries can run through a per-turn host query service, so the
|
|
90
|
+
// answer can be grounded on host evidence (see `withHostVerification`).
|
|
91
|
+
hostQueries: {},
|
|
89
92
|
});
|
|
90
93
|
return { backend: "agent-sdk", warnings, ...result };
|
|
91
94
|
}
|
|
@@ -177,6 +177,18 @@ export interface DispatchedOptions {
|
|
|
177
177
|
* flag) when unset, which is the pre-existing behavior.
|
|
178
178
|
*/
|
|
179
179
|
readonly resumeSessionId?: string;
|
|
180
|
+
/**
|
|
181
|
+
* Ask turns only (`route()` sets it; the setup and enrichment runners do not): open a per-turn
|
|
182
|
+
* host query service and hand the dispatcher a host MCP server (`--host-mcp-config`) whose
|
|
183
|
+
* `run_sql` executes through it, so the answer can be grounded on what the host ran. `wren` is
|
|
184
|
+
* the standalone Wren executable for both governed access and `wren serve mcp`; when unset it
|
|
185
|
+
* is resolved on PATH, and when none is found the turn runs without host queries (and its
|
|
186
|
+
* answer stays unverified). Ignored under `modelsConfig`: warble refuses a host MCP for a
|
|
187
|
+
* hybrid-staged plan, which a models config can produce.
|
|
188
|
+
*/
|
|
189
|
+
readonly hostQueries?: {
|
|
190
|
+
readonly wren?: string;
|
|
191
|
+
};
|
|
180
192
|
/** Cancels the owned dispatcher process without falling back to another vendor. */
|
|
181
193
|
readonly signal?: AbortSignal;
|
|
182
194
|
}
|
|
@@ -191,6 +203,11 @@ export interface DispatchedResult {
|
|
|
191
203
|
* the SDK itself never produced a session id to report.
|
|
192
204
|
*/
|
|
193
205
|
readonly sessionId?: string | null;
|
|
206
|
+
/**
|
|
207
|
+
* Ask turns with host queries only: the queries this turn's host query service executed for
|
|
208
|
+
* the model's host `run_sql`, the only evidence its answer may be grounded on. Never persisted.
|
|
209
|
+
*/
|
|
210
|
+
readonly hostObservations?: readonly import("./host-query.js").HostQueryObservation[];
|
|
194
211
|
}
|
|
195
212
|
export type DispatchedExecutor = (options: DispatchedOptions) => Promise<DispatchedResult>;
|
|
196
213
|
/** Codex subscription Ask runs through warble-codex-local, never the Claude dispatched dispatcher. */
|
|
@@ -227,7 +244,7 @@ export interface CodexAskResult {
|
|
|
227
244
|
* codex:local only: the queries this turn's host query service executed for the model's
|
|
228
245
|
* `run_sql`, the only evidence its answer may be grounded on. Never persisted.
|
|
229
246
|
*/
|
|
230
|
-
readonly hostObservations?: readonly import("./
|
|
247
|
+
readonly hostObservations?: readonly import("./host-query.js").HostQueryObservation[];
|
|
231
248
|
}
|
|
232
249
|
export type CodexAskExecutor = (options: CodexAskOptions) => Promise<CodexAskResult>;
|
|
233
250
|
/**
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* The Wren MCP server of a subscription Ask turn (codex:local, and the Claude agent-sdk host MCP):
|
|
4
|
+
* a stdio proxy in front of `wren serve mcp`.
|
|
4
5
|
*
|
|
5
|
-
* node
|
|
6
|
+
* node wren-host-proxy.js --credential-file <abs path> -- <wren> serve mcp --project <p> --quiet
|
|
6
7
|
*
|
|
7
8
|
* Every JSON-RPC line passes through to the child `wren serve mcp` unchanged, so the model sees
|
|
8
9
|
* the same tool names and schemas, except a `tools/call` of `run_sql`: that goes to this turn's
|
|
9
|
-
* host query service (see `
|
|
10
|
+
* host query service (see `host-query.ts`), which executes it through governed access and
|
|
10
11
|
* records it as host evidence. The credential file (0600, owned by this user) names the turn's
|
|
11
12
|
* unix socket and token; without a valid one, `run_sql` is refused rather than run elsewhere.
|
|
12
13
|
*
|
|
@@ -146,7 +147,7 @@ export class HostConnection {
|
|
|
146
147
|
function parseArgs(argv) {
|
|
147
148
|
const separator = argv.indexOf("--");
|
|
148
149
|
if (separator < 0 || separator === argv.length - 1) {
|
|
149
|
-
process.stderr.write("
|
|
150
|
+
process.stderr.write("wren host proxy: usage: --credential-file <path> -- <command> [args...]\n");
|
|
150
151
|
process.exit(2);
|
|
151
152
|
}
|
|
152
153
|
const own = argv.slice(0, separator);
|
|
@@ -160,7 +161,7 @@ function main() {
|
|
|
160
161
|
const host = credential ? new HostConnection(credential) : undefined;
|
|
161
162
|
const child = spawn(options.command, options.args, { stdio: ["pipe", "pipe", "inherit"] });
|
|
162
163
|
child.on("error", (error) => {
|
|
163
|
-
process.stderr.write(`
|
|
164
|
+
process.stderr.write(`wren host proxy: could not start the Wren MCP server: ${error.message}\n`);
|
|
164
165
|
process.exit(1);
|
|
165
166
|
});
|
|
166
167
|
child.on("exit", (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
|
|
@@ -423,6 +423,15 @@ export interface HostContractDiagnostic {
|
|
|
423
423
|
/** The concrete artifact/workflow conditions that must be true before another claimed success. */
|
|
424
424
|
readonly expectedArtifactContract: readonly string[];
|
|
425
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* Where the bound project lives, so a recorded command's optional
|
|
428
|
+
* `cd <dir> &&` prefix can be resolved against it. Absent binding means the
|
|
429
|
+
* prefix cannot be verified and is not accepted.
|
|
430
|
+
*/
|
|
431
|
+
export interface ProjectBinding {
|
|
432
|
+
readonly root: string;
|
|
433
|
+
readonly projectDir: string;
|
|
434
|
+
}
|
|
426
435
|
export type RecordedSchemaDiscovery = {
|
|
427
436
|
readonly kind: "successful";
|
|
428
437
|
readonly command: string;
|
|
@@ -441,7 +450,7 @@ export type RecordedSchemaDiscovery = {
|
|
|
441
450
|
* this for both terminal acceptance and the no-introspection diagnostic,
|
|
442
451
|
* rather than reimplementing the allowlist elsewhere.
|
|
443
452
|
*/
|
|
444
|
-
export declare function classifyRecordedSchemaDiscovery(worklog: readonly SetupWorklogEntry[]): RecordedSchemaDiscovery;
|
|
453
|
+
export declare function classifyRecordedSchemaDiscovery(worklog: readonly SetupWorklogEntry[], binding?: ProjectBinding): RecordedSchemaDiscovery;
|
|
445
454
|
/** Ordered, host-recorded progress; each value includes all preceding work. */
|
|
446
455
|
export type ContextLifecyclePrefix = "none" | "discovery" | "validate" | "build";
|
|
447
456
|
export type RecordedContextLifecycle = {
|
|
@@ -451,12 +460,15 @@ export type RecordedContextLifecycle = {
|
|
|
451
460
|
} | {
|
|
452
461
|
readonly kind: "validate_missing";
|
|
453
462
|
readonly observed: string;
|
|
463
|
+
readonly unrecognised?: string;
|
|
454
464
|
} | {
|
|
455
465
|
readonly kind: "build_missing";
|
|
456
466
|
readonly observed: string;
|
|
467
|
+
readonly unrecognised?: string;
|
|
457
468
|
} | {
|
|
458
469
|
readonly kind: "out_of_order";
|
|
459
470
|
readonly observed: string;
|
|
471
|
+
readonly unrecognised?: string;
|
|
460
472
|
};
|
|
461
473
|
/**
|
|
462
474
|
* Returns the greatest ordered prefix proved by the supplied successful tool
|
|
@@ -464,8 +476,8 @@ export type RecordedContextLifecycle = {
|
|
|
464
476
|
* call in the same attempt is out of order, so a corrective attempt can ask
|
|
465
477
|
* only for the unproven suffix.
|
|
466
478
|
*/
|
|
467
|
-
export declare function recordedContextLifecyclePrefix(worklog: readonly SetupWorklogEntry[], prior?: ContextLifecyclePrefix): ContextLifecyclePrefix;
|
|
468
|
-
export declare function classifyRecordedContextLifecycle(worklog: readonly SetupWorklogEntry[], prior?: ContextLifecyclePrefix): RecordedContextLifecycle;
|
|
479
|
+
export declare function recordedContextLifecyclePrefix(worklog: readonly SetupWorklogEntry[], prior?: ContextLifecyclePrefix, binding?: ProjectBinding): ContextLifecyclePrefix;
|
|
480
|
+
export declare function classifyRecordedContextLifecycle(worklog: readonly SetupWorklogEntry[], prior?: ContextLifecyclePrefix, binding?: ProjectBinding): RecordedContextLifecycle;
|
|
469
481
|
/**
|
|
470
482
|
* Parses the LAST `SETUP_STATUS: ok|needs_input|error` line out of a setup
|
|
471
483
|
* turn's finalText (the terminal contract lives in the composed
|