@whittlelabs/sifter 0.13.0 → 0.15.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 +64 -71
  2. package/bin.js.map +3 -3
  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
  }
@@ -18896,10 +18896,11 @@ var require_workspace = __commonJS({
18896
18896
  var path_1 = __importDefault(require("path"));
18897
18897
  var DEFAULT_GIT_TIMEOUT_MS = 12e4;
18898
18898
  var DEFAULT_GIT_PROTOCOLS = "https:ssh:git";
18899
- async function prepareWorkspace(repoDir, workspace, options = {}) {
18899
+ async function prepareWorkspace(workspace, options = {}) {
18900
+ const tmpBase = options.tmpDir ?? os_1.default.tmpdir();
18900
18901
  const resolved = readGitWorkspace(workspace);
18901
18902
  if ("warning" in resolved) {
18902
- return degradedWorkspace(resolved.warning);
18903
+ return degradedWorkspace(resolved.warning, tmpBase);
18903
18904
  }
18904
18905
  const gitWorkspace = resolved.git;
18905
18906
  const timeoutMs = options.commandTimeoutMs ?? DEFAULT_GIT_TIMEOUT_MS;
@@ -18907,7 +18908,6 @@ var require_workspace = __commonJS({
18907
18908
  const signal = options.signal;
18908
18909
  const authEnv = options.gitToken !== void 0 ? buildGitAuthEnv(gitWorkspace.remote, options.gitToken) : void 0;
18909
18910
  const git = (args, cwd) => runGit(args, cwd, { timeoutMs, signal, allowedProtocols, extraEnv: authEnv });
18910
- const gitCleanup = (args, cwd) => runGit(args, cwd, { timeoutMs, allowedProtocols });
18911
18911
  const hasCommit = (dir) => git(["cat-file", "-e", `${gitWorkspace.commit}^{commit}`], dir).then((r) => r.code === 0);
18912
18912
  const fetchCommit = async (dir, opts) => {
18913
18913
  if (await hasCommit(dir))
@@ -18915,9 +18915,6 @@ var require_workspace = __commonJS({
18915
18915
  const depth = opts.shallow ? ["--depth=1"] : [];
18916
18916
  const attempts = [];
18917
18917
  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
18918
  attempts.push({
18922
18919
  source: `${gitWorkspace.remote} ${ref}`,
18923
18920
  args: ["fetch", "--no-tags", ...depth, gitWorkspace.remote, ref]
@@ -18941,31 +18938,7 @@ var require_workspace = __commonJS({
18941
18938
  return failures2;
18942
18939
  };
18943
18940
  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-"));
18941
+ const tempRoot = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-ws-"));
18969
18942
  const checkoutDir = path_1.default.join(tempRoot, "checkout");
18970
18943
  const removeTempRoot = async () => {
18971
18944
  await fs_1.promises.rm(tempRoot, { recursive: true, force: true }).catch(() => {
@@ -18975,17 +18948,17 @@ var require_workspace = __commonJS({
18975
18948
  const init = await git(["init", "-q"], checkoutDir);
18976
18949
  if (init.code !== 0) {
18977
18950
  await removeTempRoot();
18978
- return degradedWorkspace(`git init failed: ${firstLine(init.stderr)}`);
18951
+ return degradedWorkspace(`git init failed: ${firstLine(init.stderr)}`, tmpBase);
18979
18952
  }
18980
- const failures = await fetchCommit(checkoutDir, { includeOrigin: false, shallow: true });
18953
+ const failures = await fetchCommit(checkoutDir, { shallow: true });
18981
18954
  if (!await hasCommit(checkoutDir)) {
18982
18955
  await removeTempRoot();
18983
- return degradedWorkspace(notMaterialized(failures));
18956
+ return degradedWorkspace(notMaterialized(failures), tmpBase);
18984
18957
  }
18985
18958
  const checkout = await git(["checkout", "--detach", gitWorkspace.commit], checkoutDir);
18986
18959
  if (checkout.code !== 0) {
18987
18960
  await removeTempRoot();
18988
- return degradedWorkspace(`git checkout failed: ${firstLine(checkout.stderr)}`);
18961
+ return degradedWorkspace(`git checkout failed: ${firstLine(checkout.stderr)}`, tmpBase);
18989
18962
  }
18990
18963
  return { cwd: checkoutDir, materialized: true, cleanup: removeTempRoot };
18991
18964
  }
@@ -19033,8 +19006,8 @@ var require_workspace = __commonJS({
19033
19006
  }
19034
19007
  return { git };
19035
19008
  }
19036
- async function degradedWorkspace(warning) {
19037
- const scratchDir = await fs_1.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "shuttle-ws-degraded-"));
19009
+ async function degradedWorkspace(warning, tmpBase = os_1.default.tmpdir()) {
19010
+ const scratchDir = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-ws-degraded-"));
19038
19011
  return {
19039
19012
  cwd: scratchDir,
19040
19013
  materialized: false,
@@ -19329,12 +19302,15 @@ var require_claude_code = __commonJS({
19329
19302
  configDir = path_1.default.join(jobScratch, "claude-config");
19330
19303
  await fs_1.promises.mkdir(configDir);
19331
19304
  }
19305
+ const tmpBase = this.config.tmpDir ? (0, apply_1.expandHome)(this.config.tmpDir) : os_1.default.tmpdir();
19332
19306
  let workspace = null;
19307
+ let promptOnlyScratch = null;
19333
19308
  let overlayResult = null;
19334
19309
  try {
19335
19310
  if (spec.workspace) {
19336
- workspace = await (0, workspace_1.prepareWorkspace)(this.config.cwd, spec.workspace, {
19311
+ workspace = await (0, workspace_1.prepareWorkspace)(spec.workspace, {
19337
19312
  signal,
19313
+ tmpDir: tmpBase,
19338
19314
  ...credentials?.githubToken ? { gitToken: credentials.githubToken } : {}
19339
19315
  });
19340
19316
  if (spec.workspace.overlay) {
@@ -19347,7 +19323,10 @@ var require_claude_code = __commonJS({
19347
19323
  const childEnv = buildChildEnv(credentials, configDir);
19348
19324
  const claudeHints = (0, loom_1.pickProviderHints)(spec, "claude-code");
19349
19325
  const hintedModel = typeof claudeHints.model === "string" && claudeHints.model.length > 0 ? claudeHints.model : void 0;
19350
- const runCwd = workspace?.cwd ?? this.config.cwd;
19326
+ if (!workspace) {
19327
+ promptOnlyScratch = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-cc-run-"));
19328
+ }
19329
+ const runCwd = workspace?.cwd ?? promptOnlyScratch;
19351
19330
  const { response, readPaths } = await this.runClaude(prompt, spec, runCwd, childEnv, signal, hintedModel);
19352
19331
  if (workspace) {
19353
19332
  response.outputs.push({
@@ -19372,6 +19351,9 @@ var require_claude_code = __commonJS({
19372
19351
  } finally {
19373
19352
  if (workspace)
19374
19353
  await workspace.cleanup();
19354
+ if (promptOnlyScratch)
19355
+ await fs_1.promises.rm(promptOnlyScratch, { recursive: true, force: true }).catch(() => {
19356
+ });
19375
19357
  if (jobScratch)
19376
19358
  await fs_1.promises.rm(jobScratch, { recursive: true, force: true }).catch(() => {
19377
19359
  });
@@ -19477,12 +19459,13 @@ var require_claude_code = __commonJS({
19477
19459
  }
19478
19460
  };
19479
19461
  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');
19462
+ const validated = { capabilityId };
19463
+ if (config.tmpDir !== void 0) {
19464
+ if (typeof config.tmpDir !== "string" || config.tmpDir.length === 0) {
19465
+ throw new Error('"tmpDir" must be a non-empty string');
19466
+ }
19467
+ validated.tmpDir = config.tmpDir;
19483
19468
  }
19484
- const cwd = (0, apply_1.expandHome)(cwdRaw);
19485
- const validated = { cwd, capabilityId };
19486
19469
  if (config.model !== void 0) {
19487
19470
  if (typeof config.model !== "string")
19488
19471
  throw new Error('"model" must be a string');
@@ -20747,6 +20730,7 @@ var require_shuttle = __commonJS({
20747
20730
  "use strict";
20748
20731
  Object.defineProperty(exports2, "__esModule", { value: true });
20749
20732
  exports2.Shuttle = void 0;
20733
+ exports2.mergeExecutorDefaults = mergeExecutorDefaults;
20750
20734
  var os_1 = require("os");
20751
20735
  var jobs_1 = require_dist2();
20752
20736
  var self_update_1 = require_self_update();
@@ -20806,7 +20790,7 @@ var require_shuttle = __commonJS({
20806
20790
  });
20807
20791
  const registry = new registry_1.ExecutorRegistry(this.brand.executorAllowlist);
20808
20792
  registerBuiltInExecutors(registry, this.brand.executorAllowlist);
20809
- const executors = registry.build(this.config.executors);
20793
+ const executors = registry.build(mergeExecutorDefaults(this.brand.executorDefaults, this.config.executors));
20810
20794
  const byStrategy = /* @__PURE__ */ new Map();
20811
20795
  for (const executor of executors) {
20812
20796
  for (const strategy of executor.strategies)
@@ -20965,6 +20949,15 @@ var require_shuttle = __commonJS({
20965
20949
  }
20966
20950
  };
20967
20951
  exports2.Shuttle = Shuttle;
20952
+ function mergeExecutorDefaults(defaults, userExecutors) {
20953
+ if (!defaults)
20954
+ return userExecutors;
20955
+ const merged = {};
20956
+ for (const [name, userConfig] of Object.entries(userExecutors)) {
20957
+ merged[name] = { ...defaults[name] ?? {}, ...userConfig };
20958
+ }
20959
+ return merged;
20960
+ }
20968
20961
  function registerBuiltInExecutors(registry, allowlist) {
20969
20962
  const all = [
20970
20963
  ["claude-code", claude_code_1.createClaudeCodeExecutor],
@@ -21532,14 +21525,13 @@ var import_shuttle2 = __toESM(require_dist5());
21532
21525
  // src/brand.ts
21533
21526
  var import_path = require("path");
21534
21527
  var import_promises = require("fs/promises");
21535
- var import_readline = require("readline");
21536
21528
  var import_yaml = __toESM(require_dist());
21537
21529
  var import_shuttle = __toESM(require_dist5());
21538
21530
 
21539
21531
  // package.json
21540
21532
  var package_default = {
21541
21533
  name: "@whittlelabs/sifter",
21542
- version: "0.13.0",
21534
+ version: "0.15.0",
21543
21535
  description: "Whittle Sifter: paired AI reviewer for Whittle Sift job pools.",
21544
21536
  bin: {
21545
21537
  "whittle-sifter": "./dist/bin.js"
@@ -21598,6 +21590,19 @@ var sifterBrand = {
21598
21590
  configSearchPaths: ["~/.sifter/sifter.yaml"]
21599
21591
  },
21600
21592
  executorAllowlist: ["claude-code", "anthropic-api", "http-api", "webhook", "custom-script"],
21593
+ // Overlay-fetch origins for the signed standards-content URLs each Sift
21594
+ // tier signs (SIFT_PUBLIC_API_URL). Without these, the executor's SSRF
21595
+ // guard silently skips the standards overlay and every review runs
21596
+ // standards-blind (2026-07-13). User config overrides per key.
21597
+ executorDefaults: {
21598
+ "claude-code": {
21599
+ overlayAllowedHosts: [
21600
+ "https://api.sift.whittlelabs.com",
21601
+ "https://api.sift.stage.whittlelabs.com",
21602
+ "http://localhost:3008"
21603
+ ]
21604
+ }
21605
+ },
21601
21606
  keepRegistration: {
21602
21607
  keepApiUrl: process.env.KEEP_API_URL ?? "https://keep.whittlelabs.com",
21603
21608
  jobsApiUrl: process.env.JOBS_API_URL ?? "https://jobs.whittlelabs.com",
@@ -21623,12 +21628,11 @@ var sifterBrand = {
21623
21628
  // A re-pair must refresh the pairing-derived fields (Jobs URL, service
21624
21629
  // identity, pools) even when sifter.yaml already exists — otherwise a
21625
21630
  // stale file silently shadows the new pairing (exactly how a leftover
21626
- // `localhost:3005` and an old `cwd` once survived a fresh pair). So we
21627
- // merge rather than skip: rebuild the pairing fields from `pairing`, carry
21628
- // over the user's executor block (e.g. claude-code `cwd`) and polling
21629
- // unless a fresh flag overrides them, and log what happened. We only
21630
- // (re)scaffold an executor block which may prompt for `cwd` — when there
21631
- // isn't one to preserve.
21631
+ // `localhost:3005` once survived a fresh pair). So we merge rather than
21632
+ // skip: rebuild the pairing fields from `pairing`, carry over the user's
21633
+ // executor block (e.g. a claude-code `tmpDir`) and polling unless a fresh
21634
+ // flag overrides them, and log what happened. We only (re)scaffold an
21635
+ // executor block when there isn't one to preserve.
21632
21636
  run: async (ctx) => {
21633
21637
  const yamlPath = (0, import_path.join)(ctx.configDir, "sifter.yaml");
21634
21638
  const pairing = new import_shuttle.PairingConfigStore((0, import_path.join)(ctx.configDir, "config.json")).read();
@@ -21683,8 +21687,10 @@ var sifterBrand = {
21683
21687
  async function buildExecutorBlock(executor, ctx) {
21684
21688
  switch (executor) {
21685
21689
  case "claude-code": {
21686
- const cwd = ctx.setupOptions.cwd ?? await promptForCwd(`Where should Claude Code run? [${process.cwd()}]: `, process.cwd());
21687
- return { type: "claude-code", cwd };
21690
+ return {
21691
+ type: "claude-code",
21692
+ ...ctx.setupOptions.tmpDir ? { tmpDir: ctx.setupOptions.tmpDir } : {}
21693
+ };
21688
21694
  }
21689
21695
  case "anthropic-api": {
21690
21696
  return {
@@ -21696,19 +21702,6 @@ async function buildExecutorBlock(executor, ctx) {
21696
21702
  return { type: executor };
21697
21703
  }
21698
21704
  }
21699
- async function promptForCwd(prompt, fallback) {
21700
- if (!process.stdin.isTTY) return fallback;
21701
- const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
21702
- try {
21703
- const answer = await new Promise((resolve) => {
21704
- rl.question(prompt, (input) => resolve(input));
21705
- });
21706
- const trimmed = answer.trim();
21707
- return trimmed.length === 0 ? fallback : trimmed;
21708
- } finally {
21709
- rl.close();
21710
- }
21711
- }
21712
21705
 
21713
21706
  // src/bin.ts
21714
21707
  (0, import_shuttle2.createCli)(sifterBrand).parseAsync(process.argv).catch((err) => {