@whittlelabs/sifter 0.14.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 +256 -129
  2. package/bin.js.map +4 -4
  3. package/package.json +1 -1
package/bin.js CHANGED
@@ -3538,7 +3538,7 @@ var require_init = __commonJS({
3538
3538
  var store_1 = require_store();
3539
3539
  var spend_store_1 = require_spend_store();
3540
3540
  function buildInitCommand(brand) {
3541
- return new commander_1.Command("init").description(`Pair this ${brand.product.title} with Keep and seed local state`).option("--pairing-code <code>", "Pairing code from the product UI").option("--keep-url <url>", "Override the Keep API URL from BrandConfig").option("--jobs-url <url>", "Override the Jobs API URL from BrandConfig").option("--executor <type>", `Executor to write into ${brand.product.id}.yaml (e.g. claude-code). Must be in the brand's executor allowlist.`).option("--poll-interval <ms>", "Run-loop poll interval in milliseconds").option("--daily-cap <tokens>", "Daily token cap; overrides the brand default").option("--monthly-cap <tokens>", "Monthly token cap; overrides the brand default").option("--cwd <path>", "Working directory for executors that run a local subprocess (e.g. claude-code)").option("-p, --profile <name>", 'Config profile to pair into (isolates this pairing under its own dir; default "default")').action(async (options) => {
3541
+ return new commander_1.Command("init").description(`Pair this ${brand.product.title} with Keep and seed local state`).option("--pairing-code <code>", "Pairing code from the product UI").option("--keep-url <url>", "Override the Keep API URL from BrandConfig").option("--jobs-url <url>", "Override the Jobs API URL from BrandConfig").option("--executor <type>", `Executor to write into ${brand.product.id}.yaml (e.g. claude-code). Must be in the brand's executor allowlist.`).option("--poll-interval <ms>", "Run-loop poll interval in milliseconds").option("--daily-cap <tokens>", "Daily token cap; overrides the brand default").option("--monthly-cap <tokens>", "Monthly token cap; overrides the brand default").option("--tmp-dir <path>", "Base directory for each job's throwaway checkout (defaults to the OS temp dir; set only when that is unsuitable)").option("-p, --profile <name>", 'Config profile to pair into (isolates this pairing under its own dir; default "default")').action(async (options) => {
3542
3542
  try {
3543
3543
  const profile = (0, apply_1.resolveProfile)(brand, options.profile);
3544
3544
  const setupOptions = parseSetupOptions(brand, options);
@@ -3621,8 +3621,8 @@ var require_init = __commonJS({
3621
3621
  if (raw.monthlyCap !== void 0) {
3622
3622
  out.monthlyCap = requireNonNegativeInt("--monthly-cap", raw.monthlyCap);
3623
3623
  }
3624
- if (raw.cwd !== void 0) {
3625
- out.cwd = raw.cwd;
3624
+ if (raw.tmpDir !== void 0) {
3625
+ out.tmpDir = raw.tmpDir;
3626
3626
  }
3627
3627
  return out;
3628
3628
  }
@@ -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) {
@@ -18896,10 +19060,11 @@ var require_workspace = __commonJS({
18896
19060
  var path_1 = __importDefault(require("path"));
18897
19061
  var DEFAULT_GIT_TIMEOUT_MS = 12e4;
18898
19062
  var DEFAULT_GIT_PROTOCOLS = "https:ssh:git";
18899
- async function prepareWorkspace(repoDir, workspace, options = {}) {
19063
+ async function prepareWorkspace(workspace, options = {}) {
19064
+ const tmpBase = options.tmpDir ?? os_1.default.tmpdir();
18900
19065
  const resolved = readGitWorkspace(workspace);
18901
19066
  if ("warning" in resolved) {
18902
- return degradedWorkspace(resolved.warning);
19067
+ return degradedWorkspace(resolved.warning, tmpBase);
18903
19068
  }
18904
19069
  const gitWorkspace = resolved.git;
18905
19070
  const timeoutMs = options.commandTimeoutMs ?? DEFAULT_GIT_TIMEOUT_MS;
@@ -18907,7 +19072,6 @@ var require_workspace = __commonJS({
18907
19072
  const signal = options.signal;
18908
19073
  const authEnv = options.gitToken !== void 0 ? buildGitAuthEnv(gitWorkspace.remote, options.gitToken) : void 0;
18909
19074
  const git = (args, cwd) => runGit(args, cwd, { timeoutMs, signal, allowedProtocols, extraEnv: authEnv });
18910
- const gitCleanup = (args, cwd) => runGit(args, cwd, { timeoutMs, allowedProtocols });
18911
19075
  const hasCommit = (dir) => git(["cat-file", "-e", `${gitWorkspace.commit}^{commit}`], dir).then((r) => r.code === 0);
18912
19076
  const fetchCommit = async (dir, opts) => {
18913
19077
  if (await hasCommit(dir))
@@ -18915,9 +19079,6 @@ var require_workspace = __commonJS({
18915
19079
  const depth = opts.shallow ? ["--depth=1"] : [];
18916
19080
  const attempts = [];
18917
19081
  for (const ref of gitWorkspace.fetchRefs ?? []) {
18918
- if (opts.includeOrigin) {
18919
- attempts.push({ source: `origin ${ref}`, args: ["fetch", "--no-tags", ...depth, "origin", ref] });
18920
- }
18921
19082
  attempts.push({
18922
19083
  source: `${gitWorkspace.remote} ${ref}`,
18923
19084
  args: ["fetch", "--no-tags", ...depth, gitWorkspace.remote, ref]
@@ -18941,31 +19102,7 @@ var require_workspace = __commonJS({
18941
19102
  return failures2;
18942
19103
  };
18943
19104
  const notMaterialized = (failures2) => `could not materialize ${gitWorkspace.commit} from ${gitWorkspace.remote}` + (failures2.length > 0 ? ` (${failures2.join("; ")})` : "");
18944
- const isRepo = (await git(["rev-parse", "--git-dir"], repoDir)).code === 0;
18945
- if (isRepo) {
18946
- const failures2 = await fetchCommit(repoDir, { includeOrigin: true, shallow: false });
18947
- if (!await hasCommit(repoDir))
18948
- return degradedWorkspace(notMaterialized(failures2));
18949
- const tempRoot2 = await fs_1.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "shuttle-ws-"));
18950
- const worktreeDir = path_1.default.join(tempRoot2, "checkout");
18951
- const add = await git(["worktree", "add", "--detach", worktreeDir, gitWorkspace.commit], repoDir);
18952
- if (add.code !== 0) {
18953
- await fs_1.promises.rm(tempRoot2, { recursive: true, force: true }).catch(() => {
18954
- });
18955
- return degradedWorkspace(`git worktree add failed: ${firstLine(add.stderr)}`);
18956
- }
18957
- return {
18958
- cwd: worktreeDir,
18959
- materialized: true,
18960
- cleanup: async () => {
18961
- await gitCleanup(["worktree", "remove", "--force", worktreeDir], repoDir);
18962
- await gitCleanup(["worktree", "prune"], repoDir);
18963
- await fs_1.promises.rm(tempRoot2, { recursive: true, force: true }).catch(() => {
18964
- });
18965
- }
18966
- };
18967
- }
18968
- const tempRoot = await fs_1.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "shuttle-ws-"));
19105
+ const tempRoot = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-ws-"));
18969
19106
  const checkoutDir = path_1.default.join(tempRoot, "checkout");
18970
19107
  const removeTempRoot = async () => {
18971
19108
  await fs_1.promises.rm(tempRoot, { recursive: true, force: true }).catch(() => {
@@ -18975,17 +19112,17 @@ var require_workspace = __commonJS({
18975
19112
  const init = await git(["init", "-q"], checkoutDir);
18976
19113
  if (init.code !== 0) {
18977
19114
  await removeTempRoot();
18978
- return degradedWorkspace(`git init failed: ${firstLine(init.stderr)}`);
19115
+ return degradedWorkspace(`git init failed: ${firstLine(init.stderr)}`, tmpBase);
18979
19116
  }
18980
- const failures = await fetchCommit(checkoutDir, { includeOrigin: false, shallow: true });
19117
+ const failures = await fetchCommit(checkoutDir, { shallow: true });
18981
19118
  if (!await hasCommit(checkoutDir)) {
18982
19119
  await removeTempRoot();
18983
- return degradedWorkspace(notMaterialized(failures));
19120
+ return degradedWorkspace(notMaterialized(failures), tmpBase);
18984
19121
  }
18985
19122
  const checkout = await git(["checkout", "--detach", gitWorkspace.commit], checkoutDir);
18986
19123
  if (checkout.code !== 0) {
18987
19124
  await removeTempRoot();
18988
- return degradedWorkspace(`git checkout failed: ${firstLine(checkout.stderr)}`);
19125
+ return degradedWorkspace(`git checkout failed: ${firstLine(checkout.stderr)}`, tmpBase);
18989
19126
  }
18990
19127
  return { cwd: checkoutDir, materialized: true, cleanup: removeTempRoot };
18991
19128
  }
@@ -19033,8 +19170,8 @@ var require_workspace = __commonJS({
19033
19170
  }
19034
19171
  return { git };
19035
19172
  }
19036
- async function degradedWorkspace(warning) {
19037
- const scratchDir = await fs_1.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "shuttle-ws-degraded-"));
19173
+ async function degradedWorkspace(warning, tmpBase = os_1.default.tmpdir()) {
19174
+ const scratchDir = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-ws-degraded-"));
19038
19175
  return {
19039
19176
  cwd: scratchDir,
19040
19177
  materialized: false,
@@ -19138,7 +19275,7 @@ var require_overlay = __commonJS({
19138
19275
  const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
19139
19276
  const allowed = new Set(options.allowedHosts.map(normalizeOrigin).filter((o) => !!o));
19140
19277
  const rootReal = await fs_1.promises.realpath(runDir);
19141
- const result = { written: 0, skipped: [] };
19278
+ const result = { written: 0, writtenPaths: [], skipped: [] };
19142
19279
  const files = Array.isArray(overlay.files) ? overlay.files : [];
19143
19280
  for (const file of files) {
19144
19281
  const skip = (reason) => result.skipped.push({ path: String(file?.path ?? "?"), reason });
@@ -19172,6 +19309,7 @@ var require_overlay = __commonJS({
19172
19309
  await fs_1.promises.mkdir(path_1.default.dirname(dest), { recursive: true });
19173
19310
  await fs_1.promises.writeFile(dest, content);
19174
19311
  result.written += 1;
19312
+ result.writtenPaths.push(file.path);
19175
19313
  } catch (err) {
19176
19314
  skip(err instanceof Error ? err.message : String(err));
19177
19315
  }
@@ -19285,9 +19423,11 @@ var require_claude_code = __commonJS({
19285
19423
  var fs_1 = require("fs");
19286
19424
  var os_1 = __importDefault(require("os"));
19287
19425
  var path_1 = __importDefault(require("path"));
19426
+ var string_decoder_1 = require("string_decoder");
19288
19427
  var loom_1 = require_dist3();
19289
19428
  var keep_1 = require_dist4();
19290
19429
  var validate_output_1 = require_validate_output();
19430
+ var progress_1 = require_progress();
19291
19431
  var workspace_1 = require_workspace();
19292
19432
  var overlay_1 = require_overlay();
19293
19433
  var env_ref_1 = require_env_ref();
@@ -19308,7 +19448,7 @@ var require_claude_code = __commonJS({
19308
19448
  outputTokens: spec.costCapHint?.estimatedOutputTokens
19309
19449
  };
19310
19450
  }
19311
- async execute(dispatch, signal) {
19451
+ async execute(dispatch, signal, ctx) {
19312
19452
  const { prompt, spec } = (0, loom_1.renderPromptExecution)(dispatch.inputs);
19313
19453
  let credentials = null;
19314
19454
  if (this.config.resolveCredentials) {
@@ -19329,12 +19469,16 @@ var require_claude_code = __commonJS({
19329
19469
  configDir = path_1.default.join(jobScratch, "claude-config");
19330
19470
  await fs_1.promises.mkdir(configDir);
19331
19471
  }
19472
+ const tmpBase = this.config.tmpDir ? (0, apply_1.expandHome)(this.config.tmpDir) : os_1.default.tmpdir();
19332
19473
  let workspace = null;
19474
+ let promptOnlyScratch = null;
19333
19475
  let overlayResult = null;
19476
+ const reporter = spec.progressChannel && ctx?.reportProgress ? new progress_1.ProgressReporter((frames) => ctx.reportProgress(spec.progressChannel, frames)) : null;
19334
19477
  try {
19335
19478
  if (spec.workspace) {
19336
- workspace = await (0, workspace_1.prepareWorkspace)(this.config.cwd, spec.workspace, {
19479
+ workspace = await (0, workspace_1.prepareWorkspace)(spec.workspace, {
19337
19480
  signal,
19481
+ tmpDir: tmpBase,
19338
19482
  ...credentials?.githubToken ? { gitToken: credentials.githubToken } : {}
19339
19483
  });
19340
19484
  if (spec.workspace.overlay) {
@@ -19347,12 +19491,20 @@ var require_claude_code = __commonJS({
19347
19491
  const childEnv = buildChildEnv(credentials, configDir);
19348
19492
  const claudeHints = (0, loom_1.pickProviderHints)(spec, "claude-code");
19349
19493
  const hintedModel = typeof claudeHints.model === "string" && claudeHints.model.length > 0 ? claudeHints.model : void 0;
19350
- const runCwd = workspace?.cwd ?? this.config.cwd;
19351
- const { response, readPaths } = await this.runClaude(prompt, spec, runCwd, childEnv, signal, hintedModel);
19494
+ if (!workspace) {
19495
+ promptOnlyScratch = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-cc-run-"));
19496
+ }
19497
+ const runCwd = workspace?.cwd ?? promptOnlyScratch;
19498
+ const { response, readPaths, toolText } = await this.runClaude(prompt, spec, runCwd, childEnv, signal, hintedModel, reporter);
19352
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
+ }
19353
19505
  response.outputs.push({
19354
19506
  label: "workspace_reads",
19355
- content: { paths: relativeWorkspaceReads(readPaths, runCwd) }
19507
+ content: { paths: relativeWorkspaceReads(reads, runCwd) }
19356
19508
  });
19357
19509
  response.outputs.push({
19358
19510
  label: "workspace",
@@ -19370,14 +19522,18 @@ var require_claude_code = __commonJS({
19370
19522
  }
19371
19523
  return response;
19372
19524
  } finally {
19525
+ reporter?.close();
19373
19526
  if (workspace)
19374
19527
  await workspace.cleanup();
19528
+ if (promptOnlyScratch)
19529
+ await fs_1.promises.rm(promptOnlyScratch, { recursive: true, force: true }).catch(() => {
19530
+ });
19375
19531
  if (jobScratch)
19376
19532
  await fs_1.promises.rm(jobScratch, { recursive: true, force: true }).catch(() => {
19377
19533
  });
19378
19534
  }
19379
19535
  }
19380
- runClaude(prompt, spec, cwd, childEnv, signal, modelOverride) {
19536
+ runClaude(prompt, spec, cwd, childEnv, signal, modelOverride, reporter) {
19381
19537
  const outputLabel = spec.outputLabel;
19382
19538
  const model = modelOverride ?? this.config.model;
19383
19539
  return new Promise((resolve, reject) => {
@@ -19413,7 +19569,17 @@ var require_claude_code = __commonJS({
19413
19569
  settled = true;
19414
19570
  fn();
19415
19571
  };
19416
- 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
+ });
19417
19583
  child.stdin.write(prompt);
19418
19584
  child.stdin.end();
19419
19585
  const timeout = this.config.timeout ?? DEFAULT_TIMEOUT;
@@ -19440,6 +19606,10 @@ var require_claude_code = __commonJS({
19440
19606
  child.on("close", (code) => {
19441
19607
  clearTimeout(timer);
19442
19608
  signal.removeEventListener("abort", onAbort);
19609
+ if (tap && decoder) {
19610
+ tap.push(decoder.end());
19611
+ tap.end();
19612
+ }
19443
19613
  const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
19444
19614
  if (code === 0) {
19445
19615
  try {
@@ -19464,7 +19634,8 @@ var require_claude_code = __commonJS({
19464
19634
  ...stream.usage ?? {}
19465
19635
  }
19466
19636
  },
19467
- readPaths: stream.readPaths
19637
+ readPaths: stream.readPaths,
19638
+ toolText: stream.toolText
19468
19639
  }));
19469
19640
  } catch (err) {
19470
19641
  settle(() => reject(err instanceof Error ? err : new Error(String(err))));
@@ -19477,12 +19648,13 @@ var require_claude_code = __commonJS({
19477
19648
  }
19478
19649
  };
19479
19650
  function createClaudeCodeExecutor(config, capabilityId = "claude-code", deps = {}) {
19480
- const cwdRaw = config.cwd;
19481
- if (typeof cwdRaw !== "string" || cwdRaw.length === 0) {
19482
- throw new Error('ClaudeCodeExecutor requires a non-empty "cwd" string in config');
19651
+ const validated = { capabilityId };
19652
+ if (config.tmpDir !== void 0) {
19653
+ if (typeof config.tmpDir !== "string" || config.tmpDir.length === 0) {
19654
+ throw new Error('"tmpDir" must be a non-empty string');
19655
+ }
19656
+ validated.tmpDir = config.tmpDir;
19483
19657
  }
19484
- const cwd = (0, apply_1.expandHome)(cwdRaw);
19485
- const validated = { cwd, capabilityId };
19486
19658
  if (config.model !== void 0) {
19487
19659
  if (typeof config.model !== "string")
19488
19660
  throw new Error('"model" must be a string');
@@ -19560,6 +19732,7 @@ var require_claude_code = __commonJS({
19560
19732
  let usage = null;
19561
19733
  let resultMeta = null;
19562
19734
  let structuredOutput = null;
19735
+ let toolText = "";
19563
19736
  for (const line of stdout.split("\n")) {
19564
19737
  const trimmed = line.trim();
19565
19738
  if (!trimmed.startsWith("{"))
@@ -19576,9 +19749,14 @@ var require_claude_code = __commonJS({
19576
19749
  continue;
19577
19750
  for (const block of content) {
19578
19751
  const b = block;
19579
- if (b?.type === "tool_use" && b.name === "Read" && typeof b.input?.file_path === "string") {
19580
- 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);
19581
19757
  }
19758
+ if (b.input && typeof b.input === "object")
19759
+ toolText += JSON.stringify(b.input);
19582
19760
  }
19583
19761
  } else if (event.type === "result") {
19584
19762
  if (typeof event.result === "string" && event.result.trim().length > 0) {
@@ -19601,7 +19779,7 @@ var require_claude_code = __commonJS({
19601
19779
  };
19602
19780
  }
19603
19781
  }
19604
- return { finalText, readPaths, usage, resultMeta, structuredOutput };
19782
+ return { finalText, readPaths, usage, resultMeta, structuredOutput, toolText };
19605
19783
  }
19606
19784
  var MAX_REPORTED_READS = 2e3;
19607
19785
  function relativeWorkspaceReads(readPaths, cwd) {
@@ -20864,9 +21042,15 @@ var require_shuttle = __commonJS({
20864
21042
  }
20865
21043
  await observerChain.notify({ kind: "executor.dispatch.started", jobId, attendanceId, executor: executor.capability.id, strategy }, onObserverError);
20866
21044
  const dispatchStartedAt = Date.now();
21045
+ const executionCtx = {
21046
+ reportProgress: (channel, frames) => {
21047
+ void jobsClient.postProgress(jobId, { channel, frames }).catch(() => {
21048
+ });
21049
+ }
21050
+ };
20867
21051
  let response;
20868
21052
  try {
20869
- response = await executor.execute(dispatch, signal);
21053
+ response = await executor.execute(dispatch, signal, executionCtx);
20870
21054
  } catch (err) {
20871
21055
  const reason = `executor failed: ${err instanceof Error ? err.message : String(err)}`;
20872
21056
  await observerChain.notify({ kind: "attendance.closed", jobId, attendanceId, status: "failed", reason, totalDurationMs: Date.now() - startedAt }, onObserverError);
@@ -21542,61 +21726,16 @@ var import_shuttle2 = __toESM(require_dist5());
21542
21726
  // src/brand.ts
21543
21727
  var import_path = require("path");
21544
21728
  var import_promises = require("fs/promises");
21545
- var import_readline = require("readline");
21546
21729
  var import_yaml = __toESM(require_dist());
21547
21730
  var import_shuttle = __toESM(require_dist5());
21548
-
21549
- // package.json
21550
- var package_default = {
21551
- name: "@whittlelabs/sifter",
21552
- version: "0.14.0",
21553
- description: "Whittle Sifter: paired AI reviewer for Whittle Sift job pools.",
21554
- bin: {
21555
- "whittle-sifter": "./dist/bin.js"
21556
- },
21557
- main: "dist/bin.js",
21558
- engines: {
21559
- node: ">=20"
21560
- },
21561
- scripts: {
21562
- dev: "tsx watch src/bin.ts",
21563
- build: "node scripts/bundle.mjs",
21564
- typecheck: "tsc --noEmit",
21565
- start: "node dist/bin.js",
21566
- lint: "eslint src/"
21567
- },
21568
- publishConfig: {
21569
- registry: "https://registry.npmjs.org/",
21570
- access: "public"
21571
- },
21572
- repository: {
21573
- type: "git",
21574
- url: "https://github.com/whittlelabs/whittlelabs.git",
21575
- directory: "apps/whittle-sifter"
21576
- },
21577
- author: "Whittle Labs",
21578
- license: "UNLICENSED",
21579
- private: false,
21580
- dependencies: {
21581
- "@whittlelabs/shuttle": "workspace:*",
21582
- yaml: "^2.7.1"
21583
- },
21584
- devDependencies: {
21585
- "@types/node": "^20.10.5",
21586
- esbuild: "^0.25.0",
21587
- tsx: "^4.21.0",
21588
- typescript: "^5.3.3"
21589
- }
21590
- };
21591
-
21592
- // src/brand.ts
21731
+ var buildVersion = true ? "0.16.0" : pkg.version;
21593
21732
  var sifterBrand = {
21594
21733
  product: {
21595
21734
  id: "sifter",
21596
21735
  title: "Whittle Sifter",
21597
21736
  cliBinary: "whittle-sifter",
21598
21737
  packageName: "@whittlelabs/sifter",
21599
- version: package_default.version,
21738
+ version: buildVersion,
21600
21739
  description: "Pairs with Whittle Sift to run AI code reviews on your hardware with your credentials."
21601
21740
  },
21602
21741
  paths: {
@@ -21646,12 +21785,11 @@ var sifterBrand = {
21646
21785
  // A re-pair must refresh the pairing-derived fields (Jobs URL, service
21647
21786
  // identity, pools) even when sifter.yaml already exists — otherwise a
21648
21787
  // stale file silently shadows the new pairing (exactly how a leftover
21649
- // `localhost:3005` and an old `cwd` once survived a fresh pair). So we
21650
- // merge rather than skip: rebuild the pairing fields from `pairing`, carry
21651
- // over the user's executor block (e.g. claude-code `cwd`) and polling
21652
- // unless a fresh flag overrides them, and log what happened. We only
21653
- // (re)scaffold an executor block which may prompt for `cwd` — when there
21654
- // isn't one to preserve.
21788
+ // `localhost:3005` once survived a fresh pair). So we merge rather than
21789
+ // skip: rebuild the pairing fields from `pairing`, carry over the user's
21790
+ // executor block (e.g. a claude-code `tmpDir`) and polling unless a fresh
21791
+ // flag overrides them, and log what happened. We only (re)scaffold an
21792
+ // executor block when there isn't one to preserve.
21655
21793
  run: async (ctx) => {
21656
21794
  const yamlPath = (0, import_path.join)(ctx.configDir, "sifter.yaml");
21657
21795
  const pairing = new import_shuttle.PairingConfigStore((0, import_path.join)(ctx.configDir, "config.json")).read();
@@ -21706,8 +21844,10 @@ var sifterBrand = {
21706
21844
  async function buildExecutorBlock(executor, ctx) {
21707
21845
  switch (executor) {
21708
21846
  case "claude-code": {
21709
- const cwd = ctx.setupOptions.cwd ?? await promptForCwd(`Where should Claude Code run? [${process.cwd()}]: `, process.cwd());
21710
- return { type: "claude-code", cwd };
21847
+ return {
21848
+ type: "claude-code",
21849
+ ...ctx.setupOptions.tmpDir ? { tmpDir: ctx.setupOptions.tmpDir } : {}
21850
+ };
21711
21851
  }
21712
21852
  case "anthropic-api": {
21713
21853
  return {
@@ -21719,19 +21859,6 @@ async function buildExecutorBlock(executor, ctx) {
21719
21859
  return { type: executor };
21720
21860
  }
21721
21861
  }
21722
- async function promptForCwd(prompt, fallback) {
21723
- if (!process.stdin.isTTY) return fallback;
21724
- const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
21725
- try {
21726
- const answer = await new Promise((resolve) => {
21727
- rl.question(prompt, (input) => resolve(input));
21728
- });
21729
- const trimmed = answer.trim();
21730
- return trimmed.length === 0 ? fallback : trimmed;
21731
- } finally {
21732
- rl.close();
21733
- }
21734
- }
21735
21862
 
21736
21863
  // src/bin.ts
21737
21864
  (0, import_shuttle2.createCli)(sifterBrand).parseAsync(process.argv).catch((err) => {
@@ -21753,7 +21880,7 @@ async function promptForCwd(prompt, fallback) {
21753
21880
  console.error("Keep refused its credentials \u2014 it was decommissioned from the");
21754
21881
  console.error("web app, or its secret was rotated by a newer pairing.");
21755
21882
  console.error("");
21756
- console.error(`Run \`${sifterBrand.product.command} init\` to pair again.`);
21883
+ console.error(`Run \`${sifterBrand.product.cliBinary} init\` to pair again.`);
21757
21884
  console.error("");
21758
21885
  process.exit(3);
21759
21886
  }