@whittlelabs/sifter 0.38.1 → 0.40.0

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 +264 -34
  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 } : {},
@@ -9963,6 +9973,10 @@ var require_progress = __commonJS({
9963
9973
  textBuf = "";
9964
9974
  /** The last two characters of reasoning emitted since the last tool; null when none. */
9965
9975
  reasoningTail = null;
9976
+ /** A streamed block has begun and none of its text has been emitted yet. */
9977
+ blockStart = false;
9978
+ /** The open streamed block's trailing whitespace, held until text follows it. */
9979
+ heldSpace = "";
9966
9980
  timer = null;
9967
9981
  closed = false;
9968
9982
  flushMs;
@@ -9996,15 +10010,42 @@ var require_progress = __commonJS({
9996
10010
  const block = text.trim();
9997
10011
  if (this.closed || !block)
9998
10012
  return;
9999
- const tail = this.reasoningTail;
10000
- const gap = tail === null || tail.endsWith("\n\n") ? "" : tail.endsWith("\n") ? "\n" : "\n\n";
10001
- this.reasoning(gap + block);
10013
+ this.reasoning(this.gap() + block);
10014
+ }
10015
+ /**
10016
+ * A block of reasoning begins that will arrive in pieces through
10017
+ * `reasoningDelta`, as the model writes it. Set off and trimmed exactly as
10018
+ * `reasoningBlock` sets off and trims a whole one, so a streamed transcript
10019
+ * reads the same as one reported block by block; only its timing differs.
10020
+ */
10021
+ beginBlock() {
10022
+ this.blockStart = true;
10023
+ this.heldSpace = "";
10024
+ }
10025
+ /** The next piece of the block `beginBlock` opened. */
10026
+ reasoningDelta(text) {
10027
+ if (this.closed || !text)
10028
+ return;
10029
+ let body = this.heldSpace + text;
10030
+ if (this.blockStart)
10031
+ body = body.trimStart();
10032
+ const kept = body.trimEnd();
10033
+ this.heldSpace = body.slice(kept.length);
10034
+ if (!kept)
10035
+ return;
10036
+ if (this.blockStart) {
10037
+ this.blockStart = false;
10038
+ this.reasoning(this.gap() + kept);
10039
+ } else {
10040
+ this.reasoning(kept);
10041
+ }
10002
10042
  }
10003
10043
  tool(name, target) {
10004
10044
  if (this.closed || !name)
10005
10045
  return;
10006
10046
  this.drainText();
10007
10047
  this.reasoningTail = null;
10048
+ this.heldSpace = "";
10008
10049
  this.push({ kind: "tool", tool: target ? { name, target } : { name } });
10009
10050
  this.flush();
10010
10051
  }
@@ -10029,6 +10070,11 @@ var require_progress = __commonJS({
10029
10070
  this.closed = true;
10030
10071
  this.clearTimer();
10031
10072
  }
10073
+ /** The break that sets a new block off from the reasoning before it. */
10074
+ gap() {
10075
+ const tail = this.reasoningTail;
10076
+ return tail === null || tail.endsWith("\n\n") ? "" : tail.endsWith("\n") ? "\n" : "\n\n";
10077
+ }
10032
10078
  drainText() {
10033
10079
  if (this.textBuf.length === 0)
10034
10080
  return;
@@ -10058,6 +10104,8 @@ var require_progress = __commonJS({
10058
10104
  var StreamJsonTap = class {
10059
10105
  handlers;
10060
10106
  buf = "";
10107
+ /** The messages whose reasoning arrived as deltas, by API message id. */
10108
+ streamed = /* @__PURE__ */ new Set();
10061
10109
  constructor(handlers) {
10062
10110
  this.handlers = handlers;
10063
10111
  }
@@ -10087,12 +10135,18 @@ var require_progress = __commonJS({
10087
10135
  } catch {
10088
10136
  return;
10089
10137
  }
10090
- if (event.type === "assistant") {
10091
- const content = event.message?.content;
10138
+ if (event.type === "stream_event") {
10139
+ this.consumeStreamEvent(event.event);
10140
+ } else if (event.type === "assistant") {
10141
+ const message = event.message;
10142
+ const content = message?.content;
10092
10143
  if (!Array.isArray(content))
10093
10144
  return;
10145
+ const streamed = typeof message?.id === "string" && this.streamed.has(message.id);
10094
10146
  for (const block of content) {
10095
10147
  const b = block;
10148
+ if (streamed && b?.type !== "tool_use")
10149
+ continue;
10096
10150
  if (b?.type === "text" && typeof b.text === "string" && b.text.length > 0) {
10097
10151
  this.handlers.onReasoning?.(b.text);
10098
10152
  } else if (b?.type === "thinking" && typeof b.thinking === "string" && b.thinking.length > 0) {
@@ -10105,6 +10159,24 @@ var require_progress = __commonJS({
10105
10159
  this.handlers.onResult?.();
10106
10160
  }
10107
10161
  }
10162
+ consumeStreamEvent(event) {
10163
+ if (!event)
10164
+ return;
10165
+ if (event.type === "message_start") {
10166
+ const id = event.message?.id;
10167
+ if (typeof id === "string")
10168
+ this.streamed.add(id);
10169
+ } else if (event.type === "content_block_start") {
10170
+ const kind = event.content_block?.type;
10171
+ if (kind === "thinking" || kind === "text")
10172
+ this.handlers.onReasoningStart?.();
10173
+ } else if (event.type === "content_block_delta") {
10174
+ const delta = event.delta;
10175
+ const text = delta?.type === "thinking_delta" ? delta.thinking : delta?.type === "text_delta" ? delta.text : void 0;
10176
+ if (typeof text === "string" && text.length > 0)
10177
+ this.handlers.onReasoningDelta?.(text);
10178
+ }
10179
+ }
10108
10180
  };
10109
10181
  exports2.StreamJsonTap = StreamJsonTap;
10110
10182
  function toolTarget(input) {
@@ -10764,19 +10836,25 @@ var require_claude_code = __commonJS({
10764
10836
  var DEFAULT_TIMEOUT = 6e5;
10765
10837
  var REVIEW_TOOLS = ["Read", "Grep", "Glob"];
10766
10838
  var THINKING_DISPLAY_FLOOR = "2.1.94";
10767
- function reviewRunArgs(settingSources) {
10839
+ function reviewRunArgs(settingSources, writesAnswerFiles = false) {
10768
10840
  return [
10769
10841
  "--setting-sources",
10770
10842
  settingSources,
10771
10843
  "--strict-mcp-config",
10772
10844
  "--tools",
10773
- REVIEW_TOOLS.join(","),
10845
+ (writesAnswerFiles ? [...REVIEW_TOOLS, "Write"] : REVIEW_TOOLS).join(","),
10774
10846
  "--permission-mode",
10775
10847
  "default",
10776
10848
  "--settings",
10777
10849
  JSON.stringify({ disableAllHooks: true })
10778
10850
  ];
10779
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
+ }
10780
10858
  var RETIRED_READ_TOOLS = ["LS"];
10781
10859
  var HOSTED_ENV_PASSTHROUGH = [
10782
10860
  "PATH",
@@ -10865,7 +10943,11 @@ var require_claude_code = __commonJS({
10865
10943
  materializeOptions: {
10866
10944
  ...tmpBase ? { tmpDir: tmpBase } : {},
10867
10945
  overlayAllowedHosts: this.config.overlayAllowedHosts ?? []
10868
- }
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"
10869
10951
  };
10870
10952
  }
10871
10953
  async execute(dispatch, signal, ctx) {
@@ -10917,7 +10999,8 @@ var require_claude_code = __commonJS({
10917
10999
  // machine's, and the runner pauses on it. With a revealed key it is one
10918
11000
  // customer's, and only their job fails.
10919
11001
  modelEnv === null,
10920
- effort
11002
+ effort,
11003
+ ctx?.outputDir
10921
11004
  );
10922
11005
  if (workspace) {
10923
11006
  response.outputs.push(...(0, workspace_telemetry_1.workspaceOutputs)(workspace, runCwd, readPaths, toolInputs));
@@ -11003,7 +11086,7 @@ var require_claude_code = __commonJS({
11003
11086
  async listModels() {
11004
11087
  return CLAUDE_CODE_MODELS;
11005
11088
  }
11006
- 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) {
11007
11090
  const outputLabel = spec.outputLabel;
11008
11091
  const model = modelOverride ?? this.config.model;
11009
11092
  const effort = effortOverride ?? this.config.reasoningEffort;
@@ -11012,9 +11095,18 @@ var require_claude_code = __commonJS({
11012
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");
11013
11096
  }
11014
11097
  const settingSources = local && this.config.claudeSettings === "user" ? "user" : "";
11015
- 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
+ ];
11016
11106
  if (resumeSessionId)
11017
11107
  args.push("--resume", resumeSessionId);
11108
+ if (reporter)
11109
+ args.push("--include-partial-messages");
11018
11110
  if (spec.outputSchema && typeof spec.outputSchema === "object" && Object.keys(spec.outputSchema).length > 0) {
11019
11111
  args.push("--json-schema", JSON.stringify(spec.outputSchema));
11020
11112
  }
@@ -11028,11 +11120,14 @@ var require_claude_code = __commonJS({
11028
11120
  }
11029
11121
  if (this.config.maxTurns !== void 0)
11030
11122
  args.push("--max-turns", String(this.config.maxTurns));
11031
- if (allowedTools && allowedTools.length > 0) {
11032
- args.push("--allowedTools", allowedTools.join(","));
11123
+ const allowRules = [...allowedTools ?? [], ...writeRule ? [writeRule] : []];
11124
+ if (allowRules.length > 0) {
11125
+ args.push("--allowedTools", allowRules.join(","));
11033
11126
  }
11034
11127
  const tap = reporter ? new progress_1.StreamJsonTap({
11035
11128
  onReasoning: (t) => reporter.reasoningBlock(t),
11129
+ onReasoningStart: () => reporter.beginBlock(),
11130
+ onReasoningDelta: (t) => reporter.reasoningDelta(t),
11036
11131
  onTool: (name, target) => reporter.tool(name, target),
11037
11132
  onResult: () => reporter.flush()
11038
11133
  }) : null;
@@ -11464,8 +11559,10 @@ var require_codex = __commonJS({
11464
11559
  var workspace_telemetry_1 = require_workspace_telemetry();
11465
11560
  var apply_1 = require_apply();
11466
11561
  var CODEX_PROFILE = "sift-review";
11467
- function reviewProfile(installDir, platform = process.platform) {
11562
+ function reviewProfile(installDir, platform = process.platform, outputDir) {
11468
11563
  const rules = ['":root"="deny"', '":minimal"="read"', '":workspace_roots"="read"', `${JSON.stringify(installDir)}="read"`];
11564
+ if (outputDir)
11565
+ rules.push(`${JSON.stringify(outputDir)}="write"`);
11469
11566
  if (platform === "linux") {
11470
11567
  rules.push(`${JSON.stringify(path_1.default.join(codexHome(), "tmp", "arg0"))}="read"`);
11471
11568
  } else {
@@ -11516,14 +11613,14 @@ var require_codex = __commonJS({
11516
11613
  }
11517
11614
  return codex;
11518
11615
  }
11519
- function codexReviewArgs(installDir, platform = process.platform) {
11616
+ function codexReviewArgs(installDir, platform = process.platform, outputDir) {
11520
11617
  return [
11521
11618
  "--json",
11522
11619
  "--skip-git-repo-check",
11523
11620
  "--ignore-user-config",
11524
11621
  "--ignore-rules",
11525
11622
  "-c",
11526
- reviewProfile(installDir, platform),
11623
+ reviewProfile(installDir, platform, outputDir),
11527
11624
  "-c",
11528
11625
  `default_permissions="${CODEX_PROFILE}"`,
11529
11626
  "-c",
@@ -11566,7 +11663,7 @@ var require_codex = __commonJS({
11566
11663
  };
11567
11664
  }
11568
11665
  async prepareEnvironment(dispatch) {
11569
- return (0, agent_common_1.agentEnvironment)(dispatch, this.config);
11666
+ return { ...(0, agent_common_1.agentEnvironment)(dispatch, this.config), writesAnswerFiles: true };
11570
11667
  }
11571
11668
  async execute(dispatch, signal, ctx) {
11572
11669
  (0, agent_common_1.refuseHosted)("codex", Boolean(ctx?.credentials));
@@ -11584,9 +11681,10 @@ var require_codex = __commonJS({
11584
11681
  runCwd = path_1.default.join(scratch, "run");
11585
11682
  await fs_1.promises.mkdir(runCwd);
11586
11683
  }
11684
+ const outputDir = ctx?.outputDir ? await fs_1.promises.realpath(ctx.outputDir) : void 0;
11587
11685
  const { response, commands } = await this.runCodex({
11588
11686
  codex,
11589
- args: ["exec", ...codexReviewArgs(codex.installDir), "-C", runCwd],
11687
+ args: ["exec", ...codexReviewArgs(codex.installDir, process.platform, outputDir), "-C", runCwd],
11590
11688
  prompt,
11591
11689
  spec,
11592
11690
  cwd: runCwd,
@@ -11950,14 +12048,22 @@ var require_copilot = __commonJS({
11950
12048
  var apply_1 = require_apply();
11951
12049
  var REVIEW_TOOLS = ["view", "grep", "glob"];
11952
12050
  var DENIED_TOOLS = ["shell", "write"];
11953
- function copilotReviewArgs() {
12051
+ function copilotReviewArgs(outputDir) {
12052
+ const permissions = outputDir ? [
12053
+ `--available-tools=${[...REVIEW_TOOLS, "create"].join(",")}`,
12054
+ ...REVIEW_TOOLS.map((t) => `--allow-tool=${t}`),
12055
+ `--allow-tool=write(${path_1.default.join(outputDir, "*")})`,
12056
+ ...DENIED_TOOLS.filter((t) => t !== "write").map((t) => `--deny-tool=${t}`)
12057
+ ] : [
12058
+ "--allow-all-tools",
12059
+ `--available-tools=${REVIEW_TOOLS.join(",")}`,
12060
+ ...DENIED_TOOLS.map((t) => `--deny-tool=${t}`)
12061
+ ];
11954
12062
  return [
11955
12063
  "--output-format",
11956
12064
  "json",
11957
12065
  "--no-color",
11958
- "--allow-all-tools",
11959
- `--available-tools=${REVIEW_TOOLS.join(",")}`,
11960
- ...DENIED_TOOLS.map((t) => `--deny-tool=${t}`),
12066
+ ...permissions,
11961
12067
  "--disable-builtin-mcps",
11962
12068
  "--disallow-temp-dir",
11963
12069
  "--no-ask-user",
@@ -11999,7 +12105,7 @@ var require_copilot = __commonJS({
11999
12105
  };
12000
12106
  }
12001
12107
  async prepareEnvironment(dispatch) {
12002
- return (0, agent_common_1.agentEnvironment)(dispatch, this.config);
12108
+ return { ...(0, agent_common_1.agentEnvironment)(dispatch, this.config), writesAnswerFiles: true };
12003
12109
  }
12004
12110
  async execute(dispatch, signal, ctx) {
12005
12111
  (0, agent_common_1.refuseHosted)("copilot", Boolean(ctx?.credentials));
@@ -12022,8 +12128,9 @@ var require_copilot = __commonJS({
12022
12128
  await fs_1.promises.mkdir(runCwd);
12023
12129
  }
12024
12130
  const sessionId = (0, crypto_1.randomUUID)();
12131
+ const outputDir = ctx?.outputDir ? await fs_1.promises.realpath(ctx.outputDir) : void 0;
12025
12132
  const { response, reads, toolInputs } = await this.runCopilot({
12026
- args: [...copilotReviewArgs(), `--session-id=${sessionId}`, "-C", runCwd],
12133
+ args: [...copilotReviewArgs(outputDir), `--session-id=${sessionId}`, "-C", runCwd],
12027
12134
  prompt: withSchema(prompt, spec),
12028
12135
  spec,
12029
12136
  cwd: runCwd,
@@ -27126,6 +27233,96 @@ var require_workspace = __commonJS({
27126
27233
  }
27127
27234
  });
27128
27235
 
27236
+ // ../../packages/shuttle/dist/core/answer-files.js
27237
+ var require_answer_files = __commonJS({
27238
+ "../../packages/shuttle/dist/core/answer-files.js"(exports2) {
27239
+ "use strict";
27240
+ var __importDefault = exports2 && exports2.__importDefault || function(mod) {
27241
+ return mod && mod.__esModule ? mod : { "default": mod };
27242
+ };
27243
+ Object.defineProperty(exports2, "__esModule", { value: true });
27244
+ exports2.OUTPUT_DIR = void 0;
27245
+ exports2.makeOutputDir = makeOutputDir;
27246
+ exports2.collectAnswerFiles = collectAnswerFiles;
27247
+ exports2.spliceAnswerFiles = spliceAnswerFiles;
27248
+ var node_fs_1 = require("node:fs");
27249
+ var node_path_1 = __importDefault(require("node:path"));
27250
+ var output_gates_1 = require_output_gates();
27251
+ exports2.OUTPUT_DIR = "out";
27252
+ var FINDING_GATE = "answer-files";
27253
+ async function makeOutputDir(runRoot) {
27254
+ const dir = node_path_1.default.join(runRoot, exports2.OUTPUT_DIR);
27255
+ await node_fs_1.promises.mkdir(dir);
27256
+ return dir;
27257
+ }
27258
+ async function collectAnswerFiles(dir, declaration) {
27259
+ const files = /* @__PURE__ */ new Map();
27260
+ let entries;
27261
+ try {
27262
+ entries = await node_fs_1.promises.readdir(dir, { withFileTypes: true });
27263
+ } catch {
27264
+ return files;
27265
+ }
27266
+ for (const entry of entries) {
27267
+ if (!entry.isFile())
27268
+ continue;
27269
+ if (!declaration.extensions.includes(node_path_1.default.extname(entry.name).toLowerCase()))
27270
+ continue;
27271
+ const full = node_path_1.default.join(dir, entry.name);
27272
+ const { size } = await node_fs_1.promises.stat(full);
27273
+ if (size > declaration.maxBytes) {
27274
+ files.set(entry.name, { problem: `is ${size} bytes, over the ${declaration.maxBytes}-byte limit` });
27275
+ continue;
27276
+ }
27277
+ files.set(entry.name, { text: await node_fs_1.promises.readFile(full, "utf8") });
27278
+ }
27279
+ return files;
27280
+ }
27281
+ function spliceAnswerFiles(response, outputLabel, declaration, files) {
27282
+ if (!response.parsed)
27283
+ return { response, findings: [] };
27284
+ const answer = structuredClone(response.parsed);
27285
+ const findings = [];
27286
+ const selected = (0, output_gates_1.resolvePath)(answer, declaration.at);
27287
+ for (const target of Array.isArray(selected) ? selected : [selected]) {
27288
+ if (typeof target !== "object" || target === null || Array.isArray(target))
27289
+ continue;
27290
+ const holder = target;
27291
+ const name = holder[declaration.reference];
27292
+ if (name === void 0)
27293
+ continue;
27294
+ const refused = (why) => findings.push({
27295
+ gate: FINDING_GATE,
27296
+ message: `${declaration.at}.${declaration.reference} ${JSON.stringify(name)} ${why}. Put the content inline as \`${declaration.into}\` and remove \`${declaration.reference}\`.`
27297
+ });
27298
+ if (typeof name !== "string" || name !== node_path_1.default.basename(name) || name.startsWith(".")) {
27299
+ refused(`is not a plain file name in ${exports2.OUTPUT_DIR}/`);
27300
+ continue;
27301
+ }
27302
+ const file = files.get(name);
27303
+ if (!file) {
27304
+ refused(`names no ${declaration.extensions.join(" or ")} file written to ${exports2.OUTPUT_DIR}/`);
27305
+ continue;
27306
+ }
27307
+ if ("problem" in file) {
27308
+ refused(file.problem);
27309
+ continue;
27310
+ }
27311
+ holder[declaration.into] = file.text;
27312
+ delete holder[declaration.reference];
27313
+ }
27314
+ return {
27315
+ response: {
27316
+ ...response,
27317
+ parsed: answer,
27318
+ outputs: response.outputs.map((o) => o.label === outputLabel ? { ...o, content: answer } : o)
27319
+ },
27320
+ findings
27321
+ };
27322
+ }
27323
+ }
27324
+ });
27325
+
27129
27326
  // ../../packages/shuttle/dist/core/run-dispatch.js
27130
27327
  var require_run_dispatch = __commonJS({
27131
27328
  "../../packages/shuttle/dist/core/run-dispatch.js"(exports2) {
@@ -27139,6 +27336,7 @@ var require_run_dispatch = __commonJS({
27139
27336
  var output_gates_1 = require_output_gates();
27140
27337
  var logger_1 = require_logger();
27141
27338
  var workspace_1 = require_workspace();
27339
+ var answer_files_1 = require_answer_files();
27142
27340
  exports2.GATE_REPAIR_BUDGET = 2;
27143
27341
  async function runDispatch(dispatch, executor, options) {
27144
27342
  const { credentials, signal } = options;
@@ -27159,46 +27357,78 @@ var require_run_dispatch = __commonJS({
27159
27357
  workspace = null;
27160
27358
  }
27161
27359
  }
27162
- const ctx = {
27360
+ const declaration = answerFilesDeclaration(dispatch);
27361
+ let outputDir;
27362
+ if (declaration && workspace && environment?.writesAnswerFiles) {
27363
+ try {
27364
+ outputDir = await (0, answer_files_1.makeOutputDir)(workspace.cwd);
27365
+ } catch (err) {
27366
+ (0, logger_1.getLogger)().warn("could not make the output directory; the answer will inline its files", {
27367
+ jobId: dispatch.job.id,
27368
+ error: err instanceof Error ? err.message : String(err)
27369
+ });
27370
+ }
27371
+ }
27372
+ const repairCtx = {
27163
27373
  ...options.reportProgress ? { reportProgress: options.reportProgress } : {},
27164
27374
  workspace,
27165
27375
  credentials
27166
27376
  };
27377
+ const ctx = { ...repairCtx, ...outputDir ? { outputDir } : {} };
27167
27378
  let response;
27379
+ let files = /* @__PURE__ */ new Map();
27168
27380
  try {
27169
27381
  response = await executor.execute(dispatch, signal, ctx);
27382
+ if (outputDir && declaration)
27383
+ files = await (0, answer_files_1.collectAnswerFiles)(outputDir, declaration);
27170
27384
  } catch (error) {
27171
27385
  return { kind: "executor-failed", error };
27172
27386
  } finally {
27173
27387
  if (workspace)
27174
27388
  await workspace.cleanup();
27175
27389
  }
27176
- const gated = await runOutputGates(dispatch, response, executor, signal, ctx);
27390
+ const gated = await runOutputGates(dispatch, response, executor, signal, repairCtx, files);
27177
27391
  if (!gated.ok)
27178
27392
  return { kind: "gate-failed", reason: gated.reason };
27179
27393
  return { kind: "completed", response: gated.response };
27180
27394
  }
27181
- async function runOutputGates(dispatch, response, executor, signal, ctx) {
27395
+ function answerFilesDeclaration(dispatch) {
27396
+ try {
27397
+ return (0, loom_1.readPromptExecutionSpec)(dispatch.inputs).answerFiles ?? null;
27398
+ } catch {
27399
+ return null;
27400
+ }
27401
+ }
27402
+ async function runOutputGates(dispatch, response, executor, signal, ctx, files = /* @__PURE__ */ new Map()) {
27182
27403
  let spec;
27183
27404
  try {
27184
27405
  spec = (0, loom_1.readPromptExecutionSpec)(dispatch.inputs);
27185
27406
  } catch {
27186
27407
  return { ok: true, response };
27187
27408
  }
27188
- const gates = spec.gates;
27189
- if (!gates || gates.length === 0)
27409
+ const gates = spec.gates ?? [];
27410
+ const declaration = spec.answerFiles;
27411
+ if (gates.length === 0 && !declaration)
27190
27412
  return { ok: true, response };
27191
- let current = response;
27192
- let result = (0, output_gates_1.evaluateOutputGates)(gates, current.parsed ?? {}, dispatch.inputs);
27413
+ const check = (candidate) => {
27414
+ const spliced = declaration ? (0, answer_files_1.spliceAnswerFiles)(candidate, spec.outputLabel, declaration, files) : { response: candidate, findings: [] };
27415
+ const gated = (0, output_gates_1.evaluateOutputGates)(gates, spliced.response.parsed ?? {}, dispatch.inputs);
27416
+ const findings = [...spliced.findings, ...gated.findings];
27417
+ return { response: spliced.response, result: { ok: findings.length === 0, findings } };
27418
+ };
27419
+ let checked = check(response);
27420
+ let current = checked.response;
27421
+ let result = checked.result;
27193
27422
  let attempts = 0;
27194
27423
  while (!result.ok && attempts < exports2.GATE_REPAIR_BUDGET && current.continuation !== void 0 && typeof executor.continue === "function") {
27195
27424
  try {
27196
27425
  const repaired = await executor.continue(current.continuation, (0, output_gates_1.renderRepairPrompt)(result.findings), signal, ctx);
27197
- current = mergeRepairedResponse(current, repaired);
27426
+ checked = check(mergeRepairedResponse(current, repaired));
27198
27427
  } catch {
27199
27428
  break;
27200
27429
  }
27201
- result = (0, output_gates_1.evaluateOutputGates)(gates, current.parsed ?? {}, dispatch.inputs);
27430
+ current = checked.response;
27431
+ result = checked.result;
27202
27432
  attempts += 1;
27203
27433
  }
27204
27434
  if (result.ok)
@@ -29403,7 +29633,7 @@ var import_path = require("path");
29403
29633
  var import_promises = require("fs/promises");
29404
29634
  var import_yaml = __toESM(require_dist3());
29405
29635
  var import_shuttle = __toESM(require_dist5());
29406
- var buildVersion = true ? "0.38.1" : pkg.version;
29636
+ var buildVersion = true ? "0.40.0" : pkg.version;
29407
29637
  var AGENT_EXECUTORS = ["claude-code", "codex", "copilot"];
29408
29638
  var sifterBrand = {
29409
29639
  product: {