@whittlelabs/sifter 0.15.0 → 0.16.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 +217 -60
  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
  });
@@ -16007,6 +16020,8 @@ var require_prompt_execution2 = __commonJS({
16007
16020
  spec.providerHints = options.providerHints;
16008
16021
  if (options.costCapHint)
16009
16022
  spec.costCapHint = options.costCapHint;
16023
+ if (options.progressChannel !== void 0)
16024
+ spec.progressChannel = options.progressChannel;
16010
16025
  if (options.rendering === "producer" && !options.prompt) {
16011
16026
  throw new Error("rendering='producer' requires `prompt`");
16012
16027
  }
@@ -18880,6 +18895,155 @@ var require_validate_output = __commonJS({
18880
18895
  }
18881
18896
  });
18882
18897
 
18898
+ // ../../packages/shuttle/dist/executors/progress.js
18899
+ var require_progress = __commonJS({
18900
+ "../../packages/shuttle/dist/executors/progress.js"(exports2) {
18901
+ "use strict";
18902
+ Object.defineProperty(exports2, "__esModule", { value: true });
18903
+ exports2.StreamJsonTap = exports2.ProgressReporter = void 0;
18904
+ var ProgressReporter = class {
18905
+ sink;
18906
+ seq = 0;
18907
+ pending = [];
18908
+ textBuf = "";
18909
+ timer = null;
18910
+ closed = false;
18911
+ flushMs;
18912
+ maxChars;
18913
+ constructor(sink, options = {}) {
18914
+ this.sink = sink;
18915
+ this.flushMs = options.flushMs ?? 400;
18916
+ this.maxChars = options.maxChars ?? 1200;
18917
+ }
18918
+ reasoning(text) {
18919
+ if (this.closed || !text)
18920
+ return;
18921
+ this.textBuf += text;
18922
+ if (this.textBuf.length >= this.maxChars) {
18923
+ this.flush();
18924
+ } else {
18925
+ this.arm();
18926
+ }
18927
+ }
18928
+ tool(name, target) {
18929
+ if (this.closed || !name)
18930
+ return;
18931
+ this.drainText();
18932
+ this.push({ kind: "tool", tool: target ? { name, target } : { name } });
18933
+ this.flush();
18934
+ }
18935
+ /** Flush pending text + frames now. */
18936
+ flush() {
18937
+ if (this.closed)
18938
+ return;
18939
+ this.drainText();
18940
+ this.clearTimer();
18941
+ if (this.pending.length === 0)
18942
+ return;
18943
+ const batch = this.pending;
18944
+ this.pending = [];
18945
+ try {
18946
+ this.sink(batch);
18947
+ } catch {
18948
+ }
18949
+ }
18950
+ /** Final flush; no further frames are emitted after this. */
18951
+ close() {
18952
+ this.flush();
18953
+ this.closed = true;
18954
+ this.clearTimer();
18955
+ }
18956
+ drainText() {
18957
+ if (this.textBuf.length === 0)
18958
+ return;
18959
+ this.push({ kind: "reasoning", text: this.textBuf });
18960
+ this.textBuf = "";
18961
+ }
18962
+ push(frame) {
18963
+ this.pending.push({ seq: this.seq++, at: (/* @__PURE__ */ new Date()).toISOString(), ...frame });
18964
+ }
18965
+ arm() {
18966
+ if (this.timer)
18967
+ return;
18968
+ this.timer = setTimeout(() => {
18969
+ this.timer = null;
18970
+ this.flush();
18971
+ }, this.flushMs);
18972
+ this.timer.unref?.();
18973
+ }
18974
+ clearTimer() {
18975
+ if (this.timer) {
18976
+ clearTimeout(this.timer);
18977
+ this.timer = null;
18978
+ }
18979
+ }
18980
+ };
18981
+ exports2.ProgressReporter = ProgressReporter;
18982
+ var StreamJsonTap = class {
18983
+ handlers;
18984
+ buf = "";
18985
+ constructor(handlers) {
18986
+ this.handlers = handlers;
18987
+ }
18988
+ /** Feed a raw stdout chunk. Emits handlers for every complete line parsed. */
18989
+ push(chunk) {
18990
+ this.buf += chunk;
18991
+ let nl;
18992
+ while ((nl = this.buf.indexOf("\n")) !== -1) {
18993
+ const line = this.buf.slice(0, nl);
18994
+ this.buf = this.buf.slice(nl + 1);
18995
+ this.consumeLine(line);
18996
+ }
18997
+ }
18998
+ /** Flush a trailing partial line (best-effort; usually empty at process close). */
18999
+ end() {
19000
+ if (this.buf.trim().length > 0)
19001
+ this.consumeLine(this.buf);
19002
+ this.buf = "";
19003
+ }
19004
+ consumeLine(line) {
19005
+ const trimmed = line.trim();
19006
+ if (!trimmed.startsWith("{"))
19007
+ return;
19008
+ let event;
19009
+ try {
19010
+ event = JSON.parse(trimmed);
19011
+ } catch {
19012
+ return;
19013
+ }
19014
+ if (event.type === "assistant") {
19015
+ const content = event.message?.content;
19016
+ if (!Array.isArray(content))
19017
+ return;
19018
+ for (const block of content) {
19019
+ const b = block;
19020
+ if (b?.type === "text" && typeof b.text === "string" && b.text.length > 0) {
19021
+ this.handlers.onReasoning?.(b.text);
19022
+ } else if (b?.type === "thinking" && typeof b.thinking === "string" && b.thinking.length > 0) {
19023
+ this.handlers.onReasoning?.(b.thinking);
19024
+ } else if (b?.type === "tool_use" && typeof b.name === "string") {
19025
+ this.handlers.onTool?.(b.name, toolTarget(b.input));
19026
+ }
19027
+ }
19028
+ } else if (event.type === "result") {
19029
+ this.handlers.onResult?.();
19030
+ }
19031
+ }
19032
+ };
19033
+ exports2.StreamJsonTap = StreamJsonTap;
19034
+ function toolTarget(input) {
19035
+ if (!input)
19036
+ return void 0;
19037
+ for (const key of ["file_path", "path", "pattern", "command", "url", "query", "notebook_path"]) {
19038
+ const v = input[key];
19039
+ if (typeof v === "string" && v.length > 0)
19040
+ return v;
19041
+ }
19042
+ return void 0;
19043
+ }
19044
+ }
19045
+ });
19046
+
18883
19047
  // ../../packages/shuttle/dist/executors/workspace.js
18884
19048
  var require_workspace = __commonJS({
18885
19049
  "../../packages/shuttle/dist/executors/workspace.js"(exports2) {
@@ -19111,7 +19275,7 @@ var require_overlay = __commonJS({
19111
19275
  const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
19112
19276
  const allowed = new Set(options.allowedHosts.map(normalizeOrigin).filter((o) => !!o));
19113
19277
  const rootReal = await fs_1.promises.realpath(runDir);
19114
- const result = { written: 0, skipped: [] };
19278
+ const result = { written: 0, writtenPaths: [], skipped: [] };
19115
19279
  const files = Array.isArray(overlay.files) ? overlay.files : [];
19116
19280
  for (const file of files) {
19117
19281
  const skip = (reason) => result.skipped.push({ path: String(file?.path ?? "?"), reason });
@@ -19145,6 +19309,7 @@ var require_overlay = __commonJS({
19145
19309
  await fs_1.promises.mkdir(path_1.default.dirname(dest), { recursive: true });
19146
19310
  await fs_1.promises.writeFile(dest, content);
19147
19311
  result.written += 1;
19312
+ result.writtenPaths.push(file.path);
19148
19313
  } catch (err) {
19149
19314
  skip(err instanceof Error ? err.message : String(err));
19150
19315
  }
@@ -19258,9 +19423,11 @@ var require_claude_code = __commonJS({
19258
19423
  var fs_1 = require("fs");
19259
19424
  var os_1 = __importDefault(require("os"));
19260
19425
  var path_1 = __importDefault(require("path"));
19426
+ var string_decoder_1 = require("string_decoder");
19261
19427
  var loom_1 = require_dist3();
19262
19428
  var keep_1 = require_dist4();
19263
19429
  var validate_output_1 = require_validate_output();
19430
+ var progress_1 = require_progress();
19264
19431
  var workspace_1 = require_workspace();
19265
19432
  var overlay_1 = require_overlay();
19266
19433
  var env_ref_1 = require_env_ref();
@@ -19281,7 +19448,7 @@ var require_claude_code = __commonJS({
19281
19448
  outputTokens: spec.costCapHint?.estimatedOutputTokens
19282
19449
  };
19283
19450
  }
19284
- async execute(dispatch, signal) {
19451
+ async execute(dispatch, signal, ctx) {
19285
19452
  const { prompt, spec } = (0, loom_1.renderPromptExecution)(dispatch.inputs);
19286
19453
  let credentials = null;
19287
19454
  if (this.config.resolveCredentials) {
@@ -19306,6 +19473,7 @@ var require_claude_code = __commonJS({
19306
19473
  let workspace = null;
19307
19474
  let promptOnlyScratch = null;
19308
19475
  let overlayResult = null;
19476
+ const reporter = spec.progressChannel && ctx?.reportProgress ? new progress_1.ProgressReporter((frames) => ctx.reportProgress(spec.progressChannel, frames)) : null;
19309
19477
  try {
19310
19478
  if (spec.workspace) {
19311
19479
  workspace = await (0, workspace_1.prepareWorkspace)(spec.workspace, {
@@ -19327,11 +19495,16 @@ var require_claude_code = __commonJS({
19327
19495
  promptOnlyScratch = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-cc-run-"));
19328
19496
  }
19329
19497
  const runCwd = workspace?.cwd ?? promptOnlyScratch;
19330
- const { response, readPaths } = await this.runClaude(prompt, spec, runCwd, childEnv, signal, hintedModel);
19498
+ const { response, readPaths, toolText } = await this.runClaude(prompt, spec, runCwd, childEnv, signal, hintedModel, reporter);
19331
19499
  if (workspace) {
19500
+ const reads = [...readPaths];
19501
+ for (const rel of overlayResult?.writtenPaths ?? []) {
19502
+ if (toolText.includes(rel))
19503
+ reads.push(path_1.default.join(runCwd, rel));
19504
+ }
19332
19505
  response.outputs.push({
19333
19506
  label: "workspace_reads",
19334
- content: { paths: relativeWorkspaceReads(readPaths, runCwd) }
19507
+ content: { paths: relativeWorkspaceReads(reads, runCwd) }
19335
19508
  });
19336
19509
  response.outputs.push({
19337
19510
  label: "workspace",
@@ -19349,6 +19522,7 @@ var require_claude_code = __commonJS({
19349
19522
  }
19350
19523
  return response;
19351
19524
  } finally {
19525
+ reporter?.close();
19352
19526
  if (workspace)
19353
19527
  await workspace.cleanup();
19354
19528
  if (promptOnlyScratch)
@@ -19359,7 +19533,7 @@ var require_claude_code = __commonJS({
19359
19533
  });
19360
19534
  }
19361
19535
  }
19362
- runClaude(prompt, spec, cwd, childEnv, signal, modelOverride) {
19536
+ runClaude(prompt, spec, cwd, childEnv, signal, modelOverride, reporter) {
19363
19537
  const outputLabel = spec.outputLabel;
19364
19538
  const model = modelOverride ?? this.config.model;
19365
19539
  return new Promise((resolve, reject) => {
@@ -19395,7 +19569,17 @@ var require_claude_code = __commonJS({
19395
19569
  settled = true;
19396
19570
  fn();
19397
19571
  };
19398
- child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
19572
+ const tap = reporter ? new progress_1.StreamJsonTap({
19573
+ onReasoning: (t) => reporter.reasoning(t),
19574
+ onTool: (name, target) => reporter.tool(name, target),
19575
+ onResult: () => reporter.flush()
19576
+ }) : null;
19577
+ const decoder = tap ? new string_decoder_1.StringDecoder("utf8") : null;
19578
+ child.stdout.on("data", (chunk) => {
19579
+ stdoutChunks.push(chunk);
19580
+ if (tap && decoder)
19581
+ tap.push(decoder.write(chunk));
19582
+ });
19399
19583
  child.stdin.write(prompt);
19400
19584
  child.stdin.end();
19401
19585
  const timeout = this.config.timeout ?? DEFAULT_TIMEOUT;
@@ -19422,6 +19606,10 @@ var require_claude_code = __commonJS({
19422
19606
  child.on("close", (code) => {
19423
19607
  clearTimeout(timer);
19424
19608
  signal.removeEventListener("abort", onAbort);
19609
+ if (tap && decoder) {
19610
+ tap.push(decoder.end());
19611
+ tap.end();
19612
+ }
19425
19613
  const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
19426
19614
  if (code === 0) {
19427
19615
  try {
@@ -19446,7 +19634,8 @@ var require_claude_code = __commonJS({
19446
19634
  ...stream.usage ?? {}
19447
19635
  }
19448
19636
  },
19449
- readPaths: stream.readPaths
19637
+ readPaths: stream.readPaths,
19638
+ toolText: stream.toolText
19450
19639
  }));
19451
19640
  } catch (err) {
19452
19641
  settle(() => reject(err instanceof Error ? err : new Error(String(err))));
@@ -19543,6 +19732,7 @@ var require_claude_code = __commonJS({
19543
19732
  let usage = null;
19544
19733
  let resultMeta = null;
19545
19734
  let structuredOutput = null;
19735
+ let toolText = "";
19546
19736
  for (const line of stdout.split("\n")) {
19547
19737
  const trimmed = line.trim();
19548
19738
  if (!trimmed.startsWith("{"))
@@ -19559,9 +19749,14 @@ var require_claude_code = __commonJS({
19559
19749
  continue;
19560
19750
  for (const block of content) {
19561
19751
  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);
19752
+ if (b?.type !== "tool_use")
19753
+ continue;
19754
+ const fp = b.input?.file_path;
19755
+ if (b.name === "Read" && typeof fp === "string") {
19756
+ readPaths.push(fp);
19564
19757
  }
19758
+ if (b.input && typeof b.input === "object")
19759
+ toolText += JSON.stringify(b.input);
19565
19760
  }
19566
19761
  } else if (event.type === "result") {
19567
19762
  if (typeof event.result === "string" && event.result.trim().length > 0) {
@@ -19584,7 +19779,7 @@ var require_claude_code = __commonJS({
19584
19779
  };
19585
19780
  }
19586
19781
  }
19587
- return { finalText, readPaths, usage, resultMeta, structuredOutput };
19782
+ return { finalText, readPaths, usage, resultMeta, structuredOutput, toolText };
19588
19783
  }
19589
19784
  var MAX_REPORTED_READS = 2e3;
19590
19785
  function relativeWorkspaceReads(readPaths, cwd) {
@@ -20847,9 +21042,15 @@ var require_shuttle = __commonJS({
20847
21042
  }
20848
21043
  await observerChain.notify({ kind: "executor.dispatch.started", jobId, attendanceId, executor: executor.capability.id, strategy }, onObserverError);
20849
21044
  const dispatchStartedAt = Date.now();
21045
+ const executionCtx = {
21046
+ reportProgress: (channel, frames) => {
21047
+ void jobsClient.postProgress(jobId, { channel, frames }).catch(() => {
21048
+ });
21049
+ }
21050
+ };
20850
21051
  let response;
20851
21052
  try {
20852
- response = await executor.execute(dispatch, signal);
21053
+ response = await executor.execute(dispatch, signal, executionCtx);
20853
21054
  } catch (err) {
20854
21055
  const reason = `executor failed: ${err instanceof Error ? err.message : String(err)}`;
20855
21056
  await observerChain.notify({ kind: "attendance.closed", jobId, attendanceId, status: "failed", reason, totalDurationMs: Date.now() - startedAt }, onObserverError);
@@ -21527,58 +21728,14 @@ var import_path = require("path");
21527
21728
  var import_promises = require("fs/promises");
21528
21729
  var import_yaml = __toESM(require_dist());
21529
21730
  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
21731
+ var buildVersion = true ? "0.16.0" : pkg.version;
21575
21732
  var sifterBrand = {
21576
21733
  product: {
21577
21734
  id: "sifter",
21578
21735
  title: "Whittle Sifter",
21579
21736
  cliBinary: "whittle-sifter",
21580
21737
  packageName: "@whittlelabs/sifter",
21581
- version: package_default.version,
21738
+ version: buildVersion,
21582
21739
  description: "Pairs with Whittle Sift to run AI code reviews on your hardware with your credentials."
21583
21740
  },
21584
21741
  paths: {
@@ -21723,7 +21880,7 @@ async function buildExecutorBlock(executor, ctx) {
21723
21880
  console.error("Keep refused its credentials \u2014 it was decommissioned from the");
21724
21881
  console.error("web app, or its secret was rotated by a newer pairing.");
21725
21882
  console.error("");
21726
- console.error(`Run \`${sifterBrand.product.command} init\` to pair again.`);
21883
+ console.error(`Run \`${sifterBrand.product.cliBinary} init\` to pair again.`);
21727
21884
  console.error("");
21728
21885
  process.exit(3);
21729
21886
  }