@whittlelabs/sifter 0.14.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 +40 -70
  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');
@@ -21542,14 +21525,13 @@ var import_shuttle2 = __toESM(require_dist5());
21542
21525
  // src/brand.ts
21543
21526
  var import_path = require("path");
21544
21527
  var import_promises = require("fs/promises");
21545
- var import_readline = require("readline");
21546
21528
  var import_yaml = __toESM(require_dist());
21547
21529
  var import_shuttle = __toESM(require_dist5());
21548
21530
 
21549
21531
  // package.json
21550
21532
  var package_default = {
21551
21533
  name: "@whittlelabs/sifter",
21552
- version: "0.14.0",
21534
+ version: "0.15.0",
21553
21535
  description: "Whittle Sifter: paired AI reviewer for Whittle Sift job pools.",
21554
21536
  bin: {
21555
21537
  "whittle-sifter": "./dist/bin.js"
@@ -21646,12 +21628,11 @@ var sifterBrand = {
21646
21628
  // A re-pair must refresh the pairing-derived fields (Jobs URL, service
21647
21629
  // identity, pools) even when sifter.yaml already exists — otherwise a
21648
21630
  // 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.
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.
21655
21636
  run: async (ctx) => {
21656
21637
  const yamlPath = (0, import_path.join)(ctx.configDir, "sifter.yaml");
21657
21638
  const pairing = new import_shuttle.PairingConfigStore((0, import_path.join)(ctx.configDir, "config.json")).read();
@@ -21706,8 +21687,10 @@ var sifterBrand = {
21706
21687
  async function buildExecutorBlock(executor, ctx) {
21707
21688
  switch (executor) {
21708
21689
  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 };
21690
+ return {
21691
+ type: "claude-code",
21692
+ ...ctx.setupOptions.tmpDir ? { tmpDir: ctx.setupOptions.tmpDir } : {}
21693
+ };
21711
21694
  }
21712
21695
  case "anthropic-api": {
21713
21696
  return {
@@ -21719,19 +21702,6 @@ async function buildExecutorBlock(executor, ctx) {
21719
21702
  return { type: executor };
21720
21703
  }
21721
21704
  }
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
21705
 
21736
21706
  // src/bin.ts
21737
21707
  (0, import_shuttle2.createCli)(sifterBrand).parseAsync(process.argv).catch((err) => {