@whittlelabs/sifter 0.16.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 +259 -6
  2. package/bin.js.map +4 -4
  3. package/package.json +1 -1
package/bin.js CHANGED
@@ -15803,6 +15803,17 @@ var require_prompt_execution = __commonJS({
15803
15803
  }
15804
15804
  }
15805
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
+ }
15806
15817
  return spec;
15807
15818
  }
15808
15819
  function renderPromptExecution(inputs) {
@@ -16022,6 +16033,8 @@ var require_prompt_execution2 = __commonJS({
16022
16033
  spec.costCapHint = options.costCapHint;
16023
16034
  if (options.progressChannel !== void 0)
16024
16035
  spec.progressChannel = options.progressChannel;
16036
+ if (options.outputGates && options.outputGates.length > 0)
16037
+ spec.gates = options.outputGates;
16025
16038
  if (options.rendering === "producer" && !options.prompt) {
16026
16039
  throw new Error("rendering='producer' requires `prompt`");
16027
16040
  }
@@ -18081,6 +18094,7 @@ var require_canonicalise = __commonJS({
18081
18094
  promptTemplate: templateText,
18082
18095
  promptTemplatePath: promptOptions.promptTemplate,
18083
18096
  outputSchema: toJsonSchema(promptOptions.outputSchema),
18097
+ ...promptOptions.outputGates ? { outputGates: promptOptions.outputGates } : {},
18084
18098
  outputLabel,
18085
18099
  rendering: promptOptions.rendering,
18086
18100
  ...promptOptions.maxAttempts !== void 0 ? { maxAttempts: promptOptions.maxAttempts } : {},
@@ -18363,6 +18377,128 @@ var require_chain = __commonJS({
18363
18377
  }
18364
18378
  });
18365
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
+
18366
18502
  // ../../packages/keep/dist/client.js
18367
18503
  var require_client2 = __commonJS({
18368
18504
  "../../packages/keep/dist/client.js"(exports2) {
@@ -19533,11 +19669,40 @@ var require_claude_code = __commonJS({
19533
19669
  });
19534
19670
  }
19535
19671
  }
19536
- runClaude(prompt, spec, cwd, childEnv, signal, modelOverride, reporter) {
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) {
19537
19700
  const outputLabel = spec.outputLabel;
19538
19701
  const model = modelOverride ?? this.config.model;
19539
19702
  return new Promise((resolve, reject) => {
19540
19703
  const args = ["--print", "--output-format", "stream-json", "--verbose"];
19704
+ if (resumeSessionId)
19705
+ args.push("--resume", resumeSessionId);
19541
19706
  if (spec.outputSchema && typeof spec.outputSchema === "object" && Object.keys(spec.outputSchema).length > 0) {
19542
19707
  args.push("--json-schema", JSON.stringify(spec.outputSchema));
19543
19708
  }
@@ -19632,7 +19797,18 @@ var require_claude_code = __commonJS({
19632
19797
  aiProvider: "claude-code",
19633
19798
  ...model ? { aiModel: model } : {},
19634
19799
  ...stream.usage ?? {}
19635
- }
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
+ } : {}
19636
19812
  },
19637
19813
  readPaths: stream.readPaths,
19638
19814
  toolText: stream.toolText
@@ -19732,6 +19908,7 @@ var require_claude_code = __commonJS({
19732
19908
  let usage = null;
19733
19909
  let resultMeta = null;
19734
19910
  let structuredOutput = null;
19911
+ let sessionId = null;
19735
19912
  let toolText = "";
19736
19913
  for (const line of stdout.split("\n")) {
19737
19914
  const trimmed = line.trim();
@@ -19743,6 +19920,8 @@ var require_claude_code = __commonJS({
19743
19920
  } catch {
19744
19921
  continue;
19745
19922
  }
19923
+ if (typeof event.session_id === "string")
19924
+ sessionId = event.session_id;
19746
19925
  if (event.type === "assistant") {
19747
19926
  const content = event.message?.content;
19748
19927
  if (!Array.isArray(content))
@@ -19779,7 +19958,7 @@ var require_claude_code = __commonJS({
19779
19958
  };
19780
19959
  }
19781
19960
  }
19782
- return { finalText, readPaths, usage, resultMeta, structuredOutput, toolText };
19961
+ return { finalText, readPaths, usage, resultMeta, structuredOutput, sessionId, toolText };
19783
19962
  }
19784
19963
  var MAX_REPORTED_READS = 2e3;
19785
19964
  function relativeWorkspaceReads(readPaths, cwd) {
@@ -20043,7 +20222,6 @@ var require_anthropic_api = __commonJS({
20043
20222
  }
20044
20223
  async execute(dispatch, signal) {
20045
20224
  const { prompt, spec } = (0, loom_1.renderPromptExecution)(dispatch.inputs);
20046
- const outputLabel = spec.outputLabel;
20047
20225
  const providerHints = (0, loom_1.pickProviderHints)(spec, this.instance.capabilityId);
20048
20226
  const apiKey = await this.instance.resolveKey(providerHints);
20049
20227
  const hintedModel = providerHints.model;
@@ -20052,6 +20230,33 @@ var require_anthropic_api = __commonJS({
20052
20230
  max_tokens: this.instance.maxTokens,
20053
20231
  messages: [{ role: "user", content: prompt }]
20054
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) {
20055
20260
  const timeoutController = new AbortController();
20056
20261
  const timer = setTimeout(() => timeoutController.abort(), this.instance.timeoutMs);
20057
20262
  function onAbort() {
@@ -20099,7 +20304,17 @@ var require_anthropic_api = __commonJS({
20099
20304
  outputs: [{ label: outputLabel, content: parsed ?? { raw: rawText } }],
20100
20305
  rawText,
20101
20306
  parsed,
20102
- ...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
+ }
20103
20318
  };
20104
20319
  } finally {
20105
20320
  clearTimeout(timer);
@@ -20932,6 +21147,7 @@ var require_shuttle = __commonJS({
20932
21147
  var decommission_1 = require_decommission();
20933
21148
  var loom_1 = require_dist3();
20934
21149
  var chain_1 = require_chain();
21150
+ var output_gates_1 = require_output_gates();
20935
21151
  var keep_1 = require_dist4();
20936
21152
  var apply_1 = require_apply();
20937
21153
  var version_check_1 = require_version_check();
@@ -20946,6 +21162,37 @@ var require_shuttle = __commonJS({
20946
21162
  var audit_log_1 = require_audit_log();
20947
21163
  var spend_tracker_1 = require_spend_tracker();
20948
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
+ }
20949
21196
  var Shuttle = class {
20950
21197
  brand;
20951
21198
  config;
@@ -21056,6 +21303,12 @@ var require_shuttle = __commonJS({
21056
21303
  await observerChain.notify({ kind: "attendance.closed", jobId, attendanceId, status: "failed", reason, totalDurationMs: Date.now() - startedAt }, onObserverError);
21057
21304
  return { status: "failed", reason };
21058
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;
21059
21312
  await observerChain.notify({
21060
21313
  kind: "executor.dispatch.completed",
21061
21314
  jobId,
@@ -21728,7 +21981,7 @@ var import_path = require("path");
21728
21981
  var import_promises = require("fs/promises");
21729
21982
  var import_yaml = __toESM(require_dist());
21730
21983
  var import_shuttle = __toESM(require_dist5());
21731
- var buildVersion = true ? "0.16.0" : pkg.version;
21984
+ var buildVersion = true ? "0.17.0" : pkg.version;
21732
21985
  var sifterBrand = {
21733
21986
  product: {
21734
21987
  id: "sifter",