@whittlelabs/sifter 0.15.0 → 0.17.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 (3) hide show
  1. package/bin.js +473 -63
  2. package/bin.js.map +4 -4
  3. package/package.json +1 -1
package/bin.js CHANGED
@@ -15420,6 +15420,15 @@ var require_client = __commonJS({
15420
15420
  async createOutput(jobId, options) {
15421
15421
  return this.request("POST", `/api/jobs/${jobId}/output`, options);
15422
15422
  }
15423
+ /**
15424
+ * Post a batch of live progress frames for a running job. Best-effort
15425
+ * telemetry: the service publishes it over core NATS and never derives
15426
+ * authoritative state from it, so callers should swallow failures rather than
15427
+ * let a dropped frame affect the job.
15428
+ */
15429
+ async postProgress(jobId, options) {
15430
+ await this.request("POST", `/api/jobs/${jobId}/progress`, options);
15431
+ }
15423
15432
  // ── Attendance (subscriber dispatch lifecycle) ───────────────────
15424
15433
  /**
15425
15434
  * Accept a dispatched job. The subscribe loop calls this; direct
@@ -15722,8 +15731,12 @@ var require_self_update = __commonJS({
15722
15731
  });
15723
15732
  }
15724
15733
  function defaultRespawn() {
15725
- const child = (0, child_process_1.spawn)(process.execPath, process.argv.slice(1), { stdio: "inherit" });
15726
- child.on("exit", (code) => process.exit(code ?? 0));
15734
+ const child = (0, child_process_1.spawn)(process.execPath, process.argv.slice(1), {
15735
+ detached: true,
15736
+ stdio: "inherit"
15737
+ });
15738
+ child.unref();
15739
+ process.exit(0);
15727
15740
  }
15728
15741
  }
15729
15742
  });
@@ -15790,6 +15803,17 @@ var require_prompt_execution = __commonJS({
15790
15803
  }
15791
15804
  }
15792
15805
  }
15806
+ if (spec.gates !== void 0) {
15807
+ if (!Array.isArray(spec.gates)) {
15808
+ throw new Error("prompt-execution: spec.gates must be an array when present");
15809
+ }
15810
+ for (const gate of spec.gates) {
15811
+ const g = gate;
15812
+ if (typeof gate !== "object" || gate === null || typeof g.name !== "string" || typeof g.primitive !== "string" || typeof g.params !== "object" || g.params === null) {
15813
+ throw new Error("prompt-execution: each spec.gates entry must be { name, primitive, params }");
15814
+ }
15815
+ }
15816
+ }
15793
15817
  return spec;
15794
15818
  }
15795
15819
  function renderPromptExecution(inputs) {
@@ -16007,6 +16031,10 @@ var require_prompt_execution2 = __commonJS({
16007
16031
  spec.providerHints = options.providerHints;
16008
16032
  if (options.costCapHint)
16009
16033
  spec.costCapHint = options.costCapHint;
16034
+ if (options.progressChannel !== void 0)
16035
+ spec.progressChannel = options.progressChannel;
16036
+ if (options.outputGates && options.outputGates.length > 0)
16037
+ spec.gates = options.outputGates;
16010
16038
  if (options.rendering === "producer" && !options.prompt) {
16011
16039
  throw new Error("rendering='producer' requires `prompt`");
16012
16040
  }
@@ -18066,6 +18094,7 @@ var require_canonicalise = __commonJS({
18066
18094
  promptTemplate: templateText,
18067
18095
  promptTemplatePath: promptOptions.promptTemplate,
18068
18096
  outputSchema: toJsonSchema(promptOptions.outputSchema),
18097
+ ...promptOptions.outputGates ? { outputGates: promptOptions.outputGates } : {},
18069
18098
  outputLabel,
18070
18099
  rendering: promptOptions.rendering,
18071
18100
  ...promptOptions.maxAttempts !== void 0 ? { maxAttempts: promptOptions.maxAttempts } : {},
@@ -18348,6 +18377,128 @@ var require_chain = __commonJS({
18348
18377
  }
18349
18378
  });
18350
18379
 
18380
+ // ../../packages/shuttle/dist/executors/output-gates.js
18381
+ var require_output_gates = __commonJS({
18382
+ "../../packages/shuttle/dist/executors/output-gates.js"(exports2) {
18383
+ "use strict";
18384
+ Object.defineProperty(exports2, "__esModule", { value: true });
18385
+ exports2.resolvePath = resolvePath;
18386
+ exports2.evaluateOutputGates = evaluateOutputGates;
18387
+ exports2.renderRepairPrompt = renderRepairPrompt;
18388
+ var INPUT_REF = "input:";
18389
+ function resolvePath(root, path) {
18390
+ let ctx = root;
18391
+ for (const seg of path.split(".")) {
18392
+ const project = seg.endsWith("[*]");
18393
+ const key = project ? seg.slice(0, -3) : seg;
18394
+ if (Array.isArray(ctx)) {
18395
+ ctx = ctx.map((el) => key ? el?.[key] : el);
18396
+ } else {
18397
+ ctx = key ? ctx?.[key] : ctx;
18398
+ }
18399
+ if (project && !Array.isArray(ctx)) {
18400
+ ctx = ctx == null ? [] : [ctx];
18401
+ }
18402
+ }
18403
+ return ctx;
18404
+ }
18405
+ function resolveRef(ref, output, inputs) {
18406
+ if (ref.startsWith(INPUT_REF)) {
18407
+ const label = ref.slice(INPUT_REF.length);
18408
+ const row = inputs.find((i) => i.label === label) ?? inputs.find((i) => i.label === "artifacts" && i.payload.label === label);
18409
+ if (!row)
18410
+ return void 0;
18411
+ return Object.prototype.hasOwnProperty.call(row.payload, "value") ? row.payload.value : row.payload;
18412
+ }
18413
+ return resolvePath(output, ref);
18414
+ }
18415
+ function asStrings(value) {
18416
+ if (Array.isArray(value))
18417
+ return value.map((v) => String(v));
18418
+ if (value == null)
18419
+ return [];
18420
+ return [String(value)];
18421
+ }
18422
+ var PRIMITIVES = {
18423
+ /**
18424
+ * `coverage` — the values at `got` must exactly cover the set at `want`:
18425
+ * every wanted value present, nothing invented, none repeated. Params:
18426
+ * `{ got: <output path>, want: <ref>, subject?: <noun> }`.
18427
+ */
18428
+ coverage(params, output, inputs) {
18429
+ const subject = typeof params.subject === "string" ? params.subject : "item";
18430
+ const got = asStrings(resolveRef(String(params.got), output, inputs));
18431
+ const want = asStrings(resolveRef(String(params.want), output, inputs));
18432
+ const gotSet = new Set(got);
18433
+ const wantSet = new Set(want);
18434
+ const missing = want.filter((v) => !gotSet.has(v));
18435
+ const unknown = [...new Set(got.filter((v) => !wantSet.has(v)))];
18436
+ const seen = /* @__PURE__ */ new Set();
18437
+ const duplicated = [];
18438
+ for (const v of got) {
18439
+ if (seen.has(v))
18440
+ duplicated.push(v);
18441
+ else
18442
+ seen.add(v);
18443
+ }
18444
+ if (missing.length === 0 && unknown.length === 0 && duplicated.length === 0)
18445
+ return [];
18446
+ const parts = [];
18447
+ if (missing.length)
18448
+ parts.push(`missing ${subject}(s): [${missing.join(", ")}]`);
18449
+ if (unknown.length)
18450
+ parts.push(`not a known ${subject}: [${unknown.join(", ")}]`);
18451
+ if (duplicated.length)
18452
+ parts.push(`assessed more than once: [${[...new Set(duplicated)].join(", ")}]`);
18453
+ return [
18454
+ `every ${subject} must be covered exactly once \u2014 ${parts.join("; ")}. Return one entry per ${subject}, using its exact id.`
18455
+ ];
18456
+ },
18457
+ /**
18458
+ * `non_empty_when` — the array at `path` must be non-empty, optionally only
18459
+ * when `when.path` equals `when.equals`. Params:
18460
+ * `{ path: <output path>, when?: { path: <output path>, equals: <value> } }`.
18461
+ */
18462
+ non_empty_when(params, output) {
18463
+ const when = params.when;
18464
+ if (when && typeof when.path === "string") {
18465
+ if (resolvePath(output, when.path) !== when.equals)
18466
+ return [];
18467
+ }
18468
+ const path = String(params.path);
18469
+ const value = resolvePath(output, path);
18470
+ if (Array.isArray(value) && value.length > 0)
18471
+ return [];
18472
+ const cond = when && typeof when.path === "string" ? ` when ${when.path} is ${JSON.stringify(when.equals)}` : "";
18473
+ return [
18474
+ `'${path}' must be a non-empty array${cond}; it was ${Array.isArray(value) ? "empty" : "absent"}.`
18475
+ ];
18476
+ }
18477
+ };
18478
+ function evaluateOutputGates(gates, output, inputs) {
18479
+ const findings = [];
18480
+ for (const gate of gates) {
18481
+ const primitive = PRIMITIVES[gate.primitive];
18482
+ if (!primitive) {
18483
+ findings.push({ gate: gate.name, message: `unknown gate primitive '${gate.primitive}'` });
18484
+ continue;
18485
+ }
18486
+ for (const message of primitive(gate.params, output, inputs)) {
18487
+ findings.push({ gate: gate.name, message });
18488
+ }
18489
+ }
18490
+ return { ok: findings.length === 0, findings };
18491
+ }
18492
+ function renderRepairPrompt(findings) {
18493
+ return [
18494
+ "Your previous answer did not satisfy these output checks. Return the corrected, complete answer that resolves every item below. Produce the same output shape with the problems fixed, and nothing else \u2014 no commentary, no explanation of the changes.",
18495
+ "",
18496
+ ...findings.map((f) => `- [${f.gate}] ${f.message}`)
18497
+ ].join("\n");
18498
+ }
18499
+ }
18500
+ });
18501
+
18351
18502
  // ../../packages/keep/dist/client.js
18352
18503
  var require_client2 = __commonJS({
18353
18504
  "../../packages/keep/dist/client.js"(exports2) {
@@ -18880,6 +19031,155 @@ var require_validate_output = __commonJS({
18880
19031
  }
18881
19032
  });
18882
19033
 
19034
+ // ../../packages/shuttle/dist/executors/progress.js
19035
+ var require_progress = __commonJS({
19036
+ "../../packages/shuttle/dist/executors/progress.js"(exports2) {
19037
+ "use strict";
19038
+ Object.defineProperty(exports2, "__esModule", { value: true });
19039
+ exports2.StreamJsonTap = exports2.ProgressReporter = void 0;
19040
+ var ProgressReporter = class {
19041
+ sink;
19042
+ seq = 0;
19043
+ pending = [];
19044
+ textBuf = "";
19045
+ timer = null;
19046
+ closed = false;
19047
+ flushMs;
19048
+ maxChars;
19049
+ constructor(sink, options = {}) {
19050
+ this.sink = sink;
19051
+ this.flushMs = options.flushMs ?? 400;
19052
+ this.maxChars = options.maxChars ?? 1200;
19053
+ }
19054
+ reasoning(text) {
19055
+ if (this.closed || !text)
19056
+ return;
19057
+ this.textBuf += text;
19058
+ if (this.textBuf.length >= this.maxChars) {
19059
+ this.flush();
19060
+ } else {
19061
+ this.arm();
19062
+ }
19063
+ }
19064
+ tool(name, target) {
19065
+ if (this.closed || !name)
19066
+ return;
19067
+ this.drainText();
19068
+ this.push({ kind: "tool", tool: target ? { name, target } : { name } });
19069
+ this.flush();
19070
+ }
19071
+ /** Flush pending text + frames now. */
19072
+ flush() {
19073
+ if (this.closed)
19074
+ return;
19075
+ this.drainText();
19076
+ this.clearTimer();
19077
+ if (this.pending.length === 0)
19078
+ return;
19079
+ const batch = this.pending;
19080
+ this.pending = [];
19081
+ try {
19082
+ this.sink(batch);
19083
+ } catch {
19084
+ }
19085
+ }
19086
+ /** Final flush; no further frames are emitted after this. */
19087
+ close() {
19088
+ this.flush();
19089
+ this.closed = true;
19090
+ this.clearTimer();
19091
+ }
19092
+ drainText() {
19093
+ if (this.textBuf.length === 0)
19094
+ return;
19095
+ this.push({ kind: "reasoning", text: this.textBuf });
19096
+ this.textBuf = "";
19097
+ }
19098
+ push(frame) {
19099
+ this.pending.push({ seq: this.seq++, at: (/* @__PURE__ */ new Date()).toISOString(), ...frame });
19100
+ }
19101
+ arm() {
19102
+ if (this.timer)
19103
+ return;
19104
+ this.timer = setTimeout(() => {
19105
+ this.timer = null;
19106
+ this.flush();
19107
+ }, this.flushMs);
19108
+ this.timer.unref?.();
19109
+ }
19110
+ clearTimer() {
19111
+ if (this.timer) {
19112
+ clearTimeout(this.timer);
19113
+ this.timer = null;
19114
+ }
19115
+ }
19116
+ };
19117
+ exports2.ProgressReporter = ProgressReporter;
19118
+ var StreamJsonTap = class {
19119
+ handlers;
19120
+ buf = "";
19121
+ constructor(handlers) {
19122
+ this.handlers = handlers;
19123
+ }
19124
+ /** Feed a raw stdout chunk. Emits handlers for every complete line parsed. */
19125
+ push(chunk) {
19126
+ this.buf += chunk;
19127
+ let nl;
19128
+ while ((nl = this.buf.indexOf("\n")) !== -1) {
19129
+ const line = this.buf.slice(0, nl);
19130
+ this.buf = this.buf.slice(nl + 1);
19131
+ this.consumeLine(line);
19132
+ }
19133
+ }
19134
+ /** Flush a trailing partial line (best-effort; usually empty at process close). */
19135
+ end() {
19136
+ if (this.buf.trim().length > 0)
19137
+ this.consumeLine(this.buf);
19138
+ this.buf = "";
19139
+ }
19140
+ consumeLine(line) {
19141
+ const trimmed = line.trim();
19142
+ if (!trimmed.startsWith("{"))
19143
+ return;
19144
+ let event;
19145
+ try {
19146
+ event = JSON.parse(trimmed);
19147
+ } catch {
19148
+ return;
19149
+ }
19150
+ if (event.type === "assistant") {
19151
+ const content = event.message?.content;
19152
+ if (!Array.isArray(content))
19153
+ return;
19154
+ for (const block of content) {
19155
+ const b = block;
19156
+ if (b?.type === "text" && typeof b.text === "string" && b.text.length > 0) {
19157
+ this.handlers.onReasoning?.(b.text);
19158
+ } else if (b?.type === "thinking" && typeof b.thinking === "string" && b.thinking.length > 0) {
19159
+ this.handlers.onReasoning?.(b.thinking);
19160
+ } else if (b?.type === "tool_use" && typeof b.name === "string") {
19161
+ this.handlers.onTool?.(b.name, toolTarget(b.input));
19162
+ }
19163
+ }
19164
+ } else if (event.type === "result") {
19165
+ this.handlers.onResult?.();
19166
+ }
19167
+ }
19168
+ };
19169
+ exports2.StreamJsonTap = StreamJsonTap;
19170
+ function toolTarget(input) {
19171
+ if (!input)
19172
+ return void 0;
19173
+ for (const key of ["file_path", "path", "pattern", "command", "url", "query", "notebook_path"]) {
19174
+ const v = input[key];
19175
+ if (typeof v === "string" && v.length > 0)
19176
+ return v;
19177
+ }
19178
+ return void 0;
19179
+ }
19180
+ }
19181
+ });
19182
+
18883
19183
  // ../../packages/shuttle/dist/executors/workspace.js
18884
19184
  var require_workspace = __commonJS({
18885
19185
  "../../packages/shuttle/dist/executors/workspace.js"(exports2) {
@@ -19111,7 +19411,7 @@ var require_overlay = __commonJS({
19111
19411
  const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
19112
19412
  const allowed = new Set(options.allowedHosts.map(normalizeOrigin).filter((o) => !!o));
19113
19413
  const rootReal = await fs_1.promises.realpath(runDir);
19114
- const result = { written: 0, skipped: [] };
19414
+ const result = { written: 0, writtenPaths: [], skipped: [] };
19115
19415
  const files = Array.isArray(overlay.files) ? overlay.files : [];
19116
19416
  for (const file of files) {
19117
19417
  const skip = (reason) => result.skipped.push({ path: String(file?.path ?? "?"), reason });
@@ -19145,6 +19445,7 @@ var require_overlay = __commonJS({
19145
19445
  await fs_1.promises.mkdir(path_1.default.dirname(dest), { recursive: true });
19146
19446
  await fs_1.promises.writeFile(dest, content);
19147
19447
  result.written += 1;
19448
+ result.writtenPaths.push(file.path);
19148
19449
  } catch (err) {
19149
19450
  skip(err instanceof Error ? err.message : String(err));
19150
19451
  }
@@ -19258,9 +19559,11 @@ var require_claude_code = __commonJS({
19258
19559
  var fs_1 = require("fs");
19259
19560
  var os_1 = __importDefault(require("os"));
19260
19561
  var path_1 = __importDefault(require("path"));
19562
+ var string_decoder_1 = require("string_decoder");
19261
19563
  var loom_1 = require_dist3();
19262
19564
  var keep_1 = require_dist4();
19263
19565
  var validate_output_1 = require_validate_output();
19566
+ var progress_1 = require_progress();
19264
19567
  var workspace_1 = require_workspace();
19265
19568
  var overlay_1 = require_overlay();
19266
19569
  var env_ref_1 = require_env_ref();
@@ -19281,7 +19584,7 @@ var require_claude_code = __commonJS({
19281
19584
  outputTokens: spec.costCapHint?.estimatedOutputTokens
19282
19585
  };
19283
19586
  }
19284
- async execute(dispatch, signal) {
19587
+ async execute(dispatch, signal, ctx) {
19285
19588
  const { prompt, spec } = (0, loom_1.renderPromptExecution)(dispatch.inputs);
19286
19589
  let credentials = null;
19287
19590
  if (this.config.resolveCredentials) {
@@ -19306,6 +19609,7 @@ var require_claude_code = __commonJS({
19306
19609
  let workspace = null;
19307
19610
  let promptOnlyScratch = null;
19308
19611
  let overlayResult = null;
19612
+ const reporter = spec.progressChannel && ctx?.reportProgress ? new progress_1.ProgressReporter((frames) => ctx.reportProgress(spec.progressChannel, frames)) : null;
19309
19613
  try {
19310
19614
  if (spec.workspace) {
19311
19615
  workspace = await (0, workspace_1.prepareWorkspace)(spec.workspace, {
@@ -19327,11 +19631,16 @@ var require_claude_code = __commonJS({
19327
19631
  promptOnlyScratch = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-cc-run-"));
19328
19632
  }
19329
19633
  const runCwd = workspace?.cwd ?? promptOnlyScratch;
19330
- const { response, readPaths } = await this.runClaude(prompt, spec, runCwd, childEnv, signal, hintedModel);
19634
+ const { response, readPaths, toolText } = await this.runClaude(prompt, spec, runCwd, childEnv, signal, hintedModel, reporter);
19331
19635
  if (workspace) {
19636
+ const reads = [...readPaths];
19637
+ for (const rel of overlayResult?.writtenPaths ?? []) {
19638
+ if (toolText.includes(rel))
19639
+ reads.push(path_1.default.join(runCwd, rel));
19640
+ }
19332
19641
  response.outputs.push({
19333
19642
  label: "workspace_reads",
19334
- content: { paths: relativeWorkspaceReads(readPaths, runCwd) }
19643
+ content: { paths: relativeWorkspaceReads(reads, runCwd) }
19335
19644
  });
19336
19645
  response.outputs.push({
19337
19646
  label: "workspace",
@@ -19349,6 +19658,7 @@ var require_claude_code = __commonJS({
19349
19658
  }
19350
19659
  return response;
19351
19660
  } finally {
19661
+ reporter?.close();
19352
19662
  if (workspace)
19353
19663
  await workspace.cleanup();
19354
19664
  if (promptOnlyScratch)
@@ -19359,11 +19669,40 @@ var require_claude_code = __commonJS({
19359
19669
  });
19360
19670
  }
19361
19671
  }
19362
- runClaude(prompt, spec, cwd, childEnv, signal, modelOverride) {
19672
+ /**
19673
+ * In-session gate repair (ADR 0017): resume the session `execute()` returned a
19674
+ * handle for and correct its output. The repair turn works from the resumed
19675
+ * session's own context (no checkout needed), under the same `--json-schema`
19676
+ * constraint. A throwaway cwd hosts the run; the same handle rides back out so
19677
+ * a second repair round can resume again.
19678
+ */
19679
+ async continue(continuation, followUp, signal, _ctx) {
19680
+ const handle = continuation;
19681
+ if (!handle || typeof handle.sessionId !== "string") {
19682
+ throw new Error("claude-code continue: missing session to resume");
19683
+ }
19684
+ const tmpBase = this.config.tmpDir ? (0, apply_1.expandHome)(this.config.tmpDir) : os_1.default.tmpdir();
19685
+ const scratch = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-cc-resume-"));
19686
+ try {
19687
+ const spec = {
19688
+ rendering: "producer",
19689
+ outputLabel: handle.outputLabel,
19690
+ outputSchema: handle.outputSchema
19691
+ };
19692
+ const { response } = await this.runClaude(followUp, spec, scratch, handle.childEnv, signal, handle.model, null, handle.sessionId);
19693
+ return response;
19694
+ } finally {
19695
+ await fs_1.promises.rm(scratch, { recursive: true, force: true }).catch(() => {
19696
+ });
19697
+ }
19698
+ }
19699
+ runClaude(prompt, spec, cwd, childEnv, signal, modelOverride, reporter, resumeSessionId) {
19363
19700
  const outputLabel = spec.outputLabel;
19364
19701
  const model = modelOverride ?? this.config.model;
19365
19702
  return new Promise((resolve, reject) => {
19366
19703
  const args = ["--print", "--output-format", "stream-json", "--verbose"];
19704
+ if (resumeSessionId)
19705
+ args.push("--resume", resumeSessionId);
19367
19706
  if (spec.outputSchema && typeof spec.outputSchema === "object" && Object.keys(spec.outputSchema).length > 0) {
19368
19707
  args.push("--json-schema", JSON.stringify(spec.outputSchema));
19369
19708
  }
@@ -19395,7 +19734,17 @@ var require_claude_code = __commonJS({
19395
19734
  settled = true;
19396
19735
  fn();
19397
19736
  };
19398
- child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
19737
+ const tap = reporter ? new progress_1.StreamJsonTap({
19738
+ onReasoning: (t) => reporter.reasoning(t),
19739
+ onTool: (name, target) => reporter.tool(name, target),
19740
+ onResult: () => reporter.flush()
19741
+ }) : null;
19742
+ const decoder = tap ? new string_decoder_1.StringDecoder("utf8") : null;
19743
+ child.stdout.on("data", (chunk) => {
19744
+ stdoutChunks.push(chunk);
19745
+ if (tap && decoder)
19746
+ tap.push(decoder.write(chunk));
19747
+ });
19399
19748
  child.stdin.write(prompt);
19400
19749
  child.stdin.end();
19401
19750
  const timeout = this.config.timeout ?? DEFAULT_TIMEOUT;
@@ -19422,6 +19771,10 @@ var require_claude_code = __commonJS({
19422
19771
  child.on("close", (code) => {
19423
19772
  clearTimeout(timer);
19424
19773
  signal.removeEventListener("abort", onAbort);
19774
+ if (tap && decoder) {
19775
+ tap.push(decoder.end());
19776
+ tap.end();
19777
+ }
19425
19778
  const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
19426
19779
  if (code === 0) {
19427
19780
  try {
@@ -19444,9 +19797,21 @@ var require_claude_code = __commonJS({
19444
19797
  aiProvider: "claude-code",
19445
19798
  ...model ? { aiModel: model } : {},
19446
19799
  ...stream.usage ?? {}
19447
- }
19800
+ },
19801
+ // In-session repair handle (ADR 0017): present only when the run
19802
+ // reported a session id, so the runner can `--resume` it.
19803
+ ...stream.sessionId ? {
19804
+ continuation: {
19805
+ sessionId: stream.sessionId,
19806
+ model,
19807
+ childEnv,
19808
+ outputLabel,
19809
+ outputSchema: spec.outputSchema
19810
+ }
19811
+ } : {}
19448
19812
  },
19449
- readPaths: stream.readPaths
19813
+ readPaths: stream.readPaths,
19814
+ toolText: stream.toolText
19450
19815
  }));
19451
19816
  } catch (err) {
19452
19817
  settle(() => reject(err instanceof Error ? err : new Error(String(err))));
@@ -19543,6 +19908,8 @@ var require_claude_code = __commonJS({
19543
19908
  let usage = null;
19544
19909
  let resultMeta = null;
19545
19910
  let structuredOutput = null;
19911
+ let sessionId = null;
19912
+ let toolText = "";
19546
19913
  for (const line of stdout.split("\n")) {
19547
19914
  const trimmed = line.trim();
19548
19915
  if (!trimmed.startsWith("{"))
@@ -19553,15 +19920,22 @@ var require_claude_code = __commonJS({
19553
19920
  } catch {
19554
19921
  continue;
19555
19922
  }
19923
+ if (typeof event.session_id === "string")
19924
+ sessionId = event.session_id;
19556
19925
  if (event.type === "assistant") {
19557
19926
  const content = event.message?.content;
19558
19927
  if (!Array.isArray(content))
19559
19928
  continue;
19560
19929
  for (const block of content) {
19561
19930
  const b = block;
19562
- if (b?.type === "tool_use" && b.name === "Read" && typeof b.input?.file_path === "string") {
19563
- readPaths.push(b.input.file_path);
19931
+ if (b?.type !== "tool_use")
19932
+ continue;
19933
+ const fp = b.input?.file_path;
19934
+ if (b.name === "Read" && typeof fp === "string") {
19935
+ readPaths.push(fp);
19564
19936
  }
19937
+ if (b.input && typeof b.input === "object")
19938
+ toolText += JSON.stringify(b.input);
19565
19939
  }
19566
19940
  } else if (event.type === "result") {
19567
19941
  if (typeof event.result === "string" && event.result.trim().length > 0) {
@@ -19584,7 +19958,7 @@ var require_claude_code = __commonJS({
19584
19958
  };
19585
19959
  }
19586
19960
  }
19587
- return { finalText, readPaths, usage, resultMeta, structuredOutput };
19961
+ return { finalText, readPaths, usage, resultMeta, structuredOutput, sessionId, toolText };
19588
19962
  }
19589
19963
  var MAX_REPORTED_READS = 2e3;
19590
19964
  function relativeWorkspaceReads(readPaths, cwd) {
@@ -19848,7 +20222,6 @@ var require_anthropic_api = __commonJS({
19848
20222
  }
19849
20223
  async execute(dispatch, signal) {
19850
20224
  const { prompt, spec } = (0, loom_1.renderPromptExecution)(dispatch.inputs);
19851
- const outputLabel = spec.outputLabel;
19852
20225
  const providerHints = (0, loom_1.pickProviderHints)(spec, this.instance.capabilityId);
19853
20226
  const apiKey = await this.instance.resolveKey(providerHints);
19854
20227
  const hintedModel = providerHints.model;
@@ -19857,6 +20230,33 @@ var require_anthropic_api = __commonJS({
19857
20230
  max_tokens: this.instance.maxTokens,
19858
20231
  messages: [{ role: "user", content: prompt }]
19859
20232
  };
20233
+ return this.callApi(apiKey, body, spec, spec.outputLabel, signal);
20234
+ }
20235
+ /**
20236
+ * In-session gate repair (ADR 0017): re-call the Messages API with the prior
20237
+ * conversation plus the model's own last turn plus the repair turn, under the
20238
+ * same output contract. A fresh conversation is not started — the appended
20239
+ * assistant turn gives the model its previous answer to correct.
20240
+ */
20241
+ async continue(continuation, followUp, signal, _ctx) {
20242
+ const handle = continuation;
20243
+ if (!handle || !Array.isArray(handle.messages)) {
20244
+ throw new Error("anthropic-api continue: missing conversation to resume");
20245
+ }
20246
+ const spec = {
20247
+ rendering: "producer",
20248
+ outputLabel: handle.outputLabel,
20249
+ outputSchema: handle.outputSchema
20250
+ };
20251
+ const body = {
20252
+ model: handle.model,
20253
+ max_tokens: handle.maxTokens,
20254
+ messages: [...handle.messages, { role: "user", content: followUp }]
20255
+ };
20256
+ return this.callApi(handle.apiKey, body, spec, handle.outputLabel, signal);
20257
+ }
20258
+ /** Shared Messages-API call + parse + validate, used by execute and continue. */
20259
+ async callApi(apiKey, body, spec, outputLabel, signal) {
19860
20260
  const timeoutController = new AbortController();
19861
20261
  const timer = setTimeout(() => timeoutController.abort(), this.instance.timeoutMs);
19862
20262
  function onAbort() {
@@ -19904,7 +20304,17 @@ var require_anthropic_api = __commonJS({
19904
20304
  outputs: [{ label: outputLabel, content: parsed ?? { raw: rawText } }],
19905
20305
  rawText,
19906
20306
  parsed,
19907
- ...usage ? { usage } : {}
20307
+ ...usage ? { usage } : {},
20308
+ // In-session repair handle (ADR 0017): the conversation with the model's
20309
+ // answer appended, ready for `continue()` to add the repair turn.
20310
+ continuation: {
20311
+ apiKey,
20312
+ model: body.model,
20313
+ maxTokens: body.max_tokens,
20314
+ messages: [...body.messages, { role: "assistant", content: rawText }],
20315
+ outputLabel,
20316
+ outputSchema: spec.outputSchema
20317
+ }
19908
20318
  };
19909
20319
  } finally {
19910
20320
  clearTimeout(timer);
@@ -20737,6 +21147,7 @@ var require_shuttle = __commonJS({
20737
21147
  var decommission_1 = require_decommission();
20738
21148
  var loom_1 = require_dist3();
20739
21149
  var chain_1 = require_chain();
21150
+ var output_gates_1 = require_output_gates();
20740
21151
  var keep_1 = require_dist4();
20741
21152
  var apply_1 = require_apply();
20742
21153
  var version_check_1 = require_version_check();
@@ -20751,6 +21162,37 @@ var require_shuttle = __commonJS({
20751
21162
  var audit_log_1 = require_audit_log();
20752
21163
  var spend_tracker_1 = require_spend_tracker();
20753
21164
  var store_1 = require_store();
21165
+ var GATE_REPAIR_BUDGET = 2;
21166
+ async function runOutputGates(dispatch, response, executor, signal, ctx) {
21167
+ let spec;
21168
+ try {
21169
+ spec = (0, loom_1.readPromptExecutionSpec)(dispatch.inputs);
21170
+ } catch {
21171
+ return { ok: true, response };
21172
+ }
21173
+ const gates = spec.gates;
21174
+ if (!gates || gates.length === 0)
21175
+ return { ok: true, response };
21176
+ let current = response;
21177
+ let result = (0, output_gates_1.evaluateOutputGates)(gates, current.parsed ?? {}, dispatch.inputs);
21178
+ let attempts = 0;
21179
+ while (!result.ok && attempts < GATE_REPAIR_BUDGET && current.continuation !== void 0 && typeof executor.continue === "function") {
21180
+ try {
21181
+ current = await executor.continue(current.continuation, (0, output_gates_1.renderRepairPrompt)(result.findings), signal, ctx);
21182
+ } catch {
21183
+ break;
21184
+ }
21185
+ result = (0, output_gates_1.evaluateOutputGates)(gates, current.parsed ?? {}, dispatch.inputs);
21186
+ attempts += 1;
21187
+ }
21188
+ if (result.ok)
21189
+ return { ok: true, response: current };
21190
+ const detail = result.findings.map((f) => `[${f.gate}] ${f.message}`).join(" | ");
21191
+ return {
21192
+ ok: false,
21193
+ reason: `output gate(s) failed${attempts > 0 ? ` after ${attempts} repair attempt(s)` : ""}: ${detail}`
21194
+ };
21195
+ }
20754
21196
  var Shuttle = class {
20755
21197
  brand;
20756
21198
  config;
@@ -20847,14 +21289,26 @@ var require_shuttle = __commonJS({
20847
21289
  }
20848
21290
  await observerChain.notify({ kind: "executor.dispatch.started", jobId, attendanceId, executor: executor.capability.id, strategy }, onObserverError);
20849
21291
  const dispatchStartedAt = Date.now();
21292
+ const executionCtx = {
21293
+ reportProgress: (channel, frames) => {
21294
+ void jobsClient.postProgress(jobId, { channel, frames }).catch(() => {
21295
+ });
21296
+ }
21297
+ };
20850
21298
  let response;
20851
21299
  try {
20852
- response = await executor.execute(dispatch, signal);
21300
+ response = await executor.execute(dispatch, signal, executionCtx);
20853
21301
  } catch (err) {
20854
21302
  const reason = `executor failed: ${err instanceof Error ? err.message : String(err)}`;
20855
21303
  await observerChain.notify({ kind: "attendance.closed", jobId, attendanceId, status: "failed", reason, totalDurationMs: Date.now() - startedAt }, onObserverError);
20856
21304
  return { status: "failed", reason };
20857
21305
  }
21306
+ const gated = await runOutputGates(dispatch, response, executor, signal, executionCtx);
21307
+ if (!gated.ok) {
21308
+ await observerChain.notify({ kind: "attendance.closed", jobId, attendanceId, status: "failed", reason: gated.reason, totalDurationMs: Date.now() - startedAt }, onObserverError);
21309
+ return { status: "failed", reason: gated.reason };
21310
+ }
21311
+ response = gated.response;
20858
21312
  await observerChain.notify({
20859
21313
  kind: "executor.dispatch.completed",
20860
21314
  jobId,
@@ -21527,58 +21981,14 @@ var import_path = require("path");
21527
21981
  var import_promises = require("fs/promises");
21528
21982
  var import_yaml = __toESM(require_dist());
21529
21983
  var import_shuttle = __toESM(require_dist5());
21530
-
21531
- // package.json
21532
- var package_default = {
21533
- name: "@whittlelabs/sifter",
21534
- version: "0.15.0",
21535
- description: "Whittle Sifter: paired AI reviewer for Whittle Sift job pools.",
21536
- bin: {
21537
- "whittle-sifter": "./dist/bin.js"
21538
- },
21539
- main: "dist/bin.js",
21540
- engines: {
21541
- node: ">=20"
21542
- },
21543
- scripts: {
21544
- dev: "tsx watch src/bin.ts",
21545
- build: "node scripts/bundle.mjs",
21546
- typecheck: "tsc --noEmit",
21547
- start: "node dist/bin.js",
21548
- lint: "eslint src/"
21549
- },
21550
- publishConfig: {
21551
- registry: "https://registry.npmjs.org/",
21552
- access: "public"
21553
- },
21554
- repository: {
21555
- type: "git",
21556
- url: "https://github.com/whittlelabs/whittlelabs.git",
21557
- directory: "apps/whittle-sifter"
21558
- },
21559
- author: "Whittle Labs",
21560
- license: "UNLICENSED",
21561
- private: false,
21562
- dependencies: {
21563
- "@whittlelabs/shuttle": "workspace:*",
21564
- yaml: "^2.7.1"
21565
- },
21566
- devDependencies: {
21567
- "@types/node": "^20.10.5",
21568
- esbuild: "^0.25.0",
21569
- tsx: "^4.21.0",
21570
- typescript: "^5.3.3"
21571
- }
21572
- };
21573
-
21574
- // src/brand.ts
21984
+ var buildVersion = true ? "0.17.0" : pkg.version;
21575
21985
  var sifterBrand = {
21576
21986
  product: {
21577
21987
  id: "sifter",
21578
21988
  title: "Whittle Sifter",
21579
21989
  cliBinary: "whittle-sifter",
21580
21990
  packageName: "@whittlelabs/sifter",
21581
- version: package_default.version,
21991
+ version: buildVersion,
21582
21992
  description: "Pairs with Whittle Sift to run AI code reviews on your hardware with your credentials."
21583
21993
  },
21584
21994
  paths: {
@@ -21723,7 +22133,7 @@ async function buildExecutorBlock(executor, ctx) {
21723
22133
  console.error("Keep refused its credentials \u2014 it was decommissioned from the");
21724
22134
  console.error("web app, or its secret was rotated by a newer pairing.");
21725
22135
  console.error("");
21726
- console.error(`Run \`${sifterBrand.product.command} init\` to pair again.`);
22136
+ console.error(`Run \`${sifterBrand.product.cliBinary} init\` to pair again.`);
21727
22137
  console.error("");
21728
22138
  process.exit(3);
21729
22139
  }