@whittlelabs/sifter 0.29.0 → 0.30.1

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 (4) hide show
  1. package/README.md +25 -5
  2. package/bin.js +348 -29
  3. package/bin.js.map +4 -4
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -21,15 +21,35 @@ Visit Sift's **Settings → Pair a Sifter**. Sift will display a pairing code an
21
21
  whittle-sifter init --pairing-code=ABCD-1234
22
22
  ```
23
23
 
24
- The Sifter defaults to production (`https://keep.whittlelabs.com`). To pair against stage, test, or any other environment, pass `--keep-url`:
24
+ The Sifter defaults to production. To pair against another Whittle Labs environment, name it with `--profile`: the name selects the backend as well as the directory the pairing lands in.
25
25
 
26
26
  ```bash
27
- whittle-sifter init \
28
- --keep-url=https://keep.stage.whittlelabs.com \
29
- --pairing-code=ABCD-1234
27
+ whittle-sifter init --profile stage # keep.stage / jobs.stage
28
+ whittle-sifter init --profile test # keep.test / jobs.test
29
+ whittle-sifter init --profile studio # a dev stack on the Studio
30
30
  ```
31
31
 
32
- Pairing writes credentials to `~/.sifter/config.json` (mode 0600).
32
+ The known names are `prod`, `stage`, `test` and `studio`. For any other backend, keep the profile as a label and pass the URLs yourself; a flag always beats the environment it would otherwise have resolved:
33
+
34
+ ```bash
35
+ whittle-sifter init --profile my-box \
36
+ --keep-url=http://box.local:3007 \
37
+ --jobs-url=http://box.local:3005
38
+ ```
39
+
40
+ Whenever a profile or a URL flag is in play, `init` prints what it resolved before it pairs, so a host that has moved is something you see rather than something you debug:
41
+
42
+ ```
43
+ Pairing environment
44
+ Profile: stage
45
+ Environment: stage (named by this build)
46
+ Keep: https://keep.stage.whittlelabs.com from the "stage" environment
47
+ Jobs: https://jobs.stage.whittlelabs.com from the "stage" environment
48
+ ```
49
+
50
+ Once the backend answers, one more line names the page it sent you to, which is its decision and not the CLI's. Add `--json` to get those two facts as JSON objects, one per line.
51
+
52
+ Pairing writes credentials to `~/.sifter/<profile>/config.json` (mode 0600), or `~/.sifter/config.json` for the default profile.
33
53
 
34
54
  ## Run
35
55
 
package/bin.js CHANGED
@@ -10008,6 +10008,8 @@ var require_claude_code = __commonJS({
10008
10008
  exports2.createClaudeCodeExecutor = createClaudeCodeExecutor;
10009
10009
  exports2.parseStreamJson = parseStreamJson;
10010
10010
  exports2.relativeWorkspaceReads = relativeWorkspaceReads;
10011
+ exports2.globToRegExp = globToRegExp;
10012
+ exports2.creditOverlayReads = creditOverlayReads;
10011
10013
  exports2.extractJsonObject = extractJsonObject;
10012
10014
  var child_process_1 = require("child_process");
10013
10015
  var fs_1 = require("fs");
@@ -10092,12 +10094,11 @@ var require_claude_code = __commonJS({
10092
10094
  promptOnlyScratch = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-cc-run-"));
10093
10095
  }
10094
10096
  const runCwd = workspace?.cwd ?? promptOnlyScratch;
10095
- const { response, readPaths, toolText } = await this.runClaude(prompt, spec, runCwd, childEnv, signal, hintedModel, reporter);
10097
+ const { response, readPaths, toolInputs } = await this.runClaude(prompt, spec, runCwd, childEnv, signal, hintedModel, reporter, void 0, dispatch.job.id);
10096
10098
  if (workspace) {
10097
10099
  const reads = [...readPaths];
10098
- for (const rel of workspace.overlay?.writtenPaths ?? []) {
10099
- if (toolText.includes(rel))
10100
- reads.push(path_1.default.join(runCwd, rel));
10100
+ for (const rel of creditOverlayReads(toolInputs, workspace.overlay?.writtenPaths ?? [])) {
10101
+ reads.push(path_1.default.join(runCwd, rel));
10101
10102
  }
10102
10103
  response.outputs.push({
10103
10104
  label: "workspace_reads",
@@ -10155,14 +10156,14 @@ var require_claude_code = __commonJS({
10155
10156
  outputLabel: handle.outputLabel,
10156
10157
  outputSchema: handle.outputSchema
10157
10158
  };
10158
- const { response } = await this.runClaude(followUp, spec, scratch, handle.childEnv, signal, handle.model, null, handle.sessionId);
10159
+ const { response } = await this.runClaude(followUp, spec, scratch, handle.childEnv, signal, handle.model, null, handle.sessionId, `${handle.sessionId}-repair`);
10159
10160
  return response;
10160
10161
  } finally {
10161
10162
  await fs_1.promises.rm(scratch, { recursive: true, force: true }).catch(() => {
10162
10163
  });
10163
10164
  }
10164
10165
  }
10165
- runClaude(prompt, spec, cwd, childEnv, signal, modelOverride, reporter, resumeSessionId) {
10166
+ runClaude(prompt, spec, cwd, childEnv, signal, modelOverride, reporter, resumeSessionId, transcriptName) {
10166
10167
  const outputLabel = spec.outputLabel;
10167
10168
  const model = modelOverride ?? this.config.model;
10168
10169
  return new Promise((resolve, reject) => {
@@ -10251,6 +10252,9 @@ var require_claude_code = __commonJS({
10251
10252
  tap.end();
10252
10253
  }
10253
10254
  const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
10255
+ if (this.config.keepTranscripts && transcriptName) {
10256
+ void keepTranscript((0, apply_1.expandHome)(this.config.keepTranscripts), transcriptName, stdout);
10257
+ }
10254
10258
  if (code === 0) {
10255
10259
  try {
10256
10260
  const stream = parseStreamJson(stdout);
@@ -10286,7 +10290,7 @@ var require_claude_code = __commonJS({
10286
10290
  } : {}
10287
10291
  },
10288
10292
  readPaths: stream.readPaths,
10289
- toolText: stream.toolText
10293
+ toolInputs: stream.toolInputs
10290
10294
  }));
10291
10295
  } catch (err) {
10292
10296
  settle(() => reject(err instanceof Error ? err : new Error(String(err))));
@@ -10334,6 +10338,12 @@ var require_claude_code = __commonJS({
10334
10338
  }
10335
10339
  validated.timeout = config.timeout;
10336
10340
  }
10341
+ if (config.keepTranscripts !== void 0) {
10342
+ if (typeof config.keepTranscripts !== "string" || config.keepTranscripts.length === 0) {
10343
+ throw new Error('"keepTranscripts" must be a non-empty directory path');
10344
+ }
10345
+ validated.keepTranscripts = config.keepTranscripts;
10346
+ }
10337
10347
  if (config.overlayAllowedHosts !== void 0) {
10338
10348
  if (!Array.isArray(config.overlayAllowedHosts) || !config.overlayAllowedHosts.every((h) => typeof h === "string")) {
10339
10349
  throw new Error('"overlayAllowedHosts" must be an array of strings');
@@ -10358,7 +10368,7 @@ var require_claude_code = __commonJS({
10358
10368
  let resultMeta = null;
10359
10369
  let structuredOutput = null;
10360
10370
  let sessionId = null;
10361
- let toolText = "";
10371
+ const toolInputs = [];
10362
10372
  for (const line of stdout.split("\n")) {
10363
10373
  const trimmed = line.trim();
10364
10374
  if (!trimmed.startsWith("{"))
@@ -10384,7 +10394,7 @@ var require_claude_code = __commonJS({
10384
10394
  readPaths.push(fp);
10385
10395
  }
10386
10396
  if (b.input && typeof b.input === "object")
10387
- toolText += JSON.stringify(b.input);
10397
+ toolInputs.push(JSON.stringify(b.input));
10388
10398
  }
10389
10399
  } else if (event.type === "result") {
10390
10400
  if (typeof event.result === "string" && event.result.trim().length > 0) {
@@ -10412,24 +10422,131 @@ var require_claude_code = __commonJS({
10412
10422
  };
10413
10423
  }
10414
10424
  }
10415
- return { finalText, readPaths, usage, resultMeta, structuredOutput, sessionId, toolText };
10425
+ return { finalText, readPaths, usage, resultMeta, structuredOutput, sessionId, toolInputs };
10416
10426
  }
10417
10427
  var MAX_REPORTED_READS = 2e3;
10418
10428
  function relativeWorkspaceReads(readPaths, cwd) {
10419
10429
  const root = path_1.default.resolve(cwd);
10430
+ const roots = [root];
10431
+ try {
10432
+ const real = (0, fs_1.realpathSync)(root);
10433
+ if (real !== root)
10434
+ roots.push(real);
10435
+ } catch {
10436
+ }
10420
10437
  const out = /* @__PURE__ */ new Set();
10421
10438
  for (const p of readPaths) {
10422
10439
  const resolved = path_1.default.resolve(root, p);
10423
- if (resolved === root)
10424
- continue;
10425
- if (!resolved.startsWith(root + path_1.default.sep))
10426
- continue;
10427
- out.add(resolved.slice(root.length + 1));
10440
+ for (const r of roots) {
10441
+ if (resolved === r)
10442
+ break;
10443
+ if (!resolved.startsWith(r + path_1.default.sep))
10444
+ continue;
10445
+ out.add(resolved.slice(r.length + 1));
10446
+ break;
10447
+ }
10428
10448
  if (out.size >= MAX_REPORTED_READS)
10429
10449
  break;
10430
10450
  }
10431
10451
  return [...out].sort();
10432
10452
  }
10453
+ var GLOB_CHARS = /[*?[{]/;
10454
+ function escapeRegExp(s) {
10455
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10456
+ }
10457
+ function globToRegExp(glob) {
10458
+ let re = "^";
10459
+ for (let i = 0; i < glob.length; i++) {
10460
+ const ch = glob[i];
10461
+ if (ch === "*") {
10462
+ if (glob[i + 1] === "*") {
10463
+ if (glob[i + 2] === "/") {
10464
+ re += "(?:.*/)?";
10465
+ i += 2;
10466
+ } else {
10467
+ re += ".*";
10468
+ i += 1;
10469
+ }
10470
+ } else {
10471
+ re += "[^/]*";
10472
+ }
10473
+ } else if (ch === "?") {
10474
+ re += "[^/]";
10475
+ } else if (ch === "[") {
10476
+ const end = glob.indexOf("]", i + 1);
10477
+ if (end === -1)
10478
+ return null;
10479
+ let cls = glob.slice(i + 1, end);
10480
+ if (cls.startsWith("!"))
10481
+ cls = `^${cls.slice(1)}`;
10482
+ re += `[${cls.replace(/\\/g, "\\\\")}]`;
10483
+ i = end;
10484
+ } else if (ch === "{") {
10485
+ const end = glob.indexOf("}", i + 1);
10486
+ if (end === -1)
10487
+ return null;
10488
+ re += `(?:${glob.slice(i + 1, end).split(",").map(escapeRegExp).join("|")})`;
10489
+ i = end;
10490
+ } else {
10491
+ re += escapeRegExp(ch);
10492
+ }
10493
+ }
10494
+ return new RegExp(`${re}$`);
10495
+ }
10496
+ function creditOverlayReads(toolInputs, writtenPaths) {
10497
+ if (writtenPaths.length === 0 || toolInputs.length === 0)
10498
+ return [];
10499
+ const dirs = /* @__PURE__ */ new Set();
10500
+ for (const rel of writtenPaths) {
10501
+ const parts = rel.split("/");
10502
+ for (let i = 1; i < parts.length; i++)
10503
+ dirs.add(`${parts.slice(0, i).join("/")}/`);
10504
+ }
10505
+ const dirAlt = [...dirs].map(escapeRegExp).join("|");
10506
+ const wordRe = dirs.size > 0 ? new RegExp(`(?:${dirAlt})[^\\s"'\`;|&<>(),\\\\]*`, "g") : null;
10507
+ const findRe = dirs.size > 0 ? new RegExp(`\\bfind\\s+(?:\\./)?(${[...dirs].map((d) => escapeRegExp(d.slice(0, -1))).join("|")})/?(?=\\s|$)`, "g") : null;
10508
+ const readerRe = /(?:^|[\s"])(?:-exec|-execdir|xargs)\b[^;|&]*\b(?:cat|head|tail|sed|awk|less|more|bat)\b/;
10509
+ const credited = /* @__PURE__ */ new Set();
10510
+ for (const input of toolInputs) {
10511
+ for (const rel of writtenPaths) {
10512
+ if (input.includes(rel))
10513
+ credited.add(rel);
10514
+ }
10515
+ if (!wordRe)
10516
+ continue;
10517
+ for (const match of input.matchAll(wordRe)) {
10518
+ const word = match[0];
10519
+ if (!GLOB_CHARS.test(word))
10520
+ continue;
10521
+ const re = globToRegExp(word);
10522
+ if (!re)
10523
+ continue;
10524
+ for (const rel of writtenPaths) {
10525
+ if (re.test(rel))
10526
+ credited.add(rel);
10527
+ }
10528
+ }
10529
+ if (findRe && readerRe.test(input)) {
10530
+ for (const match of input.matchAll(findRe)) {
10531
+ const dir = `${match[1]}/`;
10532
+ for (const rel of writtenPaths) {
10533
+ if (rel.startsWith(dir))
10534
+ credited.add(rel);
10535
+ }
10536
+ }
10537
+ }
10538
+ }
10539
+ return [...credited].sort();
10540
+ }
10541
+ async function keepTranscript(dir, name, stdout) {
10542
+ try {
10543
+ await fs_1.promises.mkdir(dir, { recursive: true });
10544
+ const safe = name.replace(/[^A-Za-z0-9._-]/g, "_");
10545
+ await fs_1.promises.writeFile(path_1.default.join(dir, `${safe}.jsonl`), stdout);
10546
+ } catch (err) {
10547
+ console.warn(`[shuttle] could not keep the transcript for ${name} under ${dir}: ${err instanceof Error ? err.message : String(err)}`);
10548
+ }
10549
+ }
10433
10550
  function extractJsonObject(s) {
10434
10551
  const exact = tryParseObject(s);
10435
10552
  if (exact)
@@ -11604,6 +11721,120 @@ var require_version_check = __commonJS({
11604
11721
  }
11605
11722
  });
11606
11723
 
11724
+ // ../../packages/shuttle/dist/branding/environment.js
11725
+ var require_environment = __commonJS({
11726
+ "../../packages/shuttle/dist/branding/environment.js"(exports2) {
11727
+ "use strict";
11728
+ Object.defineProperty(exports2, "__esModule", { value: true });
11729
+ exports2.resolveEnvironment = resolveEnvironment;
11730
+ exports2.applyEnvironment = applyEnvironment;
11731
+ exports2.isWorthReporting = isWorthReporting;
11732
+ exports2.renderEnvironmentReport = renderEnvironmentReport;
11733
+ exports2.renderVerificationLine = renderVerificationLine;
11734
+ exports2.environmentReportJson = environmentReportJson;
11735
+ exports2.verificationJson = verificationJson;
11736
+ exports2.hostOf = hostOf;
11737
+ function resolveEnvironment(brand, profile, overrides = {}) {
11738
+ const table = brand.environments ?? {};
11739
+ const known = Object.keys(table).sort();
11740
+ const matched = Object.prototype.hasOwnProperty.call(table, profile) ? profile : null;
11741
+ const entry = matched === null ? void 0 : table[matched];
11742
+ return {
11743
+ profile,
11744
+ environment: matched,
11745
+ known,
11746
+ keep: pick(overrides.keepUrl, entry?.keepApiUrl, brand.keepRegistration.keepApiUrl),
11747
+ jobs: pick(overrides.jobsUrl, entry?.jobsApiUrl, brand.keepRegistration.jobsApiUrl)
11748
+ };
11749
+ }
11750
+ function pick(flag, fromEnvironment, brandDefault) {
11751
+ if (flag)
11752
+ return { url: flag, source: "flag" };
11753
+ if (fromEnvironment)
11754
+ return { url: fromEnvironment, source: "environment" };
11755
+ return { url: brandDefault, source: "brand-default" };
11756
+ }
11757
+ function applyEnvironment(brand, resolved) {
11758
+ const reg = brand.keepRegistration;
11759
+ if (resolved.keep.url === reg.keepApiUrl && resolved.jobs.url === reg.jobsApiUrl) {
11760
+ return brand;
11761
+ }
11762
+ return {
11763
+ ...brand,
11764
+ keepRegistration: {
11765
+ ...reg,
11766
+ keepApiUrl: resolved.keep.url,
11767
+ jobsApiUrl: resolved.jobs.url
11768
+ }
11769
+ };
11770
+ }
11771
+ function isWorthReporting(resolved) {
11772
+ return resolved.environment !== null || resolved.keep.source === "flag" || resolved.jobs.source === "flag";
11773
+ }
11774
+ function renderEnvironmentReport(resolved) {
11775
+ return [
11776
+ "",
11777
+ "Pairing environment",
11778
+ field("Profile", resolved.profile),
11779
+ field("Environment", describeEnvironment(resolved)),
11780
+ field("Keep", `${resolved.keep.url} ${describeSource(resolved, resolved.keep)}`),
11781
+ field("Jobs", `${resolved.jobs.url} ${describeSource(resolved, resolved.jobs)}`),
11782
+ ""
11783
+ ];
11784
+ }
11785
+ function renderVerificationLine(verificationUri) {
11786
+ return field("Verify at", `${hostOf(verificationUri)} as Keep reported it`);
11787
+ }
11788
+ function environmentReportJson(resolved) {
11789
+ return JSON.stringify({
11790
+ kind: "pairing-environment",
11791
+ profile: resolved.profile,
11792
+ environment: resolved.environment,
11793
+ known: resolved.known,
11794
+ keep: resolved.keep,
11795
+ jobs: resolved.jobs
11796
+ });
11797
+ }
11798
+ function verificationJson(verificationUri) {
11799
+ return JSON.stringify({
11800
+ kind: "pairing-verification",
11801
+ verificationUri,
11802
+ host: hostOf(verificationUri),
11803
+ source: "keep"
11804
+ });
11805
+ }
11806
+ function field(label, value) {
11807
+ return ` ${`${label}:`.padEnd(13)}${value}`;
11808
+ }
11809
+ function describeEnvironment(resolved) {
11810
+ if (resolved.environment !== null) {
11811
+ return `${resolved.environment} (named by this build)`;
11812
+ }
11813
+ if (resolved.known.length === 0) {
11814
+ return "none (this build declares no environments)";
11815
+ }
11816
+ return `none (this build knows ${resolved.known.join(", ")})`;
11817
+ }
11818
+ function describeSource(resolved, url) {
11819
+ switch (url.source) {
11820
+ case "flag":
11821
+ return "from the flag you passed";
11822
+ case "environment":
11823
+ return `from the "${resolved.environment ?? ""}" environment`;
11824
+ default:
11825
+ return "from the brand default";
11826
+ }
11827
+ }
11828
+ function hostOf(url) {
11829
+ try {
11830
+ return new URL(url).host;
11831
+ } catch {
11832
+ return url;
11833
+ }
11834
+ }
11835
+ }
11836
+ });
11837
+
11607
11838
  // ../../packages/shuttle/dist/preflight/checks.js
11608
11839
  var require_checks = __commonJS({
11609
11840
  "../../packages/shuttle/dist/preflight/checks.js"(exports2) {
@@ -11619,6 +11850,7 @@ var require_checks = __commonJS({
11619
11850
  var loom_1 = require_dist();
11620
11851
  var version_check_1 = require_version_check();
11621
11852
  var apply_1 = require_apply();
11853
+ var environment_1 = require_environment();
11622
11854
  var NODE_FLOOR = "20.0.0";
11623
11855
  var DISK_FLOOR_BYTES = 1024 * 1024 * 1024;
11624
11856
  var WORKSPACE_EXECUTOR = "claude-code";
@@ -11745,7 +11977,7 @@ var require_checks = __commonJS({
11745
11977
  if (strays.length > 0) {
11746
11978
  return fail("pairing-environment", `the config claims from ${strays.join(", ")}, which this pairing is not rostered into`, `run \`${subject.brand.product.cliBinary} init\` to rewrite the config from this pairing`);
11747
11979
  }
11748
- return pass("pairing-environment", `config and pairing both use ${pairing.jobsApiUrl}`);
11980
+ return pass("pairing-environment", `paired with Keep at ${(0, environment_1.hostOf)(pairing.keepApiUrl)}, claiming from Jobs at ${(0, environment_1.hostOf)(pairing.jobsApiUrl)}`);
11749
11981
  });
11750
11982
  }
11751
11983
  };
@@ -22369,6 +22601,7 @@ var require_device_code = __commonJS({
22369
22601
  throw err;
22370
22602
  throw new Error(webFirstRemedy(opts.brand, err));
22371
22603
  }
22604
+ opts.onVerificationUri?.(device.verificationUri);
22372
22605
  const invitation = (0, terminal_1.renderPairingInvitation)({
22373
22606
  header: opts.brand.strings?.pairingHeader ?? `Pair your ${opts.brand.product.title}`,
22374
22607
  verificationUri: device.verificationUri,
@@ -22712,24 +22945,31 @@ var require_init = __commonJS({
22712
22945
  var fsp = __importStar(require("fs/promises"));
22713
22946
  var commander_1 = require_commander();
22714
22947
  var apply_1 = require_apply();
22948
+ var environment_1 = require_environment();
22715
22949
  var device_code_1 = require_device_code();
22716
22950
  var store_1 = require_store();
22717
22951
  var spend_store_1 = require_spend_store();
22718
22952
  var typed_exit_1 = require_typed_exit();
22719
22953
  var preflight_1 = require_preflight();
22720
22954
  function buildInitCommand(brand) {
22721
- 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) => {
22955
+ 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, beating both the profile's environment and the brand default").option("--jobs-url <url>", "Override the Jobs API URL, beating both the profile's environment and the brand default").option("--json", "Print the resolved pairing environment as JSON, one object per line; the rest of the output is unchanged").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>", profileFlagDescription(brand)).action(async (options) => {
22722
22956
  try {
22723
22957
  const profile = (0, apply_1.resolveProfile)(brand, options.profile);
22724
22958
  const setupOptions = parseSetupOptions(brand, options);
22725
- const effectiveBrand = options.keepUrl || options.jobsUrl ? {
22726
- ...brand,
22727
- keepRegistration: {
22728
- ...brand.keepRegistration,
22729
- ...options.keepUrl ? { keepApiUrl: options.keepUrl } : {},
22730
- ...options.jobsUrl ? { jobsApiUrl: options.jobsUrl } : {}
22731
- }
22732
- } : brand;
22959
+ const asJson = options.json === true;
22960
+ const environment = (0, environment_1.resolveEnvironment)(brand, profile, {
22961
+ keepUrl: options.keepUrl,
22962
+ jobsUrl: options.jobsUrl
22963
+ });
22964
+ const effectiveBrand = (0, environment_1.applyEnvironment)(brand, environment);
22965
+ const report = (0, environment_1.isWorthReporting)(environment);
22966
+ if (report) {
22967
+ if (asJson)
22968
+ console.log((0, environment_1.environmentReportJson)(environment));
22969
+ else
22970
+ for (const line of (0, environment_1.renderEnvironmentReport)(environment))
22971
+ console.log(line);
22972
+ }
22733
22973
  const paths = (0, apply_1.resolvePaths)(effectiveBrand, profile);
22734
22974
  await fsp.mkdir(paths.configDir, { recursive: true });
22735
22975
  for (const step of effectiveBrand.setupSteps ?? []) {
@@ -22759,7 +22999,14 @@ var require_init = __commonJS({
22759
22999
  }
22760
23000
  const pairing = await (0, device_code_1.pair)({
22761
23001
  brand: effectiveBrand,
22762
- pairingCode: options.pairingCode
23002
+ pairingCode: options.pairingCode,
23003
+ // Keep's answer is the half no local table can predict: it
23004
+ // builds `verification_uri` from its OWN configuration, so a
23005
+ // tier pointed at a page nobody serves looks exactly like a
23006
+ // healthy one until an operator opens the link.
23007
+ ...report ? {
23008
+ onVerificationUri: (uri) => console.log(asJson ? (0, environment_1.verificationJson)(uri) : (0, environment_1.renderVerificationLine)(uri))
23009
+ } : {}
22763
23010
  });
22764
23011
  const store = new store_1.PairingConfigStore(paths.configFile);
22765
23012
  await store.write(pairing);
@@ -22799,6 +23046,13 @@ var require_init = __commonJS({
22799
23046
  }
22800
23047
  });
22801
23048
  }
23049
+ function profileFlagDescription(brand) {
23050
+ const known = Object.keys(brand.environments ?? {}).sort();
23051
+ const base = 'Config profile to pair into (isolates this pairing under its own dir; default "default")';
23052
+ if (known.length === 0)
23053
+ return base;
23054
+ return `${base}. These names also select the backend: ${known.join(", ")}`;
23055
+ }
22802
23056
  function parseSetupOptions(brand, raw) {
22803
23057
  const out = {};
22804
23058
  if (raw.executor !== void 0) {
@@ -25096,7 +25350,8 @@ var require_dist5 = __commonJS({
25096
25350
  "../../packages/shuttle/dist/index.js"(exports2) {
25097
25351
  "use strict";
25098
25352
  Object.defineProperty(exports2, "__esModule", { value: true });
25099
- exports2.revokePairing = exports2.listPairings = exports2.PairingConfigStore = exports2.pair = exports2.DEFAULT_REFUSAL_MESSAGE = exports2.DEFAULT_MONTHLY_PERIOD = exports2.DEFAULT_DAILY_PERIOD = exports2.SUBSTRATE_BRAND = exports2.renderTemplate = exports2.expandHome = exports2.resolvePaths = exports2.SpendStore = exports2.AuditLogStore = exports2.createSpendTrackerObserver = exports2.createAuditLogObserver = exports2.createLoggerObserver = exports2.SpendCapExceeded = exports2.ObserverChain = exports2.createCustomScriptExecutor = exports2.createWebhookExecutor = exports2.createHttpApiExecutor = exports2.createClaudeCodeExecutor = exports2.ExecutorRegistry = exports2.AgentCredentialRevokedError = exports2.runPreflight = exports2.renderLines = exports2.renderJson = exports2.printReport = exports2.hasFailure = exports2.createProbes = exports2.checksFor = exports2.PREFLIGHT_FLOORS = exports2.PREFLIGHT_EXIT_CODE = exports2.CHECKS = exports2.readResponseLatestVersion = exports2.checkResponseMinVersion = exports2.compareSemver = exports2.UpgradeRequiredError = exports2.LATEST_VERSION_HEADER = exports2.MIN_VERSION_HEADER = exports2.loadConfig = exports2.Shuttle = exports2.createCli = void 0;
25353
+ exports2.listPairings = exports2.PairingConfigStore = exports2.pair = exports2.DEFAULT_REFUSAL_MESSAGE = exports2.DEFAULT_MONTHLY_PERIOD = exports2.DEFAULT_DAILY_PERIOD = exports2.SUBSTRATE_BRAND = exports2.verificationJson = exports2.resolveEnvironment = exports2.renderVerificationLine = exports2.renderEnvironmentReport = exports2.isWorthReporting = exports2.hostOf = exports2.environmentReportJson = exports2.applyEnvironment = exports2.renderTemplate = exports2.expandHome = exports2.resolvePaths = exports2.SpendStore = exports2.AuditLogStore = exports2.createSpendTrackerObserver = exports2.createAuditLogObserver = exports2.createLoggerObserver = exports2.SpendCapExceeded = exports2.ObserverChain = exports2.createCustomScriptExecutor = exports2.createWebhookExecutor = exports2.createHttpApiExecutor = exports2.createClaudeCodeExecutor = exports2.ExecutorRegistry = exports2.AgentCredentialRevokedError = exports2.runPreflight = exports2.renderLines = exports2.renderJson = exports2.printReport = exports2.hasFailure = exports2.createProbes = exports2.checksFor = exports2.PREFLIGHT_FLOORS = exports2.PREFLIGHT_EXIT_CODE = exports2.CHECKS = exports2.readResponseLatestVersion = exports2.checkResponseMinVersion = exports2.compareSemver = exports2.UpgradeRequiredError = exports2.LATEST_VERSION_HEADER = exports2.MIN_VERSION_HEADER = exports2.loadConfig = exports2.Shuttle = exports2.createCli = void 0;
25354
+ exports2.revokePairing = void 0;
25100
25355
  var cli_1 = require_cli();
25101
25356
  Object.defineProperty(exports2, "createCli", { enumerable: true, get: function() {
25102
25357
  return cli_1.createCli;
@@ -25220,6 +25475,31 @@ var require_dist5 = __commonJS({
25220
25475
  Object.defineProperty(exports2, "renderTemplate", { enumerable: true, get: function() {
25221
25476
  return apply_1.renderTemplate;
25222
25477
  } });
25478
+ var environment_1 = require_environment();
25479
+ Object.defineProperty(exports2, "applyEnvironment", { enumerable: true, get: function() {
25480
+ return environment_1.applyEnvironment;
25481
+ } });
25482
+ Object.defineProperty(exports2, "environmentReportJson", { enumerable: true, get: function() {
25483
+ return environment_1.environmentReportJson;
25484
+ } });
25485
+ Object.defineProperty(exports2, "hostOf", { enumerable: true, get: function() {
25486
+ return environment_1.hostOf;
25487
+ } });
25488
+ Object.defineProperty(exports2, "isWorthReporting", { enumerable: true, get: function() {
25489
+ return environment_1.isWorthReporting;
25490
+ } });
25491
+ Object.defineProperty(exports2, "renderEnvironmentReport", { enumerable: true, get: function() {
25492
+ return environment_1.renderEnvironmentReport;
25493
+ } });
25494
+ Object.defineProperty(exports2, "renderVerificationLine", { enumerable: true, get: function() {
25495
+ return environment_1.renderVerificationLine;
25496
+ } });
25497
+ Object.defineProperty(exports2, "resolveEnvironment", { enumerable: true, get: function() {
25498
+ return environment_1.resolveEnvironment;
25499
+ } });
25500
+ Object.defineProperty(exports2, "verificationJson", { enumerable: true, get: function() {
25501
+ return environment_1.verificationJson;
25502
+ } });
25223
25503
  var defaults_1 = require_defaults2();
25224
25504
  Object.defineProperty(exports2, "SUBSTRATE_BRAND", { enumerable: true, get: function() {
25225
25505
  return defaults_1.SUBSTRATE_BRAND;
@@ -25259,7 +25539,7 @@ var import_path = require("path");
25259
25539
  var import_promises = require("fs/promises");
25260
25540
  var import_yaml = __toESM(require_dist4());
25261
25541
  var import_shuttle = __toESM(require_dist5());
25262
- var buildVersion = true ? "0.29.0" : pkg.version;
25542
+ var buildVersion = true ? "0.30.1" : pkg.version;
25263
25543
  var sifterBrand = {
25264
25544
  product: {
25265
25545
  id: "sifter",
@@ -25304,7 +25584,46 @@ var sifterBrand = {
25304
25584
  "https://api.sift.whittlelabs.com",
25305
25585
  "https://api.sift.stage.whittlelabs.com",
25306
25586
  "http://localhost:3008"
25307
- ]
25587
+ ],
25588
+ // Transcript retention is a diagnostic switch a person flips for one
25589
+ // session, so it rides the environment rather than a file they would
25590
+ // have to remember to unset. The shared package reads no env of its
25591
+ // own (BE-005); this brand is where the variable becomes config.
25592
+ ...process.env.SHUTTLE_KEEP_TRANSCRIPTS ? { keepTranscripts: process.env.SHUTTLE_KEEP_TRANSCRIPTS } : {}
25593
+ }
25594
+ },
25595
+ // The environments Whittle Labs operates, keyed by the profile name that
25596
+ // selects one. `init --profile stage` resolves both URLs from here, so a
25597
+ // non-prod pairing is one flag instead of three and there is no host to
25598
+ // mistype. `--keep-url` / `--jobs-url` still win, which is what keeps a
25599
+ // backend Whittle Labs does not run reachable.
25600
+ //
25601
+ // `prod` is stated even though it duplicates the defaults below: a person
25602
+ // reading `--help` should see the whole set, and a profile literally named
25603
+ // `prod` should resolve rather than fall through to a default that only
25604
+ // happens to agree. The DEFAULT profile ("default") is deliberately not a
25605
+ // key — it is not an environment name, and it keeps its historic meaning
25606
+ // of "the brand's own defaults", which are production's.
25607
+ //
25608
+ // `studio` is the local dev fleet on this machine, reachable over Tailscale
25609
+ // at the `studio.whittlelabs.com` hostname rather than `localhost`, because
25610
+ // a pairing made from a phone or a second machine has to resolve too.
25611
+ environments: {
25612
+ prod: {
25613
+ keepApiUrl: "https://keep.whittlelabs.com",
25614
+ jobsApiUrl: "https://jobs.whittlelabs.com"
25615
+ },
25616
+ stage: {
25617
+ keepApiUrl: "https://keep.stage.whittlelabs.com",
25618
+ jobsApiUrl: "https://jobs.stage.whittlelabs.com"
25619
+ },
25620
+ test: {
25621
+ keepApiUrl: "https://keep.test.whittlelabs.com",
25622
+ jobsApiUrl: "https://jobs.test.whittlelabs.com"
25623
+ },
25624
+ studio: {
25625
+ keepApiUrl: "http://studio.whittlelabs.com:3007",
25626
+ jobsApiUrl: "http://studio.whittlelabs.com:3005"
25308
25627
  }
25309
25628
  },
25310
25629
  keepRegistration: {