@whittlelabs/sifter 0.39.0 → 0.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +1 -0
  2. package/bin.js +208 -32
  3. package/bin.js.map +4 -4
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -98,6 +98,7 @@ The Sifter subscribes to your job pool and waits for review jobs. Leave it runni
98
98
  A review checks out the pull request and runs Claude Code on it. The pull request is someone else's code, so the review is held to reading:
99
99
 
100
100
  - It is offered three tools, Read, Grep and Glob, and nothing else: no shell, no edits, no network tools, no subagents.
101
+ - When the review draws a guided tour, it may also write files into one empty folder of its own (`out/`, beside the checkout), and nowhere else. The Sifter puts each drawing written there back into the review and deletes the folder with the checkout. A review that loads your Claude Code user settings writes nothing, because your allow rules could widen where it writes.
101
102
  - It loads no Claude Code settings file, neither the repository's nor yours. Your allow rules, permission mode, hooks and plugins do not apply to reviews, and no MCP server starts.
102
103
 
103
104
  Your login still works: Claude Code keeps it in your keychain or credentials file, or reads `ANTHROPIC_API_KEY`. If instead your login or provider is configured in your Claude Code settings (an `apiKeyHelper`, or an `env` block naming Bedrock, Vertex or a custom base URL), reviews will fail as signed out. Tell the Sifter to load your user settings in its `sifter.yaml` (`~/.sifter/sifter.yaml`, or `~/.sifter/<profile>/sifter.yaml` for a named profile):
package/bin.js CHANGED
@@ -3214,6 +3214,13 @@ var require_prompt_execution = __commonJS({
3214
3214
  }
3215
3215
  }
3216
3216
  }
3217
+ if (spec.answerFiles !== void 0) {
3218
+ const f = spec.answerFiles;
3219
+ const isName = (v) => typeof v === "string" && v.length > 0;
3220
+ if (typeof f !== "object" || f === null || Array.isArray(f) || !isName(f.at) || !isName(f.reference) || !isName(f.into) || !Array.isArray(f.extensions) || f.extensions.length === 0 || !f.extensions.every((e) => typeof e === "string" && /^\.[a-z0-9]+$/.test(e)) || typeof f.maxBytes !== "number" || !Number.isInteger(f.maxBytes) || f.maxBytes <= 0) {
3221
+ throw new Error('prompt-execution: spec.answerFiles must be { at, reference, into, extensions: [".ext", \u2026], maxBytes }');
3222
+ }
3223
+ }
3217
3224
  return spec;
3218
3225
  }
3219
3226
  function renderPromptExecution(inputs) {
@@ -3454,6 +3461,8 @@ var require_prompt_execution2 = __commonJS({
3454
3461
  spec.progressChannel = options.progressChannel;
3455
3462
  if (options.outputGates && options.outputGates.length > 0)
3456
3463
  spec.gates = options.outputGates;
3464
+ if (options.answerFiles)
3465
+ spec.answerFiles = options.answerFiles;
3457
3466
  if (options.rendering === "producer" && !options.prompt) {
3458
3467
  throw new Error("rendering='producer' requires `prompt`");
3459
3468
  }
@@ -9648,6 +9657,7 @@ var require_canonicalise = __commonJS({
9648
9657
  promptTemplatePath: promptOptions.promptTemplate,
9649
9658
  outputSchema: toJsonSchema(promptOptions.outputSchema),
9650
9659
  ...promptOptions.outputGates ? { outputGates: promptOptions.outputGates } : {},
9660
+ ...promptOptions.answerFiles ? { answerFiles: promptOptions.answerFiles } : {},
9651
9661
  outputLabel,
9652
9662
  rendering: promptOptions.rendering,
9653
9663
  ...promptOptions.maxAttempts !== void 0 ? { maxAttempts: promptOptions.maxAttempts } : {},
@@ -10826,19 +10836,25 @@ var require_claude_code = __commonJS({
10826
10836
  var DEFAULT_TIMEOUT = 6e5;
10827
10837
  var REVIEW_TOOLS = ["Read", "Grep", "Glob"];
10828
10838
  var THINKING_DISPLAY_FLOOR = "2.1.94";
10829
- function reviewRunArgs(settingSources) {
10839
+ function reviewRunArgs(settingSources, writesAnswerFiles = false) {
10830
10840
  return [
10831
10841
  "--setting-sources",
10832
10842
  settingSources,
10833
10843
  "--strict-mcp-config",
10834
10844
  "--tools",
10835
- REVIEW_TOOLS.join(","),
10845
+ (writesAnswerFiles ? [...REVIEW_TOOLS, "Write"] : REVIEW_TOOLS).join(","),
10836
10846
  "--permission-mode",
10837
10847
  "default",
10838
10848
  "--settings",
10839
10849
  JSON.stringify({ disableAllHooks: true })
10840
10850
  ];
10841
10851
  }
10852
+ function outputDirRule(cwd, outputDir) {
10853
+ const relative = path_1.default.relative(cwd, outputDir);
10854
+ if (relative !== path_1.default.basename(outputDir) || relative.startsWith("."))
10855
+ return null;
10856
+ return `Edit(./${relative}/**)`;
10857
+ }
10842
10858
  var RETIRED_READ_TOOLS = ["LS"];
10843
10859
  var HOSTED_ENV_PASSTHROUGH = [
10844
10860
  "PATH",
@@ -10927,7 +10943,11 @@ var require_claude_code = __commonJS({
10927
10943
  materializeOptions: {
10928
10944
  ...tmpBase ? { tmpDir: tmpBase } : {},
10929
10945
  overlayAllowedHosts: this.config.overlayAllowedHosts ?? []
10930
- }
10946
+ },
10947
+ // Writes are confined by one allow rule (`outputDirRule`), which an
10948
+ // operator's own settings could widen: a run that loads them writes
10949
+ // nothing, and its answer inlines (ADR 0133).
10950
+ writesAnswerFiles: this.config.claudeSettings !== "user"
10931
10951
  };
10932
10952
  }
10933
10953
  async execute(dispatch, signal, ctx) {
@@ -10979,7 +10999,8 @@ var require_claude_code = __commonJS({
10979
10999
  // machine's, and the runner pauses on it. With a revealed key it is one
10980
11000
  // customer's, and only their job fails.
10981
11001
  modelEnv === null,
10982
- effort
11002
+ effort,
11003
+ ctx?.outputDir
10983
11004
  );
10984
11005
  if (workspace) {
10985
11006
  response.outputs.push(...(0, workspace_telemetry_1.workspaceOutputs)(workspace, runCwd, readPaths, toolInputs));
@@ -11065,7 +11086,7 @@ var require_claude_code = __commonJS({
11065
11086
  async listModels() {
11066
11087
  return CLAUDE_CODE_MODELS;
11067
11088
  }
11068
- async runClaude(prompt, spec, cwd, childEnv, signal, modelOverride, reporter, resumeSessionId, transcriptName, local = false, effortOverride) {
11089
+ async runClaude(prompt, spec, cwd, childEnv, signal, modelOverride, reporter, resumeSessionId, transcriptName, local = false, effortOverride, outputDir) {
11069
11090
  const outputLabel = spec.outputLabel;
11070
11091
  const model = modelOverride ?? this.config.model;
11071
11092
  const effort = effortOverride ?? this.config.reasoningEffort;
@@ -11074,7 +11095,14 @@ var require_claude_code = __commonJS({
11074
11095
  throw new Error("claude-code: `claudeSettings: user` is for an installed Sifter whose login lives in Claude Code settings; a hosted run authenticates with the key it is handed and loads no settings");
11075
11096
  }
11076
11097
  const settingSources = local && this.config.claudeSettings === "user" ? "user" : "";
11077
- const args = ["--print", "--output-format", "stream-json", "--verbose", ...reviewRunArgs(settingSources)];
11098
+ const writeRule = outputDir ? outputDirRule(cwd, outputDir) : null;
11099
+ const args = [
11100
+ "--print",
11101
+ "--output-format",
11102
+ "stream-json",
11103
+ "--verbose",
11104
+ ...reviewRunArgs(settingSources, writeRule !== null)
11105
+ ];
11078
11106
  if (resumeSessionId)
11079
11107
  args.push("--resume", resumeSessionId);
11080
11108
  if (reporter)
@@ -11092,8 +11120,9 @@ var require_claude_code = __commonJS({
11092
11120
  }
11093
11121
  if (this.config.maxTurns !== void 0)
11094
11122
  args.push("--max-turns", String(this.config.maxTurns));
11095
- if (allowedTools && allowedTools.length > 0) {
11096
- args.push("--allowedTools", allowedTools.join(","));
11123
+ const allowRules = [...allowedTools ?? [], ...writeRule ? [writeRule] : []];
11124
+ if (allowRules.length > 0) {
11125
+ args.push("--allowedTools", allowRules.join(","));
11097
11126
  }
11098
11127
  const tap = reporter ? new progress_1.StreamJsonTap({
11099
11128
  onReasoning: (t) => reporter.reasoningBlock(t),
@@ -11530,8 +11559,10 @@ var require_codex = __commonJS({
11530
11559
  var workspace_telemetry_1 = require_workspace_telemetry();
11531
11560
  var apply_1 = require_apply();
11532
11561
  var CODEX_PROFILE = "sift-review";
11533
- function reviewProfile(installDir, platform = process.platform) {
11562
+ function reviewProfile(installDir, platform = process.platform, outputDir) {
11534
11563
  const rules = ['":root"="deny"', '":minimal"="read"', '":workspace_roots"="read"', `${JSON.stringify(installDir)}="read"`];
11564
+ if (outputDir)
11565
+ rules.push(`${JSON.stringify(outputDir)}="write"`);
11535
11566
  if (platform === "linux") {
11536
11567
  rules.push(`${JSON.stringify(path_1.default.join(codexHome(), "tmp", "arg0"))}="read"`);
11537
11568
  } else {
@@ -11582,14 +11613,14 @@ var require_codex = __commonJS({
11582
11613
  }
11583
11614
  return codex;
11584
11615
  }
11585
- function codexReviewArgs(installDir, platform = process.platform) {
11616
+ function codexReviewArgs(installDir, platform = process.platform, outputDir) {
11586
11617
  return [
11587
11618
  "--json",
11588
11619
  "--skip-git-repo-check",
11589
11620
  "--ignore-user-config",
11590
11621
  "--ignore-rules",
11591
11622
  "-c",
11592
- reviewProfile(installDir, platform),
11623
+ reviewProfile(installDir, platform, outputDir),
11593
11624
  "-c",
11594
11625
  `default_permissions="${CODEX_PROFILE}"`,
11595
11626
  "-c",
@@ -11632,7 +11663,7 @@ var require_codex = __commonJS({
11632
11663
  };
11633
11664
  }
11634
11665
  async prepareEnvironment(dispatch) {
11635
- return (0, agent_common_1.agentEnvironment)(dispatch, this.config);
11666
+ return { ...(0, agent_common_1.agentEnvironment)(dispatch, this.config), writesAnswerFiles: true };
11636
11667
  }
11637
11668
  async execute(dispatch, signal, ctx) {
11638
11669
  (0, agent_common_1.refuseHosted)("codex", Boolean(ctx?.credentials));
@@ -11650,9 +11681,10 @@ var require_codex = __commonJS({
11650
11681
  runCwd = path_1.default.join(scratch, "run");
11651
11682
  await fs_1.promises.mkdir(runCwd);
11652
11683
  }
11684
+ const outputDir = ctx?.outputDir ? await fs_1.promises.realpath(ctx.outputDir) : void 0;
11653
11685
  const { response, commands } = await this.runCodex({
11654
11686
  codex,
11655
- args: ["exec", ...codexReviewArgs(codex.installDir), "-C", runCwd],
11687
+ args: ["exec", ...codexReviewArgs(codex.installDir, process.platform, outputDir), "-C", runCwd],
11656
11688
  prompt,
11657
11689
  spec,
11658
11690
  cwd: runCwd,
@@ -12013,24 +12045,43 @@ var require_copilot = __commonJS({
12013
12045
  var json_answer_1 = require_json_answer();
12014
12046
  var models_1 = require_models();
12015
12047
  var workspace_telemetry_1 = require_workspace_telemetry();
12048
+ var cli_version_1 = require_cli_version();
12049
+ var version_check_1 = require_version_check();
12016
12050
  var apply_1 = require_apply();
12017
12051
  var REVIEW_TOOLS = ["view", "grep", "glob"];
12018
12052
  var DENIED_TOOLS = ["shell", "write"];
12019
- function copilotReviewArgs() {
12053
+ function copilotReviewArgs(version, outputDir) {
12054
+ const permissions = outputDir ? [
12055
+ `--available-tools=${[...REVIEW_TOOLS, "create"].join(",")}`,
12056
+ ...REVIEW_TOOLS.map((t) => `--allow-tool=${t}`),
12057
+ `--allow-tool=write(${path_1.default.join(outputDir, "*")})`,
12058
+ ...DENIED_TOOLS.filter((t) => t !== "write").map((t) => `--deny-tool=${t}`)
12059
+ ] : [
12060
+ "--allow-all-tools",
12061
+ `--available-tools=${REVIEW_TOOLS.join(",")}`,
12062
+ ...DENIED_TOOLS.map((t) => `--deny-tool=${t}`)
12063
+ ];
12020
12064
  return [
12021
12065
  "--output-format",
12022
12066
  "json",
12023
12067
  "--no-color",
12024
- "--allow-all-tools",
12025
- `--available-tools=${REVIEW_TOOLS.join(",")}`,
12026
- ...DENIED_TOOLS.map((t) => `--deny-tool=${t}`),
12068
+ ...permissions,
12027
12069
  "--disable-builtin-mcps",
12028
12070
  "--disallow-temp-dir",
12029
12071
  "--no-ask-user",
12030
12072
  "--no-auto-update",
12031
- "--no-remote"
12073
+ ...sessionStaysLocal(version)
12032
12074
  ];
12033
12075
  }
12076
+ var REMOTE_FLAG_RETIRED = "1.0.103";
12077
+ function sessionStaysLocal(version) {
12078
+ if (version === null)
12079
+ return [];
12080
+ return (0, version_check_1.compareSemver)(version, REMOTE_FLAG_RETIRED) >= 0 ? ["--sync-session=off"] : ["--no-remote"];
12081
+ }
12082
+ function copilotVersion(home) {
12083
+ return (0, cli_version_1.cliVersion)("copilot", runEnv(home));
12084
+ }
12034
12085
  var RUN_SETTINGS = { disableAllHooks: true, autoUpdate: false };
12035
12086
  var COPILOT_FAULTS = {
12036
12087
  installRemedy: "install GitHub Copilot CLI (brew install --cask copilot-cli, or npm install -g @github/copilot) where this process can find it on PATH",
@@ -12065,7 +12116,7 @@ var require_copilot = __commonJS({
12065
12116
  };
12066
12117
  }
12067
12118
  async prepareEnvironment(dispatch) {
12068
- return (0, agent_common_1.agentEnvironment)(dispatch, this.config);
12119
+ return { ...(0, agent_common_1.agentEnvironment)(dispatch, this.config), writesAnswerFiles: true };
12069
12120
  }
12070
12121
  async execute(dispatch, signal, ctx) {
12071
12122
  (0, agent_common_1.refuseHosted)("copilot", Boolean(ctx?.credentials));
@@ -12088,8 +12139,9 @@ var require_copilot = __commonJS({
12088
12139
  await fs_1.promises.mkdir(runCwd);
12089
12140
  }
12090
12141
  const sessionId = (0, crypto_1.randomUUID)();
12142
+ const outputDir = ctx?.outputDir ? await fs_1.promises.realpath(ctx.outputDir) : void 0;
12091
12143
  const { response, reads, toolInputs } = await this.runCopilot({
12092
- args: [...copilotReviewArgs(), `--session-id=${sessionId}`, "-C", runCwd],
12144
+ args: [...copilotReviewArgs(await copilotVersion(home), outputDir), `--session-id=${sessionId}`, "-C", runCwd],
12093
12145
  prompt: withSchema(prompt, spec),
12094
12146
  spec,
12095
12147
  cwd: runCwd,
@@ -12136,7 +12188,7 @@ var require_copilot = __commonJS({
12136
12188
  outputSchema: handle.outputSchema
12137
12189
  };
12138
12190
  const { response } = await this.runCopilot({
12139
- args: [...copilotReviewArgs(), `--resume=${handle.sessionId}`, "-C", cwd],
12191
+ args: [...copilotReviewArgs(await copilotVersion(handle.home)), `--resume=${handle.sessionId}`, "-C", cwd],
12140
12192
  prompt: followUp,
12141
12193
  spec,
12142
12194
  cwd,
@@ -12156,11 +12208,12 @@ var require_copilot = __commonJS({
12156
12208
  const scratch = await fs_1.promises.mkdtemp(path_1.default.join(this.tmpBase(), "shuttle-copilot-ready-"));
12157
12209
  try {
12158
12210
  const home = await makeRunHome(scratch);
12211
+ const version = await copilotVersion(home);
12159
12212
  let run;
12160
12213
  try {
12161
12214
  run = await withOperatorLogin(home, () => (0, agent_process_1.runAgentProcess)({
12162
12215
  bin: "copilot",
12163
- args: [...copilotReviewArgs(), "-C", scratch, "-p", READINESS_PROMPT],
12216
+ args: [...copilotReviewArgs(version), "-C", scratch, "-p", READINESS_PROMPT],
12164
12217
  cwd: scratch,
12165
12218
  env: runEnv(home),
12166
12219
  idleTimeoutMs: READINESS_TIMEOUT_MS,
@@ -27192,6 +27245,96 @@ var require_workspace = __commonJS({
27192
27245
  }
27193
27246
  });
27194
27247
 
27248
+ // ../../packages/shuttle/dist/core/answer-files.js
27249
+ var require_answer_files = __commonJS({
27250
+ "../../packages/shuttle/dist/core/answer-files.js"(exports2) {
27251
+ "use strict";
27252
+ var __importDefault = exports2 && exports2.__importDefault || function(mod) {
27253
+ return mod && mod.__esModule ? mod : { "default": mod };
27254
+ };
27255
+ Object.defineProperty(exports2, "__esModule", { value: true });
27256
+ exports2.OUTPUT_DIR = void 0;
27257
+ exports2.makeOutputDir = makeOutputDir;
27258
+ exports2.collectAnswerFiles = collectAnswerFiles;
27259
+ exports2.spliceAnswerFiles = spliceAnswerFiles;
27260
+ var node_fs_1 = require("node:fs");
27261
+ var node_path_1 = __importDefault(require("node:path"));
27262
+ var output_gates_1 = require_output_gates();
27263
+ exports2.OUTPUT_DIR = "out";
27264
+ var FINDING_GATE = "answer-files";
27265
+ async function makeOutputDir(runRoot) {
27266
+ const dir = node_path_1.default.join(runRoot, exports2.OUTPUT_DIR);
27267
+ await node_fs_1.promises.mkdir(dir);
27268
+ return dir;
27269
+ }
27270
+ async function collectAnswerFiles(dir, declaration) {
27271
+ const files = /* @__PURE__ */ new Map();
27272
+ let entries;
27273
+ try {
27274
+ entries = await node_fs_1.promises.readdir(dir, { withFileTypes: true });
27275
+ } catch {
27276
+ return files;
27277
+ }
27278
+ for (const entry of entries) {
27279
+ if (!entry.isFile())
27280
+ continue;
27281
+ if (!declaration.extensions.includes(node_path_1.default.extname(entry.name).toLowerCase()))
27282
+ continue;
27283
+ const full = node_path_1.default.join(dir, entry.name);
27284
+ const { size } = await node_fs_1.promises.stat(full);
27285
+ if (size > declaration.maxBytes) {
27286
+ files.set(entry.name, { problem: `is ${size} bytes, over the ${declaration.maxBytes}-byte limit` });
27287
+ continue;
27288
+ }
27289
+ files.set(entry.name, { text: await node_fs_1.promises.readFile(full, "utf8") });
27290
+ }
27291
+ return files;
27292
+ }
27293
+ function spliceAnswerFiles(response, outputLabel, declaration, files) {
27294
+ if (!response.parsed)
27295
+ return { response, findings: [] };
27296
+ const answer = structuredClone(response.parsed);
27297
+ const findings = [];
27298
+ const selected = (0, output_gates_1.resolvePath)(answer, declaration.at);
27299
+ for (const target of Array.isArray(selected) ? selected : [selected]) {
27300
+ if (typeof target !== "object" || target === null || Array.isArray(target))
27301
+ continue;
27302
+ const holder = target;
27303
+ const name = holder[declaration.reference];
27304
+ if (name === void 0)
27305
+ continue;
27306
+ const refused = (why) => findings.push({
27307
+ gate: FINDING_GATE,
27308
+ message: `${declaration.at}.${declaration.reference} ${JSON.stringify(name)} ${why}. Put the content inline as \`${declaration.into}\` and remove \`${declaration.reference}\`.`
27309
+ });
27310
+ if (typeof name !== "string" || name !== node_path_1.default.basename(name) || name.startsWith(".")) {
27311
+ refused(`is not a plain file name in ${exports2.OUTPUT_DIR}/`);
27312
+ continue;
27313
+ }
27314
+ const file = files.get(name);
27315
+ if (!file) {
27316
+ refused(`names no ${declaration.extensions.join(" or ")} file written to ${exports2.OUTPUT_DIR}/`);
27317
+ continue;
27318
+ }
27319
+ if ("problem" in file) {
27320
+ refused(file.problem);
27321
+ continue;
27322
+ }
27323
+ holder[declaration.into] = file.text;
27324
+ delete holder[declaration.reference];
27325
+ }
27326
+ return {
27327
+ response: {
27328
+ ...response,
27329
+ parsed: answer,
27330
+ outputs: response.outputs.map((o) => o.label === outputLabel ? { ...o, content: answer } : o)
27331
+ },
27332
+ findings
27333
+ };
27334
+ }
27335
+ }
27336
+ });
27337
+
27195
27338
  // ../../packages/shuttle/dist/core/run-dispatch.js
27196
27339
  var require_run_dispatch = __commonJS({
27197
27340
  "../../packages/shuttle/dist/core/run-dispatch.js"(exports2) {
@@ -27205,6 +27348,7 @@ var require_run_dispatch = __commonJS({
27205
27348
  var output_gates_1 = require_output_gates();
27206
27349
  var logger_1 = require_logger();
27207
27350
  var workspace_1 = require_workspace();
27351
+ var answer_files_1 = require_answer_files();
27208
27352
  exports2.GATE_REPAIR_BUDGET = 2;
27209
27353
  async function runDispatch(dispatch, executor, options) {
27210
27354
  const { credentials, signal } = options;
@@ -27225,46 +27369,78 @@ var require_run_dispatch = __commonJS({
27225
27369
  workspace = null;
27226
27370
  }
27227
27371
  }
27228
- const ctx = {
27372
+ const declaration = answerFilesDeclaration(dispatch);
27373
+ let outputDir;
27374
+ if (declaration && workspace && environment?.writesAnswerFiles) {
27375
+ try {
27376
+ outputDir = await (0, answer_files_1.makeOutputDir)(workspace.cwd);
27377
+ } catch (err) {
27378
+ (0, logger_1.getLogger)().warn("could not make the output directory; the answer will inline its files", {
27379
+ jobId: dispatch.job.id,
27380
+ error: err instanceof Error ? err.message : String(err)
27381
+ });
27382
+ }
27383
+ }
27384
+ const repairCtx = {
27229
27385
  ...options.reportProgress ? { reportProgress: options.reportProgress } : {},
27230
27386
  workspace,
27231
27387
  credentials
27232
27388
  };
27389
+ const ctx = { ...repairCtx, ...outputDir ? { outputDir } : {} };
27233
27390
  let response;
27391
+ let files = /* @__PURE__ */ new Map();
27234
27392
  try {
27235
27393
  response = await executor.execute(dispatch, signal, ctx);
27394
+ if (outputDir && declaration)
27395
+ files = await (0, answer_files_1.collectAnswerFiles)(outputDir, declaration);
27236
27396
  } catch (error) {
27237
27397
  return { kind: "executor-failed", error };
27238
27398
  } finally {
27239
27399
  if (workspace)
27240
27400
  await workspace.cleanup();
27241
27401
  }
27242
- const gated = await runOutputGates(dispatch, response, executor, signal, ctx);
27402
+ const gated = await runOutputGates(dispatch, response, executor, signal, repairCtx, files);
27243
27403
  if (!gated.ok)
27244
27404
  return { kind: "gate-failed", reason: gated.reason };
27245
27405
  return { kind: "completed", response: gated.response };
27246
27406
  }
27247
- async function runOutputGates(dispatch, response, executor, signal, ctx) {
27407
+ function answerFilesDeclaration(dispatch) {
27408
+ try {
27409
+ return (0, loom_1.readPromptExecutionSpec)(dispatch.inputs).answerFiles ?? null;
27410
+ } catch {
27411
+ return null;
27412
+ }
27413
+ }
27414
+ async function runOutputGates(dispatch, response, executor, signal, ctx, files = /* @__PURE__ */ new Map()) {
27248
27415
  let spec;
27249
27416
  try {
27250
27417
  spec = (0, loom_1.readPromptExecutionSpec)(dispatch.inputs);
27251
27418
  } catch {
27252
27419
  return { ok: true, response };
27253
27420
  }
27254
- const gates = spec.gates;
27255
- if (!gates || gates.length === 0)
27421
+ const gates = spec.gates ?? [];
27422
+ const declaration = spec.answerFiles;
27423
+ if (gates.length === 0 && !declaration)
27256
27424
  return { ok: true, response };
27257
- let current = response;
27258
- let result = (0, output_gates_1.evaluateOutputGates)(gates, current.parsed ?? {}, dispatch.inputs);
27425
+ const check = (candidate) => {
27426
+ const spliced = declaration ? (0, answer_files_1.spliceAnswerFiles)(candidate, spec.outputLabel, declaration, files) : { response: candidate, findings: [] };
27427
+ const gated = (0, output_gates_1.evaluateOutputGates)(gates, spliced.response.parsed ?? {}, dispatch.inputs);
27428
+ const findings = [...spliced.findings, ...gated.findings];
27429
+ return { response: spliced.response, result: { ok: findings.length === 0, findings } };
27430
+ };
27431
+ let checked = check(response);
27432
+ let current = checked.response;
27433
+ let result = checked.result;
27259
27434
  let attempts = 0;
27260
27435
  while (!result.ok && attempts < exports2.GATE_REPAIR_BUDGET && current.continuation !== void 0 && typeof executor.continue === "function") {
27261
27436
  try {
27262
27437
  const repaired = await executor.continue(current.continuation, (0, output_gates_1.renderRepairPrompt)(result.findings), signal, ctx);
27263
- current = mergeRepairedResponse(current, repaired);
27438
+ checked = check(mergeRepairedResponse(current, repaired));
27264
27439
  } catch {
27265
27440
  break;
27266
27441
  }
27267
- result = (0, output_gates_1.evaluateOutputGates)(gates, current.parsed ?? {}, dispatch.inputs);
27442
+ current = checked.response;
27443
+ result = checked.result;
27268
27444
  attempts += 1;
27269
27445
  }
27270
27446
  if (result.ok)
@@ -29469,7 +29645,7 @@ var import_path = require("path");
29469
29645
  var import_promises = require("fs/promises");
29470
29646
  var import_yaml = __toESM(require_dist3());
29471
29647
  var import_shuttle = __toESM(require_dist5());
29472
- var buildVersion = true ? "0.39.0" : pkg.version;
29648
+ var buildVersion = true ? "0.40.1" : pkg.version;
29473
29649
  var AGENT_EXECUTORS = ["claude-code", "codex", "copilot"];
29474
29650
  var sifterBrand = {
29475
29651
  product: {