@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.
Files changed (27) hide show
  1. package/README.md +1 -1
  2. package/dist-server/harness/route/codex-ask.d.ts +1 -1
  3. package/dist-server/harness/route/codex-ask.js +5 -46
  4. package/dist-server/harness/route/dispatched.d.ts +20 -0
  5. package/dist-server/harness/route/dispatched.js +122 -24
  6. package/dist-server/harness/route/host-query.d.ts +93 -0
  7. package/dist-server/harness/route/{codex-host-query.js → host-query.js} +120 -54
  8. package/dist-server/harness/route/route.js +3 -0
  9. package/dist-server/harness/route/types.d.ts +18 -1
  10. package/dist-server/harness/route/{codex-wren-proxy.js → wren-host-proxy.js} +6 -5
  11. package/dist-server/harness/setup/runner.d.ts +15 -3
  12. package/dist-server/harness/setup/runner.js +234 -72
  13. package/dist-server/harness/tools/setup-native.d.ts +7 -0
  14. package/dist-server/harness/tools/setup-native.js +7 -4
  15. package/dist-server/server/app.js +13 -4
  16. package/dist-server/server/bin.js +8 -0
  17. package/dist-server/server/fold.d.ts +9 -8
  18. package/dist-server/server/fold.js +17 -12
  19. package/dist-server/server/host-evidence-grounding.d.ts +21 -0
  20. package/dist-server/server/{codex-local-grounding.js → host-evidence-grounding.js} +15 -14
  21. package/dist-server/server/native-artifacts.d.ts +93 -4
  22. package/dist-server/server/native-artifacts.js +153 -4
  23. package/dist-server/server/turn.js +4 -1
  24. package/package.json +2 -2
  25. package/dist-server/harness/route/codex-host-query.d.ts +0 -52
  26. package/dist-server/server/codex-local-grounding.d.ts +0 -18
  27. /package/dist-server/harness/route/{codex-wren-proxy.d.ts → wren-host-proxy.d.ts} +0 -0
@@ -124,6 +124,22 @@ function resolveDispatchedUserProject(runOptions, agentId) {
124
124
  }
125
125
  return canonicalProject;
126
126
  }
127
+ /**
128
+ * The project directory a project-bound step (`connect_resume`/`context`) runs
129
+ * against, as the default exec cwd of the in-process `setup_execution` tool.
130
+ * `undefined` for the initial `connect` step, which scaffolds the project from
131
+ * the workspace root, and for a missing/unsafe project name. The tool applies
132
+ * it only while the directory actually exists.
133
+ */
134
+ function boundSetupProjectDir(runOptions, agentId) {
135
+ const stepKey = runOptions.stepKey ?? (agentId === BUILD_CONTEXT_AGENT_ID ? "context" : "connect");
136
+ if (stepKey === "connect")
137
+ return undefined;
138
+ const projectName = runOptions.projectName;
139
+ if (projectName === undefined || !SAFE_PROJECT_NAME.test(projectName))
140
+ return undefined;
141
+ return path.resolve(runOptions.workspaceRoot, projectName);
142
+ }
127
143
  /**
128
144
  * Dispatches a setup-wizard turn over dispatched, bypassing `route()` entirely
129
145
  * (unlike the Ask turn path) so it can perform its own, setup-specific
@@ -562,8 +578,14 @@ export class InProcessSetupRunner {
562
578
  // project-root write scope on the dispatched side.
563
579
  const policy = { readOnly: false, artifactWriteScope: "." };
564
580
  const env = createLocalExecutionEnv({ rootDir: runOptions.workspaceRoot });
581
+ const boundProjectDir = boundSetupProjectDir(runOptions, agentId);
565
582
  const nativeTools = createNativeToolRegistry();
566
- nativeTools.register(SETUP_EXECUTION_TOOL_NAME, () => createSetupExecutionTool({ env, policy, workspaceRoot: runOptions.workspaceRoot }));
583
+ nativeTools.register(SETUP_EXECUTION_TOOL_NAME, () => createSetupExecutionTool({
584
+ env,
585
+ policy,
586
+ workspaceRoot: runOptions.workspaceRoot,
587
+ ...(boundProjectDir !== undefined ? { defaultExecCwd: boundProjectDir } : {}),
588
+ }));
567
589
  // Kept in lockstep with `effectiveMaxTurns()` above via the shared
568
590
  // `resolveEffectiveMaxTurns` helper, exactly like `DispatchedSetupRunner.run()`
569
591
  // does for its own `--max-turns` flag.
@@ -1052,6 +1074,64 @@ const SCHEMA_INTROSPECTION_COMMANDS = [
1052
1074
  /\b(?:psql|mysql|sqlite3|duckdb|snowsql|clickhouse-client|trino(?:-cli)?)\b[\s\S]*(?:\s(?:-c|--command|--execute|--query)\s|\b(?:information_schema|show\s+(?:tables|columns)|describe\s+(?:table|schema)|\.tables)\b)/i,
1053
1075
  /\bbq\s+query\b/i,
1054
1076
  ];
1077
+ const CD_PREFIX = /^\s*cd\s+("[^"$`\\]*"|'[^']*'|[^\s;&|<>()$`\\'"~]+)\s*&&\s*([\s\S]*)$/;
1078
+ const STARTS_WITH_CD = /^\s*cd(?:\s|$)/;
1079
+ /**
1080
+ * A recorded relative `cwd` resolves against the tool's own root: the workspace
1081
+ * for the in-process tool, the project for Codex local (whose tool root is the
1082
+ * project directory for project steps).
1083
+ */
1084
+ function recordedCwdBase(entry, cwd, binding) {
1085
+ return path.resolve(entry.label === CODEX_SETUP_EXECUTION_TOOL_NAME ? binding.projectDir : binding.root, cwd);
1086
+ }
1087
+ function samePath(a, b) {
1088
+ const resolvedA = path.resolve(a);
1089
+ const resolvedB = path.resolve(b);
1090
+ if (resolvedA === resolvedB)
1091
+ return true;
1092
+ try {
1093
+ return realpathSync(resolvedA) === realpathSync(resolvedB);
1094
+ }
1095
+ catch {
1096
+ return false;
1097
+ }
1098
+ }
1099
+ /**
1100
+ * The ONE recogniser rule shared by schema discovery and the context
1101
+ * lifecycle: a recorded command counts only as the bare command or as exactly
1102
+ * one `cd <dir> && <cmd>` whose `<dir>` is the bound project. Returns the
1103
+ * command to match against the per-stage patterns (the `cd` prefix stripped),
1104
+ * or `undefined` when the shape is not recognised (another directory, a second
1105
+ * `cd`, a dynamic or `~` directory, or no binding to verify against).
1106
+ *
1107
+ * `<dir>` resolves against the exec's explicit `cwd` when the entry recorded
1108
+ * one. Otherwise the effective cwd was not recorded, so the target is accepted
1109
+ * if it names the project from either candidate default (workspace root, the
1110
+ * pre-binding default; or the project itself, the post-binding default). The
1111
+ * entry already exited 0, so the `cd` really resolved to that directory.
1112
+ */
1113
+ function projectLocalCommand(entry, command, binding) {
1114
+ const explicitCwd = entry.input?.cwd;
1115
+ const recordedCwd = typeof explicitCwd === "string" && explicitCwd.length > 0 ? explicitCwd : undefined;
1116
+ if (!STARTS_WITH_CD.test(command)) {
1117
+ if (binding === undefined || recordedCwd === undefined)
1118
+ return command;
1119
+ // A recorded cwd must itself be the project for a bare command to count.
1120
+ return samePath(recordedCwdBase(entry, recordedCwd, binding), binding.projectDir) ? command : undefined;
1121
+ }
1122
+ if (binding === undefined)
1123
+ return undefined;
1124
+ const match = CD_PREFIX.exec(command);
1125
+ if (!match)
1126
+ return undefined;
1127
+ const raw = match[1];
1128
+ const dir = raw.startsWith('"') || raw.startsWith("'") ? raw.slice(1, -1) : raw;
1129
+ const inner = match[2];
1130
+ if (dir.length === 0 || STARTS_WITH_CD.test(inner))
1131
+ return undefined;
1132
+ const bases = recordedCwd !== undefined ? [recordedCwdBase(entry, recordedCwd, binding)] : [binding.root, binding.projectDir];
1133
+ return bases.some((base) => samePath(path.resolve(base, dir), binding.projectDir)) ? inner : undefined;
1134
+ }
1055
1135
  /**
1056
1136
  * The single setup-workflow contract for recognisable schema discovery. A
1057
1137
  * matching command is evidence only after it completed successfully (see
@@ -1060,13 +1140,18 @@ const SCHEMA_INTROSPECTION_COMMANDS = [
1060
1140
  * this for both terminal acceptance and the no-introspection diagnostic,
1061
1141
  * rather than reimplementing the allowlist elsewhere.
1062
1142
  */
1063
- export function classifyRecordedSchemaDiscovery(worklog) {
1143
+ export function classifyRecordedSchemaDiscovery(worklog, binding) {
1064
1144
  let failed;
1065
1145
  for (const entry of worklog) {
1066
1146
  const command = isSetupExecutionEntry(entry) ? execCommandOf(entry) : undefined;
1067
- if (command === undefined ||
1068
- !SCHEMA_DISCOVERY_EVIDENCE.test(command) ||
1069
- !SCHEMA_INTROSPECTION_COMMANDS.some((pattern) => pattern.test(command))) {
1147
+ if (command === undefined)
1148
+ continue;
1149
+ // Without a binding (legacy callers) the matching below is unchanged; with
1150
+ // one, a leading `cd` must satisfy the shared project-local rule.
1151
+ const recognised = binding === undefined ? command : projectLocalCommand(entry, command, binding);
1152
+ if (recognised === undefined ||
1153
+ !SCHEMA_DISCOVERY_EVIDENCE.test(recognised) ||
1154
+ !SCHEMA_INTROSPECTION_COMMANDS.some((pattern) => pattern.test(recognised))) {
1070
1155
  continue;
1071
1156
  }
1072
1157
  const succeeded = execSucceeded(entry);
@@ -1087,7 +1172,7 @@ const DIRECT_CONTEXT_LIFECYCLE_COMMAND = new RegExp(String.raw `^\s*(?:(?:${SHEL
1087
1172
  * context build`, each recorded as a successful setup execution. An artifact
1088
1173
  * alone or merely naming a command in the final message is not evidence.
1089
1174
  */
1090
- function contextLifecycleStage(command) {
1175
+ function contextLifecycleStage(entry, command, binding) {
1091
1176
  // This evidence must describe the actual lifecycle invocation, rather than
1092
1177
  // merely mention it. `setup_execution` already supplies the project cwd as
1093
1178
  // a separate structured field; the only shell prefixes we accept here are
@@ -1096,7 +1181,12 @@ function contextLifecycleStage(command) {
1096
1181
  // command cwd to the canonical project, so this is the same project-local
1097
1182
  // lifecycle operation emitted by some Wren guidance. Arbitrary paths, other
1098
1183
  // flags, shell sequencing, quoted text, and wrappers remain unrecognized.
1099
- const match = DIRECT_CONTEXT_LIFECYCLE_COMMAND.exec(command);
1184
+ // The one additional shape is a single `cd <project> &&` prefix, decided by
1185
+ // the shared `projectLocalCommand` rule (also used for schema discovery).
1186
+ const inner = projectLocalCommand(entry, command, binding);
1187
+ if (inner === undefined)
1188
+ return undefined;
1189
+ const match = DIRECT_CONTEXT_LIFECYCLE_COMMAND.exec(inner);
1100
1190
  return match?.[1]?.toLowerCase();
1101
1191
  }
1102
1192
  function stageAfter(prefix) {
@@ -1111,40 +1201,55 @@ function prefixFor(stage) {
1111
1201
  return "discovery";
1112
1202
  return stage === "build" ? "validate" : "none";
1113
1203
  }
1114
- function scanRecordedContextLifecycle(worklog, prior) {
1204
+ const CONTEXT_LIFECYCLE_MENTION = /\bwren\s+context\s+(validate|build)\b/i;
1205
+ function scanRecordedContextLifecycle(worklog, prior, binding) {
1115
1206
  let stage = stageAfter(prior);
1116
1207
  let completed = prior === "build";
1117
1208
  let outOfOrderObserved;
1209
+ // Successful commands that mention a lifecycle command but were not
1210
+ // recognised as one, so a failure can quote them instead of claiming the
1211
+ // command never ran.
1212
+ const unrecognisedMentions = [];
1118
1213
  for (const entry of worklog) {
1119
1214
  if (!isSetupExecutionEntry(entry) || execSucceeded(entry) !== true)
1120
1215
  continue;
1121
1216
  const command = execCommandOf(entry);
1122
1217
  if (command === undefined)
1123
1218
  continue;
1219
+ const noteUnrecognised = (candidate, text) => {
1220
+ if (contextLifecycleStage(candidate, text, binding) !== undefined)
1221
+ return;
1222
+ const mention = CONTEXT_LIFECYCLE_MENTION.exec(text);
1223
+ if (mention)
1224
+ unrecognisedMentions.push({ command: text, stage: mention[1].toLowerCase() });
1225
+ };
1124
1226
  if (stage === "discovery") {
1125
- if (classifyRecordedSchemaDiscovery([entry]).kind === "successful") {
1227
+ if (classifyRecordedSchemaDiscovery([entry], binding).kind === "successful") {
1126
1228
  stage = "validate";
1127
1229
  continue;
1128
1230
  }
1129
- if (contextLifecycleStage(command) !== undefined) {
1130
- outOfOrderObserved ??= `successful \`${command}\` ran before successful schema discovery`;
1231
+ if (contextLifecycleStage(entry, command, binding) !== undefined) {
1232
+ outOfOrderObserved ??= `successful ${quoteCommand(command)} ran before successful schema discovery`;
1131
1233
  }
1234
+ noteUnrecognised(entry, command);
1132
1235
  continue;
1133
1236
  }
1134
- const nativeStage = contextLifecycleStage(command);
1237
+ const nativeStage = contextLifecycleStage(entry, command, binding);
1238
+ if (nativeStage === undefined)
1239
+ noteUnrecognised(entry, command);
1135
1240
  if (stage === "validate") {
1136
1241
  if (nativeStage === "validate") {
1137
1242
  stage = "build";
1138
1243
  }
1139
1244
  else if (nativeStage === "build") {
1140
- outOfOrderObserved ??= `successful \`${command}\` ran before successful \`wren context validate\``;
1245
+ outOfOrderObserved ??= `successful ${quoteCommand(command)} ran before successful \`wren context validate\``;
1141
1246
  }
1142
1247
  continue;
1143
1248
  }
1144
1249
  if (nativeStage === "build")
1145
1250
  completed = true;
1146
1251
  }
1147
- return { stage, completed, outOfOrderObserved };
1252
+ return { stage, completed, outOfOrderObserved, unrecognisedMentions };
1148
1253
  }
1149
1254
  /**
1150
1255
  * Returns the greatest ordered prefix proved by the supplied successful tool
@@ -1152,26 +1257,122 @@ function scanRecordedContextLifecycle(worklog, prior) {
1152
1257
  * call in the same attempt is out of order, so a corrective attempt can ask
1153
1258
  * only for the unproven suffix.
1154
1259
  */
1155
- export function recordedContextLifecyclePrefix(worklog, prior = "none") {
1156
- const scan = scanRecordedContextLifecycle(worklog, prior);
1260
+ export function recordedContextLifecyclePrefix(worklog, prior = "none", binding) {
1261
+ const scan = scanRecordedContextLifecycle(worklog, prior, binding);
1157
1262
  return scan.completed ? "build" : prefixFor(scan.stage);
1158
1263
  }
1159
- export function classifyRecordedContextLifecycle(worklog, prior = "none") {
1160
- const { stage, completed, outOfOrderObserved } = scanRecordedContextLifecycle(worklog, prior);
1264
+ export function classifyRecordedContextLifecycle(worklog, prior = "none", binding) {
1265
+ const { stage, completed, outOfOrderObserved, unrecognisedMentions } = scanRecordedContextLifecycle(worklog, prior, binding);
1266
+ const wanted = stage === "validate" ? "validate" : "build";
1267
+ const unrecognised = (unrecognisedMentions.find((mention) => mention.stage === wanted) ?? unrecognisedMentions[0])?.command;
1161
1268
  if (completed)
1162
1269
  return { kind: "successful" };
1270
+ const withUnrecognised = unrecognised !== undefined ? { unrecognised } : {};
1163
1271
  if (outOfOrderObserved !== undefined)
1164
- return { kind: "out_of_order", observed: outOfOrderObserved };
1272
+ return { kind: "out_of_order", observed: outOfOrderObserved, ...withUnrecognised };
1165
1273
  if (stage === "discovery")
1166
1274
  return { kind: "missing_discovery" };
1167
- if (stage === "validate")
1168
- return { kind: "validate_missing", observed: "no successful `wren context validate` followed successful schema discovery" };
1169
- return { kind: "build_missing", observed: "no successful `wren context build` followed successful `wren context validate`" };
1275
+ if (stage === "validate") {
1276
+ return { kind: "validate_missing", observed: "no recognised successful `wren context validate` followed successful schema discovery", ...withUnrecognised };
1277
+ }
1278
+ return { kind: "build_missing", observed: "no recognised successful `wren context build` followed successful `wren context validate`", ...withUnrecognised };
1170
1279
  }
1171
1280
  /** A narrow terminal-message check: only override a context error that explicitly claims a zero-table/model outcome. */
1172
1281
  function claimsNoTablesOrModels(message) {
1173
1282
  return /\b(?:no|zero)\s+(?:tables?|models?)\b|\b0\s+(?:tables?|models?)\b/i.test(message);
1174
1283
  }
1284
+ const ENV_ASSIGNMENT_VALUE = /(^|[\s;&|(])([A-Za-z_][A-Za-z0-9_]*)=("[^"]*"|'[^']*'|[^\s;&|]*)/g;
1285
+ /** Masks the value of every `NAME=value` assignment (leading, after `env`, or after `&&`/`;`). */
1286
+ function redactEnvAssignments(command) {
1287
+ return command.replace(ENV_ASSIGNMENT_VALUE, "$1$2=***");
1288
+ }
1289
+ /** Quotes a command for an error message: env values redacted first, then bounded so a long script cannot flood it. */
1290
+ function quoteCommand(command) {
1291
+ const flat = redactEnvAssignments(command).replace(/\s+/g, " ").trim();
1292
+ return `\`${flat.length > 300 ? `${flat.slice(0, 300)}...` : flat}\``;
1293
+ }
1294
+ const UNRECOGNISED_LIFECYCLE_HINT = "It exited successfully but is not a recognised lifecycle invocation: only the bare command (optionally with environment assignments or `--path .`) or a single `cd <project directory> &&` prefix naming this step's project directory is accepted. Run it again as the bare command and set the `setup_execution` `cwd` field to the project directory instead of shell chaining.";
1295
+ function projectBindingOf(context) {
1296
+ return { root: context.root, projectDir: path.join(context.root, context.name) };
1297
+ }
1298
+ /**
1299
+ * The host-held evidence gate for a successful `context` step: the recorded
1300
+ * discovery -> validate -> build lifecycle, then the built `target/mdl.json`.
1301
+ * Returns the failure, or `undefined` when the evidence supports success. It
1302
+ * never looks at the agent's own status line.
1303
+ */
1304
+ function verifyContextEvidence(context) {
1305
+ if (context.worklog !== undefined) {
1306
+ const lifecycle = classifyRecordedContextLifecycle(context.worklog, context.priorContextLifecycle, projectBindingOf(context));
1307
+ if (lifecycle.kind === "missing_discovery") {
1308
+ return hostContractFailure("context_schema_discovery_missing", "The agent never completed recognized schema discovery in its recorded worklog, so its model/build result cannot be accepted. This is an agent-workflow failure, not evidence that the connection or data source lacks tables.", "the recorded worklog has no successful recognized schema-discovery command", [
1309
+ "run a recognized schema-discovery command against the connected source and make sure it exits successfully",
1310
+ "build target/mdl.json only from that discovered schema, with at least one model",
1311
+ ], "no_successful_schema_discovery");
1312
+ }
1313
+ const discovery = classifyRecordedSchemaDiscovery(context.worklog, projectBindingOf(context));
1314
+ if (discovery.kind === "failed") {
1315
+ const exitCodeSuffix = discovery.exitCode !== undefined ? ` with exit code ${discovery.exitCode}` : "";
1316
+ return hostContractFailure("context_schema_discovery_failed", `${quoteCommand(discovery.command)} failed${exitCodeSuffix} during schema discovery — a model/build result cannot be accepted until that command/tool failure is resolved; this is not evidence that the connection or data source lacks tables.`, discovery.exitCode !== undefined
1317
+ ? `the recorded schema-discovery command exited with code ${discovery.exitCode}`
1318
+ : "the recorded schema-discovery command failed", [
1319
+ "resolve the schema-discovery command failure and run a recognized discovery command successfully",
1320
+ "build target/mdl.json only from that discovered schema, with at least one model",
1321
+ ]);
1322
+ }
1323
+ if (lifecycle.kind === "validate_missing") {
1324
+ return hostContractFailure("context_validate_missing", lifecycle.unrecognised !== undefined
1325
+ ? `The agent ran ${quoteCommand(lifecycle.unrecognised)} successfully, but it was not recognised as \`wren context validate\` after schema discovery. ${UNRECOGNISED_LIFECYCLE_HINT}`
1326
+ : "The agent did not successfully run `wren context validate` after recognized schema discovery, so its model/build result cannot be accepted.", lifecycle.unrecognised !== undefined
1327
+ ? `${lifecycle.observed}; unrecognised successful command: ${quoteCommand(lifecycle.unrecognised)}`
1328
+ : lifecycle.observed, [
1329
+ "run a recognized schema-discovery command successfully",
1330
+ "run `wren context validate` successfully before `wren context build`",
1331
+ "build target/mdl.json only after validation, with at least one model",
1332
+ ...(lifecycle.unrecognised !== undefined
1333
+ ? ["pass the project directory through the `setup_execution` `cwd` field rather than `cd`/shell chaining"]
1334
+ : []),
1335
+ ]);
1336
+ }
1337
+ if (lifecycle.kind === "build_missing") {
1338
+ return hostContractFailure("context_build_missing", lifecycle.unrecognised !== undefined
1339
+ ? `The agent ran ${quoteCommand(lifecycle.unrecognised)} successfully, but it was not recognised as \`wren context build\` after \`wren context validate\`. ${UNRECOGNISED_LIFECYCLE_HINT}`
1340
+ : "The agent did not successfully run `wren context build` after `wren context validate`, so its claimed context success cannot be accepted.", lifecycle.unrecognised !== undefined
1341
+ ? `${lifecycle.observed}; unrecognised successful command: ${quoteCommand(lifecycle.unrecognised)}`
1342
+ : lifecycle.observed, [
1343
+ "run a recognized schema-discovery command successfully",
1344
+ "run `wren context validate` successfully",
1345
+ "run `wren context build` successfully after validation and produce target/mdl.json with at least one model",
1346
+ ...(lifecycle.unrecognised !== undefined
1347
+ ? ["pass the project directory through the `setup_execution` `cwd` field rather than `cd`/shell chaining"]
1348
+ : []),
1349
+ ]);
1350
+ }
1351
+ if (lifecycle.kind === "out_of_order") {
1352
+ return hostContractFailure("context_lifecycle_out_of_order", lifecycle.unrecognised !== undefined
1353
+ ? `The native context lifecycle ran out of order; Setup requires successful schema discovery followed by \`wren context validate\` and then \`wren context build\`. The agent also ran ${quoteCommand(lifecycle.unrecognised)} successfully, but it was not recognised. ${UNRECOGNISED_LIFECYCLE_HINT}`
1354
+ : "The native context lifecycle ran out of order; Setup requires successful schema discovery followed by `wren context validate` and then `wren context build`.", lifecycle.unrecognised !== undefined
1355
+ ? `${lifecycle.observed}; unrecognised successful command: ${quoteCommand(lifecycle.unrecognised)}`
1356
+ : lifecycle.observed, [
1357
+ "run a recognized schema-discovery command successfully",
1358
+ "run `wren context validate` successfully before `wren context build`",
1359
+ "produce target/mdl.json with at least one schema-derived model",
1360
+ ...(lifecycle.unrecognised !== undefined
1361
+ ? ["pass the project directory through the `setup_execution` `cwd` field rather than `cd`/shell chaining"]
1362
+ : []),
1363
+ ]);
1364
+ }
1365
+ }
1366
+ const mdlPath = path.join(context.root, context.name, "target", "mdl.json");
1367
+ if (!existsSync(mdlPath)) {
1368
+ return hostContractFailure("context_mdl_missing", `the setup agent reported "ok" but ${mdlPath} does not exist — treating this as a failed context build`, "target/mdl.json is missing after the claimed context build", ["target/mdl.json must exist", "target/mdl.json must contain at least one model"]);
1369
+ }
1370
+ const modelCount = countMdlModels(mdlPath);
1371
+ if (modelCount < 1) {
1372
+ return hostContractFailure("context_mdl_empty", `the setup agent reported "ok" but ${mdlPath} has ${modelCount} models — treating this as a failed context build`, `target/mdl.json has ${modelCount} models after the claimed context build`, ["target/mdl.json must contain at least one model"]);
1373
+ }
1374
+ return undefined;
1375
+ }
1175
1376
  /**
1176
1377
  * Parses the LAST `SETUP_STATUS: ok|needs_input|error` line out of a setup
1177
1378
  * turn's finalText (the terminal contract lives in the composed
@@ -1214,6 +1415,12 @@ export function parseSetupTerminal(finalText, context) {
1214
1415
  }
1215
1416
  }
1216
1417
  if (!matched) {
1418
+ // For the context step the status line is advisory: recorded execution
1419
+ // evidence plus the built project decide success. Anything short of that
1420
+ // keeps the missing-status failure (and its bounded correction turn).
1421
+ if (context.stepKey === "context" && context.worklog !== undefined && verifyContextEvidence(context) === undefined) {
1422
+ return { status: "ok", message: defaultMessageFor("ok") };
1423
+ }
1217
1424
  return {
1218
1425
  status: "error",
1219
1426
  message: "the setup agent's final message did not contain a SETUP_STATUS line",
@@ -1222,54 +1429,9 @@ export function parseSetupTerminal(finalText, context) {
1222
1429
  }
1223
1430
  if (matched.status === "ok") {
1224
1431
  if (context.stepKey === "context") {
1225
- if (context.worklog !== undefined) {
1226
- const lifecycle = classifyRecordedContextLifecycle(context.worklog, context.priorContextLifecycle);
1227
- if (lifecycle.kind === "missing_discovery") {
1228
- return hostContractFailure("context_schema_discovery_missing", "The agent never completed recognized schema discovery in its recorded worklog, so its model/build result cannot be accepted. This is an agent-workflow failure, not evidence that the connection or data source lacks tables.", "the recorded worklog has no successful recognized schema-discovery command", [
1229
- "run a recognized schema-discovery command against the connected source and make sure it exits successfully",
1230
- "build target/mdl.json only from that discovered schema, with at least one model",
1231
- ], "no_successful_schema_discovery");
1232
- }
1233
- const discovery = classifyRecordedSchemaDiscovery(context.worklog);
1234
- if (discovery.kind === "failed") {
1235
- const exitCodeSuffix = discovery.exitCode !== undefined ? ` with exit code ${discovery.exitCode}` : "";
1236
- return hostContractFailure("context_schema_discovery_failed", `\`${discovery.command}\` failed${exitCodeSuffix} during schema discovery — a model/build result cannot be accepted until that command/tool failure is resolved; this is not evidence that the connection or data source lacks tables.`, discovery.exitCode !== undefined
1237
- ? `the recorded schema-discovery command exited with code ${discovery.exitCode}`
1238
- : "the recorded schema-discovery command failed", [
1239
- "resolve the schema-discovery command failure and run a recognized discovery command successfully",
1240
- "build target/mdl.json only from that discovered schema, with at least one model",
1241
- ]);
1242
- }
1243
- if (lifecycle.kind === "validate_missing") {
1244
- return hostContractFailure("context_validate_missing", "The agent did not successfully run `wren context validate` after recognized schema discovery, so its model/build result cannot be accepted.", lifecycle.observed, [
1245
- "run a recognized schema-discovery command successfully",
1246
- "run `wren context validate` successfully before `wren context build`",
1247
- "build target/mdl.json only after validation, with at least one model",
1248
- ]);
1249
- }
1250
- if (lifecycle.kind === "build_missing") {
1251
- return hostContractFailure("context_build_missing", "The agent did not successfully run `wren context build` after `wren context validate`, so its claimed context success cannot be accepted.", lifecycle.observed, [
1252
- "run a recognized schema-discovery command successfully",
1253
- "run `wren context validate` successfully",
1254
- "run `wren context build` successfully after validation and produce target/mdl.json with at least one model",
1255
- ]);
1256
- }
1257
- if (lifecycle.kind === "out_of_order") {
1258
- return hostContractFailure("context_lifecycle_out_of_order", "The native context lifecycle ran out of order; Setup requires successful schema discovery followed by `wren context validate` and then `wren context build`.", lifecycle.observed, [
1259
- "run a recognized schema-discovery command successfully",
1260
- "run `wren context validate` successfully before `wren context build`",
1261
- "produce target/mdl.json with at least one schema-derived model",
1262
- ]);
1263
- }
1264
- }
1265
- const mdlPath = path.join(context.root, context.name, "target", "mdl.json");
1266
- if (!existsSync(mdlPath)) {
1267
- return hostContractFailure("context_mdl_missing", `the setup agent reported "ok" but ${mdlPath} does not exist — treating this as a failed context build`, "target/mdl.json is missing after the claimed context build", ["target/mdl.json must exist", "target/mdl.json must contain at least one model"]);
1268
- }
1269
- const modelCount = countMdlModels(mdlPath);
1270
- if (modelCount < 1) {
1271
- return hostContractFailure("context_mdl_empty", `the setup agent reported "ok" but ${mdlPath} has ${modelCount} models — treating this as a failed context build`, `target/mdl.json has ${modelCount} models after the claimed context build`, ["target/mdl.json must contain at least one model"]);
1272
- }
1432
+ const failure = verifyContextEvidence(context);
1433
+ if (failure !== undefined)
1434
+ return failure;
1273
1435
  }
1274
1436
  else {
1275
1437
  const markerName = context.stepKey === "connect_resume" ? ".wren-validated" : "wren_project.yml";
@@ -1316,14 +1478,14 @@ export function parseSetupTerminal(finalText, context) {
1316
1478
  // `setup_execution` call that exited non-zero, and reframe if one is found.
1317
1479
  const failedExec = firstFailedExec(context.worklog);
1318
1480
  if (failedExec !== undefined) {
1319
- const command = failedExec.command !== undefined ? `\`${failedExec.command}\`` : "a command";
1481
+ const command = failedExec.command !== undefined ? quoteCommand(failedExec.command) : "a command";
1320
1482
  const exitCodeSuffix = failedExec.exitCode !== undefined ? ` with exit code ${failedExec.exitCode}` : "";
1321
1483
  return {
1322
1484
  status: "error",
1323
1485
  message: `${command} failed${exitCodeSuffix} during this step — a command in this step's own history failed, so this step's error framing can't be trusted until that's ruled out; this is a command/tool failure, not by itself evidence that the connection or data source lacks tables. The agent's own report was: "${matched.message}"`,
1324
1486
  };
1325
1487
  }
1326
- if (claimsNoTablesOrModels(matched.message) && classifyRecordedSchemaDiscovery(context.worklog).kind === "none") {
1488
+ if (claimsNoTablesOrModels(matched.message) && classifyRecordedSchemaDiscovery(context.worklog, projectBindingOf(context)).kind === "none") {
1327
1489
  return {
1328
1490
  status: "error",
1329
1491
  failureKind: "no_successful_schema_discovery",
@@ -17,6 +17,13 @@ export interface SetupExecutionToolOptions {
17
17
  * never broaden either its cwd or its write scope beyond it.
18
18
  */
19
19
  readonly workspaceRoot: string;
20
+ /**
21
+ * The bound project directory of a project-bound setup step. When set and the
22
+ * directory exists, an `exec` without an explicit `cwd` runs there instead of
23
+ * at `workspaceRoot`; an explicit `cwd` still wins. Must lie within
24
+ * `workspaceRoot` (same containment check as an explicit `cwd`).
25
+ */
26
+ readonly defaultExecCwd?: string;
20
27
  }
21
28
  /**
22
29
  * A single flat top-level object, NOT a `z.discriminatedUnion` — a
@@ -109,7 +109,7 @@ export const setupExecutionInputSchema = z.object({
109
109
  cwd: z
110
110
  .string()
111
111
  .optional()
112
- .describe('Optional for both actions: use this directory instead of the workspace root as the command cwd or the base for a relative write path (e.g. the project directory). Absolute or relative to the workspace root, and must resolve within it. Defaults to the workspace root when omitted. For exec, use this instead of "cd <dir> && ..." chaining.'),
112
+ .describe('Optional for both actions: use this directory instead of the workspace root as the command cwd or the base for a relative write path (e.g. the project directory). Absolute or relative to the workspace root, and must resolve within it. Defaults to the workspace root when omitted (for exec, the bound project directory once the step has one). For exec, use this instead of "cd <dir> && ..." chaining.'),
113
113
  path: z
114
114
  .string()
115
115
  .optional()
@@ -291,8 +291,11 @@ function resolveWritePath(workspaceRoot, requestedPath, requestedCwd) {
291
291
  * exec cwd to any directory within `workspaceRoot` (e.g. the project
292
292
  * directory), but never broaden it beyond that root.
293
293
  */
294
- function resolveExecCwd(workspaceRoot, requestedCwd) {
294
+ function resolveExecCwd(workspaceRoot, requestedCwd, defaultCwd) {
295
295
  if (requestedCwd === undefined) {
296
+ if (defaultCwd !== undefined && existsSync(defaultCwd)) {
297
+ return assertWithinScope(workspaceRoot, defaultCwd, (requested, scope) => new SetupExecCwdScopeError(requested, scope));
298
+ }
296
299
  return workspaceRoot;
297
300
  }
298
301
  return assertWithinScope(workspaceRoot, requestedCwd, (requestedCwd, scope) => new SetupExecCwdScopeError(requestedCwd, scope));
@@ -319,7 +322,7 @@ export async function executeSetupExecution(input, options) {
319
322
  throw new SetupExecutionInputError("exec", "command");
320
323
  }
321
324
  assertCommandAllowed(input.command);
322
- const cwd = resolveExecCwd(workspaceRoot, input.cwd);
325
+ const cwd = resolveExecCwd(workspaceRoot, input.cwd, options.defaultExecCwd);
323
326
  const result = await env.exec({ mode: "write", command: "/bin/sh", args: ["-c", input.command], cwd, timeoutMs: SETUP_EXEC_TIMEOUT_MS }, policy);
324
327
  const { stdout, stderr } = redactSetupExecutionOutput(input.command, result);
325
328
  return {
@@ -343,7 +346,7 @@ export function createSetupExecutionTool(options) {
343
346
  return tool({
344
347
  description: "Run a shell command or write a file within the setup workspace, subject to a destructive/redirection " +
345
348
  'command denylist and a workspace-root scope. Two call shapes: {action: "exec", command, cwd?} runs a ' +
346
- "shell command (command is required; cwd is optional, defaults to the workspace root, and must resolve " +
349
+ "shell command (command is required; cwd is optional, defaults to the bound project directory for a project step once it exists (otherwise the workspace root), and must resolve " +
347
350
  'within it — path/content are unused); {action: "write", path, content, cwd?} writes a file (path and content ' +
348
351
  "are both required; a relative path uses cwd when supplied, otherwise the workspace root; command is unused). A call missing the field(s) its action requires, or " +
349
352
  "supplying a cwd/path that escapes the workspace root, is rejected before anything runs or is written.",
@@ -24,7 +24,7 @@ import { verifyProposal } from "./enrichment-verify.js";
24
24
  import { canonicalizeProposal, hashEnrichmentOperation, NATIVE_ENRICHMENT_SUBMIT_INPUT_SCHEMA, NATIVE_ENRICHMENT_SUBMIT_MCP_TOOL_NAME, requiresApproval, resolveEnrichmentBinding, sameEnrichmentBinding, EnrichmentContractError } from "./enrichment.js";
25
25
  import { INTERACTIVE_TARGETS, InteractiveLaunchError } from "./interactive-terminal.js";
26
26
  import { nativeSessionLaunchErrorCode, nativeSessionLaunchFailure, nativeSessionLifecycle, NATIVE_PURPOSES, NATIVE_SETUP_RECOVERY_MCP_TOOL_NAME, NativeSetupRecoveryError } from "./native-sessions.js";
27
- import { NATIVE_MCP_PERSIST_ANSWER_TOOL_NAME, NATIVE_MCP_TOOL_NAME, NATIVE_PERSIST_ANSWER_CONTRACT, NATIVE_SAVE_DASHBOARD_CONTRACT, NativeArtifactError } from "./native-artifacts.js";
27
+ import { NATIVE_MCP_PERSIST_ANSWER_TOOL_NAME, NATIVE_MCP_QUERY_TOOL_NAME, NATIVE_MCP_TOOL_NAME, NATIVE_PERSIST_ANSWER_CONTRACT, NATIVE_QUERY_INPUT_SCHEMA, NATIVE_SAVE_DASHBOARD_CONTRACT, NativeArtifactError } from "./native-artifacts.js";
28
28
  import { sameNativeRuntimeBinding } from "./native-dispatch-registry.js";
29
29
  import { ProfileRegistryError, toWarbleProfileDto } from "./profile-registry.js";
30
30
  import { DEFAULT_CONVERSATION_PROFILE } from "./native-sessions.js";
@@ -1196,14 +1196,19 @@ export function createApp(deps) {
1196
1196
  };
1197
1197
  const artifactTool = {
1198
1198
  name: NATIVE_MCP_TOOL_NAME,
1199
- description: "Save a dashboard to Artifacts. Use a prior persist_answer reference, or select this session's latest retained answer when the opaque reference is no longer available, without recomputing. The host marks the artifact verified only when the referenced answer is one it grounded itself; a dashboard built from a payload you write is stored as unverified. The existing payload form remains available for compatibility.",
1199
+ description: "Save a dashboard to Artifacts. Use a prior persist_answer reference, or select this session's latest retained answer when the opaque reference is no longer available, without recomputing. The host marks the artifact verified only when the referenced answer is one it grounded itself, or when a payload you write holds only table and definition blocks (plus an optional summary) and every definition cites a query this session ran through the query tool (by its query_id, else its SQL); its tables and definitions are then rebuilt from those query results and your summary is kept. A payload with any other block (narrative, chart, KPI) or an estimate flag is stored intact and unverified, as is any other payload. The existing payload form remains available for compatibility.",
1200
1200
  inputSchema: NATIVE_SAVE_DASHBOARD_CONTRACT.inputSchema,
1201
1201
  };
1202
1202
  const persistAnswerTool = {
1203
1203
  name: NATIVE_MCP_PERSIST_ANSWER_TOOL_NAME,
1204
- description: "Persist this answer's exact typed table and optional definition result before conversational presentation. idempotency_key is retry authority only; the host returns canonical provenance. On success, later save_dashboard calls should use its opaque answer_ref, with the session-scoped latest selector available if that ref leaves the conversation; on failure, present the answer but report that reference saving is unavailable.",
1204
+ description: "Persist this answer's exact typed table and optional definition result before conversational presentation. idempotency_key is retry authority only; the host returns canonical provenance. On success, later save_dashboard calls should use its opaque answer_ref, with the session-scoped latest selector available if that ref leaves the conversation; on failure, present the answer but report that reference saving is unavailable. The host records the answer as verified only when its definition cites a query this session ran through the query tool (by its query_id, else its SQL); its table and definition are then rebuilt from that query's result. Any other answer is stored as written and unverified.",
1205
1205
  inputSchema: NATIVE_PERSIST_ANSWER_CONTRACT.inputSchema,
1206
1206
  };
1207
+ const queryTool = {
1208
+ name: NATIVE_MCP_QUERY_TOOL_NAME,
1209
+ description: "Run one read-only SQL query through the host and return its rows plus a query_id. Cite that query_id in the definition block of the answer you persist or save; only answers whose every definition cites a query this session ran here can be recorded as verified.",
1210
+ inputSchema: NATIVE_QUERY_INPUT_SCHEMA,
1211
+ };
1207
1212
  const enrichmentSubmitTool = {
1208
1213
  name: NATIVE_ENRICHMENT_SUBMIT_MCP_TOOL_NAME,
1209
1214
  description: "Submit a drafted context enrichment proposal for host verification. The host re-checks the project binding, canonicalizes the proposal, and runs it through a shadow-copy verification ladder (grammar, sink, validate, build, dry-run) against a throwaway copy of the project -- the bound project itself is never written to. Only a verified proposal is recorded, into the same enrichment ledger a human reviews through accept/edit/skip. A refuted proposal is never recorded; the refuting step and reason are returned so the draft can be revised.",
@@ -1216,7 +1221,7 @@ export function createApp(deps) {
1216
1221
  const nativeToolsForPurpose = (purpose) => {
1217
1222
  switch (purpose) {
1218
1223
  case "setup": return [setupTool];
1219
- case "analysis": return [persistAnswerTool, artifactTool];
1224
+ case "analysis": return deps.nativeArtifacts?.queryAvailable() ? [queryTool, persistAnswerTool, artifactTool] : [persistAnswerTool, artifactTool];
1220
1225
  case "context_enrichment": return [enrichmentSubmitTool];
1221
1226
  default: {
1222
1227
  const exhaustive = purpose;
@@ -1244,6 +1249,10 @@ export function createApp(deps) {
1244
1249
  const saved = deps.nativeArtifacts.save(credential, request.params.arguments);
1245
1250
  return c.json({ jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(saved) }], structuredContent: saved } });
1246
1251
  }
1252
+ if (nativeSession.purpose === "analysis" && request.params.name === NATIVE_MCP_QUERY_TOOL_NAME && deps.nativeArtifacts.queryAvailable()) {
1253
+ const queried = await deps.nativeArtifacts.query(credential, request.params.arguments);
1254
+ return c.json({ jsonrpc: "2.0", id, result: { isError: queried.isError, content: [{ type: "text", text: queried.text }], ...(queried.result !== undefined ? { structuredContent: queried.result } : {}) } });
1255
+ }
1247
1256
  if (nativeSession.purpose === "analysis" && request.params.name === NATIVE_MCP_PERSIST_ANSWER_TOOL_NAME) {
1248
1257
  const persisted = deps.nativeArtifacts.persistAnswer(credential, request.params.arguments);
1249
1258
  return c.json({ jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(persisted) }], structuredContent: persisted } });
@@ -122,6 +122,8 @@ import { resolveWarbleBinary } from "../harness/compile/resolve-binary.js";
122
122
  import { toAuthChoiceFromRuntimeSettings } from "./auth-choice.js";
123
123
  import { Store } from "./db.js";
124
124
  import { resolveEnrichmentBinding, resolveProjectIdentity } from "./enrichment.js";
125
+ import { hashDirectory } from "../harness/compile/fingerprint.js";
126
+ import { openWrenComponentAccess } from "../harness/components/wren-access.js";
125
127
  import { createDispatchedEnrichmentDraftRunner } from "./enrichment-runner.js";
126
128
  import { dispatchInteractiveArtifacts, getInteractiveTerminalReadiness, InteractiveLaunchError, InteractiveTerminalManager, prepareInteractiveHandoff, unavailableInteractiveReadiness } from "./interactive-terminal.js";
127
129
  import { createProbedPtyFactory, ensureDarwinNodePtySpawnHelper } from "./node-pty-host.js";
@@ -556,6 +558,12 @@ async function main() {
556
558
  expectedMcpUrl: nativeMcpUrl,
557
559
  mcpUrl: process.env["WREN_HARNESS_NATIVE_MCP_URL"] ?? nativeMcpUrl,
558
560
  getBinding: currentInteractiveBinding,
561
+ // The session `query` tool runs through the same `wren` native sessions are given.
562
+ ...(nativeWrenExecutable ? {
563
+ openQueryAccess: async (binding, signal) => openWrenComponentAccess({
564
+ executable: nativeWrenExecutable.executable, project: binding.path, fingerprint: await hashDirectory(binding.path), signal,
565
+ }),
566
+ } : {}),
559
567
  });
560
568
  const nativeTerminalHostAvailable = async () => {
561
569
  try {
@@ -57,14 +57,15 @@ export declare function toAnswerOrRefusalEvent(id: string, result: RouteResult,
57
57
  * The one place a subscription answer's `verified` is decided; a `verified`
58
58
  * field in model-written text is never kept.
59
59
  *
60
- * - `codex-local`: the model's `run_sql` executes through this turn's host
61
- * query service, so the answer is grounded against that turn's observations
62
- * ({@link groundCodexLocalEnvelope}): verified only when every query it
63
- * cites is one the host ran this turn and that read a table, and then its
64
- * tables and definitions are the observed ones. Otherwise it stays as
65
- * written, badged "Unverified".
66
- * - `agent-sdk`: the host runs none of its queries and holds no evidence, so
67
- * it is always `false`, with the rest of the envelope kept as-is.
60
+ * - `codex-local` and `agent-sdk`: the model's host query tool (codex:local's
61
+ * `run_sql`, the agent-sdk host MCP's `run_sql`) executes through this turn's
62
+ * host query service, so the answer is grounded against that turn's
63
+ * observations ({@link groundOnHostEvidence}): verified only when every
64
+ * query it cites is one the host ran this turn and that read a table, and
65
+ * then its tables and definitions are the observed ones. Otherwise it stays
66
+ * as written, badged "Unverified" — including any answer whose data came
67
+ * another way (agent-sdk's Bash `wren` stays available and yields no host
68
+ * evidence).
68
69
  *
69
70
  * Other backends pass through.
70
71
  */