squad-kit 0.11.0 → 0.12.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.
package/dist/cli.js CHANGED
@@ -259,9 +259,9 @@ function isSquadExit(err2) {
259
259
  }
260
260
 
261
261
  // src/commands/init.ts
262
- import fs14 from "fs";
263
- import path6 from "path";
264
- import { input as input2, select as select3, checkbox, confirm as confirm5, password as password3 } from "@inquirer/prompts";
262
+ import fs17 from "fs";
263
+ import path7 from "path";
264
+ import { input as input2, select as select3, checkbox, confirm as confirm6, password as password4 } from "@inquirer/prompts";
265
265
 
266
266
  // src/core/paths.ts
267
267
  import path from "path";
@@ -352,8 +352,158 @@ function ensureGitignore(repoRoot) {
352
352
  }
353
353
 
354
354
  // src/core/config.ts
355
- import fs3 from "fs";
355
+ import fs4 from "fs";
356
356
  import yaml from "js-yaml";
357
+
358
+ // src/core/planner-auth.ts
359
+ import { execFileSync } from "child_process";
360
+ import fs3 from "fs";
361
+ import os from "os";
362
+ import path3 from "path";
363
+ var PLANNER_AUTH_MODES = ["subscription", "api-key", "auto"];
364
+ var PROVIDER_ENV_VAR = {
365
+ anthropic: "ANTHROPIC_API_KEY",
366
+ openai: "OPENAI_API_KEY",
367
+ google: "GOOGLE_API_KEY"
368
+ };
369
+ function buildUnavailableMessage(provider, configuredMode) {
370
+ if (provider === "anthropic") {
371
+ const lead = configuredMode === "api-key" ? "planner.auth.anthropic is `api-key`, but no Anthropic API key resolved." : "No Anthropic credential available.";
372
+ return `${lead} Either log in with your Claude subscription (\`squad auth login\`) or save an API key (\`squad config set planner\`, or export ANTHROPIC_API_KEY).`;
373
+ }
374
+ return `No ${provider} API key available. Run \`squad config set planner\` to save one, or export ${PROVIDER_ENV_VAR[provider]}.`;
375
+ }
376
+ var PlannerAuthUnavailableError = class extends Error {
377
+ constructor(provider, configuredMode) {
378
+ super(buildUnavailableMessage(provider, configuredMode));
379
+ this.provider = provider;
380
+ this.configuredMode = configuredMode;
381
+ this.name = "PlannerAuthUnavailableError";
382
+ }
383
+ provider;
384
+ configuredMode;
385
+ };
386
+ var KEYCHAIN_SERVICE = "Claude Code-credentials";
387
+ var NOT_LOGGED_IN = { present: false, hint: "none", detail: "no Claude login detected" };
388
+ function claudeConfigDir() {
389
+ const override = process.env.CLAUDE_CONFIG_DIR;
390
+ if (override && override.trim().length > 0) return path3.resolve(process.cwd(), override.trim());
391
+ try {
392
+ return path3.join(os.homedir(), ".claude");
393
+ } catch {
394
+ return void 0;
395
+ }
396
+ }
397
+ function keychainItemExists() {
398
+ try {
399
+ execFileSync("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE], {
400
+ stdio: "ignore",
401
+ timeout: 2e3
402
+ });
403
+ return true;
404
+ } catch {
405
+ return false;
406
+ }
407
+ }
408
+ function detectClaudeLogin(secrets) {
409
+ const envToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
410
+ if (envToken && envToken.trim().length > 0) {
411
+ return {
412
+ present: true,
413
+ hint: "oauth-token-env",
414
+ detail: "CLAUDE_CODE_OAUTH_TOKEN",
415
+ token: envToken.trim()
416
+ };
417
+ }
418
+ const savedToken = secrets?.planner?.anthropicOauthToken;
419
+ if (savedToken && savedToken.trim().length > 0) {
420
+ return {
421
+ present: true,
422
+ hint: "oauth-token-secrets",
423
+ detail: ".squad/secrets.yaml",
424
+ token: savedToken.trim()
425
+ };
426
+ }
427
+ if (process.platform === "darwin") {
428
+ return keychainItemExists() ? { present: true, hint: "credential-store", detail: "macOS Keychain" } : NOT_LOGGED_IN;
429
+ }
430
+ const configDir = claudeConfigDir();
431
+ if (!configDir) return NOT_LOGGED_IN;
432
+ const credentialsFile = path3.join(configDir, ".credentials.json");
433
+ try {
434
+ if (fs3.existsSync(credentialsFile)) {
435
+ return { present: true, hint: "credential-store", detail: credentialsFile };
436
+ }
437
+ } catch {
438
+ return NOT_LOGGED_IN;
439
+ }
440
+ return NOT_LOGGED_IN;
441
+ }
442
+ function apiKeyAuth(apiKey, reason) {
443
+ return {
444
+ mode: "api-key",
445
+ reason,
446
+ key: apiKey.value,
447
+ source: apiKey.source,
448
+ detail: apiKey.detail
449
+ };
450
+ }
451
+ function subscriptionAuth(login, reason) {
452
+ if (login.hint === "oauth-token-env" || login.hint === "oauth-token-secrets") {
453
+ return {
454
+ mode: "subscription",
455
+ reason,
456
+ oauthToken: login.token,
457
+ oauthTokenSource: login.hint === "oauth-token-env" ? "env" : "secrets",
458
+ loginHint: login.hint
459
+ };
460
+ }
461
+ return {
462
+ mode: "subscription",
463
+ reason,
464
+ loginHint: login.hint === "credential-store" ? "credential-store" : "assumed"
465
+ };
466
+ }
467
+ function resolvePlannerAuth(input5) {
468
+ const { provider, apiKey, login } = input5;
469
+ const configuredMode = input5.configuredMode ?? "auto";
470
+ if (provider !== "anthropic") {
471
+ if (apiKey) return apiKeyAuth(apiKey, "provider-is-api-key-only");
472
+ throw new PlannerAuthUnavailableError(provider, configuredMode);
473
+ }
474
+ if (configuredMode === "api-key") {
475
+ if (apiKey) return apiKeyAuth(apiKey, "explicit-config");
476
+ throw new PlannerAuthUnavailableError(provider, configuredMode);
477
+ }
478
+ if (configuredMode === "subscription") {
479
+ return subscriptionAuth(login, "explicit-config");
480
+ }
481
+ if (login.present) {
482
+ return subscriptionAuth(login, login.hint === "credential-store" ? "auto-login-detected" : "auto-oauth-token");
483
+ }
484
+ if (apiKey) return apiKeyAuth(apiKey, "auto-fallback-api-key");
485
+ throw new PlannerAuthUnavailableError(provider, configuredMode);
486
+ }
487
+ function describeAuth(auth2) {
488
+ if (auth2.mode === "api-key") {
489
+ return { mode: "api-key", reason: auth2.reason, credentialHint: auth2.detail };
490
+ }
491
+ return { mode: "subscription", reason: auth2.reason, credentialHint: loginCredentialHint(auth2.loginHint) };
492
+ }
493
+ function loginCredentialHint(loginHint) {
494
+ switch (loginHint) {
495
+ case "oauth-token-env":
496
+ return "Claude login (CLAUDE_CODE_OAUTH_TOKEN)";
497
+ case "oauth-token-secrets":
498
+ return "Claude login (.squad/secrets.yaml)";
499
+ case "credential-store":
500
+ return process.platform === "darwin" ? "Claude login (macOS Keychain)" : "Claude login (~/.claude/.credentials.json)";
501
+ case "assumed":
502
+ return "Claude login (assumed \u2014 not verified locally)";
503
+ }
504
+ }
505
+
506
+ // src/core/config.ts
357
507
  var DEFAULT_CONFIG = {
358
508
  version: 1,
359
509
  project: {
@@ -366,7 +516,16 @@ var DEFAULT_CONFIG = {
366
516
  agents: [],
367
517
  planner: void 0
368
518
  };
369
- var FORBIDDEN_KEYS = ["apikey", "api_key", "token", "secret", "credential", "credentials"];
519
+ var FORBIDDEN_KEYS = [
520
+ "apikey",
521
+ "api_key",
522
+ "token",
523
+ "oauthtoken",
524
+ "anthropicoauthtoken",
525
+ "secret",
526
+ "credential",
527
+ "credentials"
528
+ ];
370
529
  var DEFAULT_PLANNER_MAX_OUTPUT_TOKENS = 16384;
371
530
  var MIN_PLANNER_MAX_OUTPUT_TOKENS = 1024;
372
531
  var MAX_PLANNER_MAX_OUTPUT_TOKENS = 128e3;
@@ -379,23 +538,34 @@ function clampPlannerMaxOut(raw) {
379
538
  Math.max(MIN_PLANNER_MAX_OUTPUT_TOKENS, Math.round(n))
380
539
  );
381
540
  }
382
- function rejectSecretsInYaml(node, configFile, path39 = []) {
541
+ function rejectSecretsInYaml(node, configFile, path40 = []) {
383
542
  if (node === null || typeof node !== "object") return;
384
543
  for (const [key, value] of Object.entries(node)) {
385
544
  if (FORBIDDEN_KEYS.includes(key.toLowerCase())) {
386
545
  throw new Error(
387
- `Refusing to load ${configFile}: key "${[...path39, key].join(".")}" looks like a secret. Run \`squad config set planner\` or \`squad config set tracker\` to save it to .squad/secrets.yaml, then remove the key from config.yaml. Provider keys also work via ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY.`
546
+ `Refusing to load ${configFile}: key "${[...path40, key].join(".")}" looks like a secret. Run \`squad config set planner\` or \`squad config set tracker\` to save it to .squad/secrets.yaml, then remove the key from config.yaml. Provider keys also work via ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY.`
388
547
  );
389
548
  }
390
- if (typeof value === "object") rejectSecretsInYaml(value, configFile, [...path39, key]);
549
+ if (typeof value === "object") rejectSecretsInYaml(value, configFile, [...path40, key]);
391
550
  }
392
551
  }
393
- function mergePlanner(base, override) {
552
+ function normalizeAuthMode(raw, configFile) {
553
+ if (raw === void 0 || raw === null) return void 0;
554
+ const value = typeof raw === "string" ? raw.trim().toLowerCase() : "";
555
+ if (PLANNER_AUTH_MODES.includes(value)) return value;
556
+ throw new Error(
557
+ `Invalid planner.auth.anthropic ${JSON.stringify(raw)} in ${configFile}: must be ${PLANNER_AUTH_MODES.join(", ")}. Run \`squad config set planner\` to fix it.`
558
+ );
559
+ }
560
+ function mergePlanner(base, override, configFile) {
394
561
  if (!override) return base;
395
562
  const merged = {
396
563
  enabled: override.enabled ?? base?.enabled ?? false,
397
564
  provider: override.provider ?? base?.provider ?? "anthropic",
398
565
  mode: override.mode ?? base?.mode ?? "auto",
566
+ auth: {
567
+ anthropic: normalizeAuthMode(override.auth?.anthropic ?? base?.auth?.anthropic, configFile) ?? "auto"
568
+ },
399
569
  budget: {
400
570
  maxFileReads: override.budget?.maxFileReads ?? base?.budget?.maxFileReads ?? 25,
401
571
  maxContextBytes: override.budget?.maxContextBytes ?? base?.budget?.maxContextBytes ?? 5e4,
@@ -480,7 +650,7 @@ function parseConfig(raw, configFile) {
480
650
  );
481
651
  }
482
652
  rejectSecretsInYaml(parsed, configFile);
483
- const merged = mergeConfig(DEFAULT_CONFIG, parsed);
653
+ const merged = mergeConfig(DEFAULT_CONFIG, parsed, configFile);
484
654
  validateModelOverride(merged.planner?.modelOverride, configFile);
485
655
  if (merged.planner?.cache !== void 0) {
486
656
  const c = merged.planner.cache;
@@ -527,7 +697,7 @@ function parseConfig(raw, configFile) {
527
697
  return merged;
528
698
  }
529
699
  function loadConfig(configFile) {
530
- const raw = fs3.readFileSync(configFile, "utf8");
700
+ const raw = fs4.readFileSync(configFile, "utf8");
531
701
  return parseConfig(raw, configFile);
532
702
  }
533
703
  function serializeConfig(config2) {
@@ -536,32 +706,32 @@ function serializeConfig(config2) {
536
706
  function saveConfig(configFile, config2) {
537
707
  const body = serializeConfig(config2);
538
708
  parseConfig(body, configFile);
539
- fs3.writeFileSync(configFile, body, "utf8");
709
+ fs4.writeFileSync(configFile, body, "utf8");
540
710
  }
541
- function mergeConfig(base, override) {
711
+ function mergeConfig(base, override, configFile) {
542
712
  return {
543
713
  version: override.version ?? base.version,
544
714
  project: { ...base.project, ...override.project ?? {} },
545
715
  tracker: { ...base.tracker, ...override.tracker ?? {} },
546
716
  naming: { ...base.naming, ...override.naming ?? {} },
547
717
  agents: override.agents ?? base.agents,
548
- planner: mergePlanner(base.planner, override.planner)
718
+ planner: mergePlanner(base.planner, override.planner, configFile)
549
719
  };
550
720
  }
551
721
 
552
722
  // src/core/planner-models.ts
553
- import fs5 from "fs";
723
+ import fs6 from "fs";
554
724
 
555
725
  // src/core/secrets.ts
556
- import fs4 from "fs";
557
- import path3 from "path";
726
+ import fs5 from "fs";
727
+ import path4 from "path";
558
728
  import yaml2 from "js-yaml";
559
729
  var EMPTY = {};
560
730
  function loadSecrets(secretsFile) {
561
- if (!fs4.existsSync(secretsFile)) return EMPTY;
731
+ if (!fs5.existsSync(secretsFile)) return EMPTY;
562
732
  let raw;
563
733
  try {
564
- raw = fs4.readFileSync(secretsFile, "utf8");
734
+ raw = fs5.readFileSync(secretsFile, "utf8");
565
735
  } catch (err2) {
566
736
  throw new Error(
567
737
  `Failed to read ${secretsFile}: ${err2.message}. Run \`squad doctor\` to diagnose, or delete the file and run \`squad config set planner\` or \`squad config set tracker\` to re-enter credentials.`
@@ -584,15 +754,15 @@ function loadSecrets(secretsFile) {
584
754
  return parsed;
585
755
  }
586
756
  function saveSecrets(secretsFile, secrets) {
587
- fs4.mkdirSync(path3.dirname(secretsFile), { recursive: true });
757
+ fs5.mkdirSync(path4.dirname(secretsFile), { recursive: true });
588
758
  const body = yaml2.dump(secrets, { lineWidth: 100, noRefs: true, sortKeys: false });
589
- fs4.writeFileSync(secretsFile, body, "utf8");
759
+ fs5.writeFileSync(secretsFile, body, "utf8");
590
760
  tightenPermissions(secretsFile);
591
761
  }
592
762
  function tightenPermissions(file) {
593
763
  if (process.platform === "win32") return;
594
764
  try {
595
- fs4.chmodSync(file, 384);
765
+ fs5.chmodSync(file, 384);
596
766
  } catch {
597
767
  }
598
768
  }
@@ -667,7 +837,7 @@ function resolveProviderKey(provider) {
667
837
  const root = findSquadRoot();
668
838
  if (root) {
669
839
  const paths = buildPaths(root);
670
- if (fs5.existsSync(paths.secretsFile)) {
840
+ if (fs6.existsSync(paths.secretsFile)) {
671
841
  const secrets = loadSecrets(paths.secretsFile);
672
842
  const fromFile = secrets.planner?.[provider];
673
843
  if (fromFile) return { value: fromFile, source: "secrets", detail: ".squad/secrets.yaml" };
@@ -678,68 +848,89 @@ function resolveProviderKey(provider) {
678
848
  function readProviderKey(provider) {
679
849
  return resolveProviderKey(provider)?.value;
680
850
  }
681
- function readProviderKeyForPaths(paths, provider) {
851
+ function resolveProviderKeyForPaths(paths, provider) {
682
852
  const envVar = providerEnvVar(provider);
683
853
  const envValue = process.env[envVar];
684
- if (envValue) return envValue;
854
+ if (envValue) return { value: envValue, source: "env", detail: envVar };
685
855
  const fallback = process.env.SQUAD_PLANNER_API_KEY;
686
- if (fallback) return fallback;
687
- if (fs5.existsSync(paths.secretsFile)) {
856
+ if (fallback) return { value: fallback, source: "fallback-env", detail: "SQUAD_PLANNER_API_KEY" };
857
+ if (fs6.existsSync(paths.secretsFile)) {
688
858
  const secrets = loadSecrets(paths.secretsFile);
689
859
  const fromFile = secrets.planner?.[provider];
690
- if (fromFile) return fromFile;
860
+ if (fromFile) return { value: fromFile, source: "secrets", detail: ".squad/secrets.yaml" };
691
861
  }
692
862
  return void 0;
693
863
  }
864
+ function configuredAuthMode(provider, config2) {
865
+ return provider === "anthropic" ? config2?.auth?.anthropic : void 0;
866
+ }
867
+ function resolvePlannerAuthForCwd(provider, config2) {
868
+ const root = findSquadRoot();
869
+ const secrets = root ? loadSecrets(buildPaths(root).secretsFile) : void 0;
870
+ return resolvePlannerAuth({
871
+ provider,
872
+ configuredMode: configuredAuthMode(provider, config2),
873
+ apiKey: resolveProviderKey(provider),
874
+ login: detectClaudeLogin(secrets)
875
+ });
876
+ }
877
+ function resolvePlannerAuthForPaths(paths, provider, config2) {
878
+ return resolvePlannerAuth({
879
+ provider,
880
+ configuredMode: configuredAuthMode(provider, config2),
881
+ apiKey: resolveProviderKeyForPaths(paths, provider),
882
+ login: detectClaudeLogin(loadSecrets(paths.secretsFile))
883
+ });
884
+ }
694
885
 
695
886
  // src/utils/fs.ts
696
- import fs6 from "fs";
697
- import path4 from "path";
887
+ import fs7 from "fs";
888
+ import path5 from "path";
698
889
  import { fileURLToPath } from "url";
699
890
  function readFile(file) {
700
- return fs6.readFileSync(file, "utf8");
891
+ return fs7.readFileSync(file, "utf8");
701
892
  }
702
893
  function writeFileSafe(file, content, overwrite = false) {
703
- if (!overwrite && fs6.existsSync(file)) return false;
704
- fs6.mkdirSync(path4.dirname(file), { recursive: true });
705
- fs6.writeFileSync(file, content, "utf8");
894
+ if (!overwrite && fs7.existsSync(file)) return false;
895
+ fs7.mkdirSync(path5.dirname(file), { recursive: true });
896
+ fs7.writeFileSync(file, content, "utf8");
706
897
  return true;
707
898
  }
708
899
  function copyTree(srcDir, destDir, overwrite = false) {
709
- fs6.mkdirSync(destDir, { recursive: true });
710
- for (const entry of fs6.readdirSync(srcDir, { withFileTypes: true })) {
711
- const src = path4.join(srcDir, entry.name);
712
- const dest = path4.join(destDir, entry.name);
900
+ fs7.mkdirSync(destDir, { recursive: true });
901
+ for (const entry of fs7.readdirSync(srcDir, { withFileTypes: true })) {
902
+ const src = path5.join(srcDir, entry.name);
903
+ const dest = path5.join(destDir, entry.name);
713
904
  if (entry.isDirectory()) {
714
905
  copyTree(src, dest, overwrite);
715
906
  } else if (entry.isFile()) {
716
- if (!overwrite && fs6.existsSync(dest)) continue;
717
- fs6.copyFileSync(src, dest);
907
+ if (!overwrite && fs7.existsSync(dest)) continue;
908
+ fs7.copyFileSync(src, dest);
718
909
  }
719
910
  }
720
911
  }
721
912
  function packageRoot() {
722
- const here = path4.dirname(fileURLToPath(import.meta.url));
913
+ const here = path5.dirname(fileURLToPath(import.meta.url));
723
914
  let dir = here;
724
915
  for (let i = 0; i < 6; i++) {
725
- if (fs6.existsSync(path4.join(dir, "package.json")) && fs6.existsSync(path4.join(dir, "templates"))) {
916
+ if (fs7.existsSync(path5.join(dir, "package.json")) && fs7.existsSync(path5.join(dir, "templates"))) {
726
917
  return dir;
727
918
  }
728
- dir = path4.dirname(dir);
919
+ dir = path5.dirname(dir);
729
920
  }
730
921
  throw new Error(`Could not locate squad-kit package root from ${here}`);
731
922
  }
732
923
  function templatesDir() {
733
- return path4.join(packageRoot(), "templates");
924
+ return path5.join(packageRoot(), "templates");
734
925
  }
735
926
  function readBundledPrompt(name) {
736
- const file = path4.join(templatesDir(), "prompts", name);
737
- if (!fs6.existsSync(file)) {
927
+ const file = path5.join(templatesDir(), "prompts", name);
928
+ if (!fs7.existsSync(file)) {
738
929
  throw new Error(
739
930
  `Bundled prompt "${name}" not found at ${file}. This indicates a broken squad-kit install \u2014 reinstall with: pnpm add -g squad-kit`
740
931
  );
741
932
  }
742
- return fs6.readFileSync(file, "utf8");
933
+ return fs7.readFileSync(file, "utf8");
743
934
  }
744
935
 
745
936
  // src/core/template.ts
@@ -755,7 +946,7 @@ function escapeTemplateValue(s) {
755
946
  }
756
947
 
757
948
  // src/commands/config/show.ts
758
- import fs7 from "fs";
949
+ import fs8 from "fs";
759
950
 
760
951
  // src/commands/config/masks.ts
761
952
  function maskStringKey(value) {
@@ -866,7 +1057,7 @@ async function runConfigShow(opts) {
866
1057
  const root = requireSquadRoot();
867
1058
  const paths = buildPaths(root);
868
1059
  const config2 = loadConfig(paths.configFile);
869
- const secrets = fs7.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
1060
+ const secrets = fs8.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
870
1061
  const payload = buildPayload(config2, secrets);
871
1062
  if (opts.json) {
872
1063
  process.stdout.write(`${JSON.stringify(payload, null, 2)}
@@ -877,8 +1068,8 @@ async function runConfigShow(opts) {
877
1068
  }
878
1069
 
879
1070
  // src/commands/config/set-planner.ts
880
- import fs9 from "fs";
881
- import { confirm as confirm2, password as password2, select } from "@inquirer/prompts";
1071
+ import fs12 from "fs";
1072
+ import { confirm as confirm3, password as password3, select } from "@inquirer/prompts";
882
1073
 
883
1074
  // src/tracker/env-overlay.ts
884
1075
  function overlayTrackerEnv(base) {
@@ -978,11 +1169,11 @@ async function probeJiraConnectivity(secrets, config2) {
978
1169
  const host = jira.host ?? config2.tracker.workspace ?? "";
979
1170
  const email = jira.email ?? "";
980
1171
  const token = jira.token ?? "";
981
- const auth = Buffer.from(`${email}:${token}`).toString("base64");
1172
+ const auth2 = Buffer.from(`${email}:${token}`).toString("base64");
982
1173
  const url = jiraMyselfUrl(host);
983
1174
  try {
984
1175
  const res = await fetch(url, {
985
- headers: { Authorization: `Basic ${auth}`, Accept: "application/json" }
1176
+ headers: { Authorization: `Basic ${auth2}`, Accept: "application/json" }
986
1177
  });
987
1178
  if (res.ok) return { ok: true };
988
1179
  return { ok: false, status: res.status, detail: (await res.text()).slice(0, 200) };
@@ -994,10 +1185,10 @@ async function probeAzureConnectivity(secrets, config2) {
994
1185
  const az = overlayTrackerEnv(secrets).tracker?.azure ?? {};
995
1186
  const org = az.organization ?? config2.tracker.workspace ?? "";
996
1187
  const pat = az.pat ?? "";
997
- const auth = Buffer.from(`:${pat}`).toString("base64");
1188
+ const auth2 = Buffer.from(`:${pat}`).toString("base64");
998
1189
  const url = `https://dev.azure.com/${encodeURIComponent(org)}/_apis/projects?api-version=7.1`;
999
1190
  try {
1000
- const res = await fetch(url, { headers: { Authorization: `Basic ${auth}` } });
1191
+ const res = await fetch(url, { headers: { Authorization: `Basic ${auth2}` } });
1001
1192
  if (res.ok) return { ok: true };
1002
1193
  return { ok: false, status: res.status, detail: (await res.text()).slice(0, 200) };
1003
1194
  } catch (err2) {
@@ -1026,6 +1217,357 @@ async function probeGitHubConnectivity(secrets, _config) {
1026
1217
  }
1027
1218
  }
1028
1219
 
1220
+ // src/core/claude-binary.ts
1221
+ import { execFileSync as execFileSync2 } from "child_process";
1222
+ import fs9 from "fs";
1223
+ import { createRequire } from "module";
1224
+ function claudeBinaryPackageSpecs(platform = process.platform, arch = process.arch) {
1225
+ const packages = platform === "linux" ? [
1226
+ `@anthropic-ai/claude-agent-sdk-linux-${arch}-musl`,
1227
+ `@anthropic-ai/claude-agent-sdk-linux-${arch}`
1228
+ ] : [`@anthropic-ai/claude-agent-sdk-${platform}-${arch}`];
1229
+ const fileNames = platform === "win32" ? ["claude.exe", "claude"] : ["claude"];
1230
+ return packages.flatMap((pkg) => fileNames.map((file) => `${pkg}/${file}`));
1231
+ }
1232
+ function requireAnchors() {
1233
+ const own = createRequire(import.meta.url);
1234
+ const anchors = [own];
1235
+ try {
1236
+ const sdkEntry = own.resolve("@anthropic-ai/claude-agent-sdk");
1237
+ anchors.push(createRequire(sdkEntry));
1238
+ const realEntry = fs9.realpathSync(sdkEntry);
1239
+ if (realEntry !== sdkEntry) anchors.push(createRequire(realEntry));
1240
+ } catch {
1241
+ }
1242
+ return anchors;
1243
+ }
1244
+ function resolveBundled() {
1245
+ const specs = claudeBinaryPackageSpecs();
1246
+ for (const req of requireAnchors()) {
1247
+ for (const spec of specs) {
1248
+ try {
1249
+ const resolved = req.resolve(spec);
1250
+ if (fs9.existsSync(resolved)) return resolved;
1251
+ } catch {
1252
+ }
1253
+ }
1254
+ }
1255
+ return void 0;
1256
+ }
1257
+ function resolveOnPath() {
1258
+ const finder = process.platform === "win32" ? "where" : "which";
1259
+ try {
1260
+ const out = execFileSync2(finder, ["claude"], {
1261
+ encoding: "utf8",
1262
+ stdio: ["ignore", "pipe", "ignore"],
1263
+ timeout: 3e3
1264
+ });
1265
+ const first = out.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0);
1266
+ if (first && fs9.existsSync(first)) return first;
1267
+ } catch {
1268
+ }
1269
+ return void 0;
1270
+ }
1271
+ var cached;
1272
+ function resolveClaudeBinary() {
1273
+ if (cached) return cached.value;
1274
+ const bundled = resolveBundled();
1275
+ if (bundled) {
1276
+ cached = { value: { path: bundled, source: "bundled" } };
1277
+ return cached.value;
1278
+ }
1279
+ const onPath = resolveOnPath();
1280
+ cached = { value: onPath ? { path: onPath, source: "path" } : void 0 };
1281
+ return cached.value;
1282
+ }
1283
+ var MISSING_CLAUDE_BINARY_MESSAGE = "Could not find a `claude` executable. Reinstall squad-kit so the Agent SDK's platform binary is present, or install Claude Code and ensure `claude` is on your PATH.";
1284
+
1285
+ // src/planner/provider-errors.ts
1286
+ function detectModelNotFound(provider, model, status, rawBody) {
1287
+ if (status !== 404) return void 0;
1288
+ const body = rawBody.toLowerCase();
1289
+ const hints = ["not_found_error", "model_not_found", "not_found", "model:"];
1290
+ if (!hints.some((h) => body.includes(h))) return void 0;
1291
+ return { provider, model, status, rawBody };
1292
+ }
1293
+ function modelNotFoundMessage(err2) {
1294
+ return [
1295
+ `The ${err2.provider} planner model "${err2.model}" is no longer available.`,
1296
+ "",
1297
+ "Recovery options:",
1298
+ ` 1. Run \`squad upgrade\` to install a newer squad-kit (or install manually from npm).`,
1299
+ ` 2. Run \`squad config set planner --provider openai\` (or another \`--provider\`) to switch models via a different provider, then save a key when prompted.`,
1300
+ ` 3. Run \`squad config set planner\` to set \`planner.modelOverride.${err2.provider}\` to a still-valid id (no raw YAML), then \`squad doctor\` to confirm.`,
1301
+ "",
1302
+ `Raw provider response: ${err2.rawBody.slice(0, 200)}`
1303
+ ].join("\n");
1304
+ }
1305
+ function detectRateLimit(provider, status, headers, rawBody) {
1306
+ if (status !== 429) return void 0;
1307
+ return {
1308
+ provider,
1309
+ status: 429,
1310
+ retryAfterSec: parseRetryAfter(headers, rawBody),
1311
+ rawBody
1312
+ };
1313
+ }
1314
+ function parseRetryAfterSec(headers) {
1315
+ const raw = headerGet(headers, "retry-after");
1316
+ if (!raw) return void 0;
1317
+ const sec = Number(raw);
1318
+ if (Number.isFinite(sec) && sec >= 0) return Math.ceil(sec);
1319
+ const asDate = Date.parse(raw);
1320
+ if (Number.isFinite(asDate)) {
1321
+ return Math.max(0, Math.ceil((asDate - Date.now()) / 1e3));
1322
+ }
1323
+ return void 0;
1324
+ }
1325
+ function parseRetryAfter(headers, body) {
1326
+ const fromHeader = parseRetryAfterSec(headers);
1327
+ if (fromHeader !== void 0) return fromHeader;
1328
+ const bodyMatch = /"retryDelay"\s*:\s*"(\d+)(?:\.\d+)?s"/i.exec(body);
1329
+ if (bodyMatch) {
1330
+ const sec = Number(bodyMatch[1]);
1331
+ if (Number.isFinite(sec) && sec >= 0) return sec;
1332
+ }
1333
+ return void 0;
1334
+ }
1335
+ function headerGet(h, name) {
1336
+ if (typeof h.get === "function") {
1337
+ return h.get(name);
1338
+ }
1339
+ const key = name.toLowerCase();
1340
+ const rec = h;
1341
+ for (const k of Object.keys(rec)) {
1342
+ if (k.toLowerCase() === key) return rec[k] ?? null;
1343
+ }
1344
+ return null;
1345
+ }
1346
+ function rateLimitMessage(err2) {
1347
+ const waitHint = err2.retryAfterSec ?? 60;
1348
+ const limitsUrl = {
1349
+ anthropic: "https://console.anthropic.com/settings/limits",
1350
+ openai: "https://platform.openai.com/settings/organization/limits",
1351
+ google: "https://aistudio.google.com/app/plan_information"
1352
+ };
1353
+ const headline = err2.retryAfterSec ? `${err2.provider} rate limit hit \u2014 provider asked us to wait ${err2.retryAfterSec}s before retrying.` : `${err2.provider} rate limit hit.`;
1354
+ const cap = err2.maxRetrySec ?? 90;
1355
+ const retried = err2.retrySkippedReason === "retry_after_too_long" ? `squad-kit did not auto-retry: the provider's ${err2.retryAfterSec}s wait is longer than our ${cap}s cap, so retrying would just burn another request inside the same throttle window.` : err2.retryAlreadyAttempted ? "squad-kit already retried once automatically and was throttled again \u2014 your org is firmly over its per-minute quota." : "squad-kit aborted before retrying.";
1356
+ return [
1357
+ headline,
1358
+ retried,
1359
+ "",
1360
+ "Recovery options:",
1361
+ ` 1. Wait ${waitHint}s and rerun \`squad new-plan --api\`. Per-minute limits reset quickly.`,
1362
+ ` 2. Switch to a smaller planner model: \`squad config set planner\` (pick a cheaper model id).`,
1363
+ ` 3. Tighten \`planner.budget\` in \`.squad/config.yaml\` (smaller \`maxContextBytes\` / \`maxFileReads\`) so each request carries fewer tokens.`,
1364
+ ` 4. Upgrade your ${err2.provider} tier: ${limitsUrl[err2.provider]}.`,
1365
+ "",
1366
+ "Full runbook: docs/migrating-from-0.1.md (see \xA78. If something goes wrong).",
1367
+ "",
1368
+ `Raw provider response: ${err2.rawBody.slice(0, 200)}`
1369
+ ].join("\n");
1370
+ }
1371
+
1372
+ // src/planner/auth-errors.ts
1373
+ var AUTH_SHAPED_SDK_ERRORS = [
1374
+ "authentication_failed",
1375
+ "oauth_org_not_allowed",
1376
+ "billing_error",
1377
+ "rate_limit"
1378
+ ];
1379
+ function authErrorContextFrom(auth2) {
1380
+ if (auth2.mode === "api-key") return { mode: "api-key" };
1381
+ return auth2.oauthTokenSource ? { mode: "subscription", oauthTokenSource: auth2.oauthTokenSource } : { mode: "subscription" };
1382
+ }
1383
+ function detectAuthShapedSdkError(raw) {
1384
+ if (typeof raw === "string") {
1385
+ const direct = AUTH_SHAPED_SDK_ERRORS.find((s) => s === raw);
1386
+ if (direct) return direct;
1387
+ return AUTH_SHAPED_SDK_ERRORS.find((s) => raw.includes(s));
1388
+ }
1389
+ if (Array.isArray(raw)) {
1390
+ for (const entry of raw) {
1391
+ const hit = detectAuthShapedSdkError(entry);
1392
+ if (hit) return hit;
1393
+ }
1394
+ }
1395
+ return void 0;
1396
+ }
1397
+ function authenticationFailedMessage(ctx) {
1398
+ if (ctx.mode === "api-key") {
1399
+ return "Anthropic rejected the planner API key. Save a valid key with `squad config set planner`, or fix the exported ANTHROPIC_API_KEY, then re-run.";
1400
+ }
1401
+ if (ctx.oauthTokenSource === "env") {
1402
+ return "Claude login failed or expired. This run used the token in CLAUDE_CODE_OAUTH_TOKEN \u2014 unset it (or replace it with a fresh `claude setup-token` value), then run `squad auth login` to sign in again and re-run.";
1403
+ }
1404
+ if (ctx.oauthTokenSource === "secrets") {
1405
+ return "Claude login failed or expired. This run used the token saved in `.squad/secrets.yaml` \u2014 run `squad auth logout` to clear it, then `squad auth login` to sign in again, then re-run.";
1406
+ }
1407
+ return "Claude login failed or expired. Run `squad auth login` to sign in again, then re-run.";
1408
+ }
1409
+ function billingErrorMessage(ctx) {
1410
+ if (ctx.mode === "api-key") {
1411
+ return "Anthropic refused the request for billing reasons. Check your credit balance at https://console.anthropic.com/settings/billing, then re-run.";
1412
+ }
1413
+ return "Your Claude plan cannot run this request. Check your plan at claude.ai/settings, or switch to an API key with `squad config set planner`.";
1414
+ }
1415
+ function rateLimitCopy(ctx, rawBody) {
1416
+ if (ctx.mode === "api-key") {
1417
+ return rateLimitMessage({ provider: "anthropic", rawBody });
1418
+ }
1419
+ return "Claude subscription usage limit reached. Planning draws on the same limits as Claude and Claude Code \u2014 wait for your usage window to reset, or switch to an API key for this run with `squad config set planner`.";
1420
+ }
1421
+ function authErrorMessage(signal, ctx, rawBody = "") {
1422
+ switch (signal) {
1423
+ case "authentication_failed":
1424
+ return authenticationFailedMessage(ctx);
1425
+ case "oauth_org_not_allowed":
1426
+ return "Your Claude account's organization is not allowed to use this login. Ask your admin, or switch to an API key with `squad config set planner`.";
1427
+ case "billing_error":
1428
+ return billingErrorMessage(ctx);
1429
+ case "rate_limit":
1430
+ return rateLimitCopy(ctx, rawBody);
1431
+ }
1432
+ }
1433
+
1434
+ // src/planner/runtimes/sdk-env.ts
1435
+ var SUBSCRIPTION_WITHHELD_ENV_KEYS = ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"];
1436
+ function buildSdkEnv(auth2) {
1437
+ const env = { ...process.env };
1438
+ if (auth2.mode === "api-key") {
1439
+ env.ANTHROPIC_API_KEY = auth2.key;
1440
+ return env;
1441
+ }
1442
+ for (const key of Object.keys(env)) {
1443
+ if (SUBSCRIPTION_WITHHELD_ENV_KEYS.includes(key.toUpperCase())) delete env[key];
1444
+ }
1445
+ if (auth2.oauthToken) env.CLAUDE_CODE_OAUTH_TOKEN = auth2.oauthToken;
1446
+ return env;
1447
+ }
1448
+
1449
+ // src/planner/runtimes/auth-probe.ts
1450
+ var DEFAULT_AUTH_PROBE_TIMEOUT_MS = 1e4;
1451
+ function readAccount(raw) {
1452
+ if (!raw || typeof raw !== "object") return {};
1453
+ const o = raw;
1454
+ const account = {};
1455
+ if (typeof o.email === "string") account.email = o.email;
1456
+ if (typeof o.organization === "string") account.organization = o.organization;
1457
+ if (typeof o.subscriptionType === "string") account.subscriptionType = o.subscriptionType;
1458
+ const apiKeySource = typeof o.apiKeySource === "string" ? o.apiKeySource : void 0;
1459
+ const tokenSource = typeof o.tokenSource === "string" ? o.tokenSource : void 0;
1460
+ const apiProvider = typeof o.apiProvider === "string" ? o.apiProvider : void 0;
1461
+ return {
1462
+ account: Object.keys(account).length > 0 ? account : void 0,
1463
+ apiKeySource,
1464
+ tokenSource,
1465
+ apiProvider
1466
+ };
1467
+ }
1468
+ var EXPIRED_HINT = /expire|refresh|revoked|no longer valid/i;
1469
+ var NO_BINARY_HINT = /native cli binary|executable not found|claude code executable/i;
1470
+ function mapProbeFailure(err2, auth2) {
1471
+ const message = err2 instanceof Error ? err2.message : String(err2);
1472
+ const signal = detectAuthShapedSdkError(message);
1473
+ if (signal === "authentication_failed") {
1474
+ return {
1475
+ ok: false,
1476
+ kind: EXPIRED_HINT.test(message) ? "expired" : "not-logged-in",
1477
+ // Reuse story 15's mapper rather than writing a second one: the copy a failed probe prints
1478
+ // must match the copy a failed run prints, or the two disagree about the same credential.
1479
+ detail: authErrorMessage("authentication_failed", authErrorContextFrom(auth2))
1480
+ };
1481
+ }
1482
+ if (signal === "oauth_org_not_allowed") {
1483
+ return {
1484
+ ok: false,
1485
+ kind: "org-not-allowed",
1486
+ detail: authErrorMessage("oauth_org_not_allowed", authErrorContextFrom(auth2))
1487
+ };
1488
+ }
1489
+ if (signal) {
1490
+ return { ok: false, kind: "unknown", detail: authErrorMessage(signal, authErrorContextFrom(auth2)) };
1491
+ }
1492
+ if (NO_BINARY_HINT.test(message)) {
1493
+ return { ok: false, kind: "no-binary", detail: message.slice(0, 300) };
1494
+ }
1495
+ return { ok: false, kind: "unknown", detail: message.slice(0, 300) };
1496
+ }
1497
+ async function probeClaudeAuth(auth2, opts = {}) {
1498
+ if (!resolveClaudeBinary()) {
1499
+ return {
1500
+ ok: false,
1501
+ kind: "no-binary",
1502
+ detail: "No `claude` executable found. Reinstall squad-kit so the Agent SDK platform binary is present, or install Claude Code and put `claude` on your PATH."
1503
+ };
1504
+ }
1505
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_AUTH_PROBE_TIMEOUT_MS;
1506
+ let releaseInput = () => {
1507
+ };
1508
+ const inputClosed = new Promise((resolve2) => {
1509
+ releaseInput = resolve2;
1510
+ });
1511
+ async function* neverSends() {
1512
+ await inputClosed;
1513
+ }
1514
+ let q;
1515
+ let timer;
1516
+ try {
1517
+ const { query } = await import("@anthropic-ai/claude-agent-sdk");
1518
+ q = query({
1519
+ prompt: neverSends(),
1520
+ options: {
1521
+ tools: [],
1522
+ settingSources: [],
1523
+ persistSession: false,
1524
+ maxTurns: 1,
1525
+ env: buildSdkEnv(auth2)
1526
+ }
1527
+ });
1528
+ const timedOut = /* @__PURE__ */ Symbol("timeout");
1529
+ const info2 = await Promise.race([
1530
+ typeof q.accountInfo === "function" ? q.accountInfo() : Promise.resolve(void 0),
1531
+ new Promise((resolve2) => {
1532
+ timer = setTimeout(() => resolve2(timedOut), timeoutMs);
1533
+ })
1534
+ ]);
1535
+ if (info2 === timedOut) {
1536
+ return {
1537
+ ok: false,
1538
+ kind: "timeout",
1539
+ detail: `The Claude auth check did not answer within ${timeoutMs}ms.`
1540
+ };
1541
+ }
1542
+ const { account, apiKeySource, tokenSource, apiProvider } = readAccount(info2);
1543
+ const base = { ...apiKeySource ? { apiKeySource } : {}, ...account ? { account } : {} };
1544
+ if (auth2.mode !== "subscription") return { ok: true, ...base };
1545
+ if (account) return { ok: true, ...base };
1546
+ if (tokenSource || apiProvider) {
1547
+ return {
1548
+ ok: true,
1549
+ ...base,
1550
+ ...tokenSource ? { credentialSource: tokenSource } : {},
1551
+ unverifiable: `The Agent SDK resolved ${tokenSource ?? "a credential"}, but token auth reports no account, so its validity can only be confirmed by an actual planning run.`
1552
+ };
1553
+ }
1554
+ return {
1555
+ ok: false,
1556
+ kind: "unknown",
1557
+ detail: "The Claude login reported neither an account nor a credential source, so it could not be verified. Run `squad auth login` to sign in again."
1558
+ };
1559
+ } catch (err2) {
1560
+ return mapProbeFailure(err2, auth2);
1561
+ } finally {
1562
+ if (timer) clearTimeout(timer);
1563
+ releaseInput();
1564
+ try {
1565
+ await q?.return?.();
1566
+ } catch {
1567
+ }
1568
+ }
1569
+ }
1570
+
1029
1571
  // src/commands/config/shared.ts
1030
1572
  import { input, password } from "@inquirer/prompts";
1031
1573
  var notEmpty = (v) => v.trim().length > 0 || "required";
@@ -1106,9 +1648,12 @@ function mergePlannerKeyIntoSecrets(base, provider, key) {
1106
1648
  }
1107
1649
  return mergeSecrets(base, { planner: { google: key } });
1108
1650
  }
1651
+ function mergePlannerOauthTokenIntoSecrets(base, token) {
1652
+ return mergeSecrets(base, { planner: { anthropicOauthToken: token } });
1653
+ }
1109
1654
 
1110
1655
  // src/commands/config/unset-planner.ts
1111
- import fs8 from "fs";
1656
+ import fs10 from "fs";
1112
1657
  import { confirm } from "@inquirer/prompts";
1113
1658
  async function runConfigUnsetPlanner(opts = {}) {
1114
1659
  const root = requireSquadRoot();
@@ -1129,24 +1674,248 @@ async function runConfigUnsetPlanner(opts = {}) {
1129
1674
  delete next.planner;
1130
1675
  saveConfig(paths.configFile, next);
1131
1676
  if (opts.removeCredentials) {
1132
- const s = fs8.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
1677
+ const s = fs10.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
1133
1678
  if (s.planner) {
1134
1679
  delete s.planner;
1135
1680
  saveSecrets(paths.secretsFile, s);
1136
1681
  }
1137
- success("Planner disabled; planner credentials were removed from .squad/secrets.yaml.");
1138
- } else {
1139
- success("Planner disabled; existing planner keys in .squad/secrets.yaml (if any) were preserved.");
1682
+ success("Planner disabled; planner credentials were removed from .squad/secrets.yaml.");
1683
+ } else {
1684
+ success("Planner disabled; existing planner keys in .squad/secrets.yaml (if any) were preserved.");
1685
+ }
1686
+ info("Re-enable any time with `squad config set planner`.");
1687
+ }
1688
+
1689
+ // src/commands/auth/login.ts
1690
+ import fs11 from "fs";
1691
+ import { confirm as confirm2, password as password2 } from "@inquirer/prompts";
1692
+
1693
+ // src/core/ci-env.ts
1694
+ function skipExternalProbesInAutomation() {
1695
+ const c = process.env.CI;
1696
+ if (c === "true" || c === "1") return true;
1697
+ if (process.env.GITHUB_ACTIONS) return true;
1698
+ return false;
1699
+ }
1700
+
1701
+ // src/commands/auth/setup-token.ts
1702
+ import { spawn } from "child_process";
1703
+ function spawnSetupToken(binaryPath, opts = {}) {
1704
+ const tee = opts.tee !== false;
1705
+ return new Promise((resolve2, reject) => {
1706
+ const child = spawn(binaryPath, ["setup-token"], {
1707
+ stdio: tee ? ["inherit", "pipe", "inherit"] : "inherit",
1708
+ windowsHide: true
1709
+ });
1710
+ let output = "";
1711
+ if (tee && child.stdout) {
1712
+ child.stdout.setEncoding("utf8");
1713
+ child.stdout.on("data", (chunk) => {
1714
+ output += chunk;
1715
+ process.stderr.write(chunk);
1716
+ });
1717
+ }
1718
+ child.once("error", reject);
1719
+ child.once("close", (code, signal) => {
1720
+ resolve2({ code, signal, output });
1721
+ });
1722
+ });
1723
+ }
1724
+
1725
+ // src/commands/auth/shared.ts
1726
+ var OAUTH_TOKEN_PREFIX = "sk-ant-oat";
1727
+ var OAUTH_TOKEN_PATTERN = /sk-ant-oat[0-9]{2}-[A-Za-z0-9_-]{16,}/;
1728
+ var MIN_TOKEN_LENGTH = 20;
1729
+ var MAX_TOKEN_LENGTH = 2048;
1730
+ var InvalidOauthTokenError = class extends Error {
1731
+ constructor(message) {
1732
+ super(message);
1733
+ this.name = "InvalidOauthTokenError";
1734
+ }
1735
+ };
1736
+ function validateOauthToken(raw) {
1737
+ const value = raw ?? "";
1738
+ if (value.trim().length === 0) {
1739
+ throw new InvalidOauthTokenError(
1740
+ "The token is empty. Run `squad auth login` without --token to sign in through the browser, or pass the value printed by `claude setup-token`."
1741
+ );
1742
+ }
1743
+ if (/\s/.test(value.trim())) {
1744
+ throw new InvalidOauthTokenError(
1745
+ "That token contains whitespace. Paste only the single-line value printed by `claude setup-token` (quote it in your shell if needed)."
1746
+ );
1747
+ }
1748
+ const token = value.trim();
1749
+ if (token.startsWith("sk-ant-") && !token.startsWith(OAUTH_TOKEN_PREFIX)) {
1750
+ throw new InvalidOauthTokenError(
1751
+ "That looks like an Anthropic API key; use `squad config set planner` for keys. `squad auth login --token` takes the OAuth token printed by `claude setup-token`."
1752
+ );
1753
+ }
1754
+ if (token.length < MIN_TOKEN_LENGTH) {
1755
+ throw new InvalidOauthTokenError(
1756
+ `That token is only ${token.length} characters \u2014 too short to be a \`claude setup-token\` value. Re-run \`claude setup-token\` and paste the whole line.`
1757
+ );
1758
+ }
1759
+ if (token.length > MAX_TOKEN_LENGTH) {
1760
+ throw new InvalidOauthTokenError(
1761
+ "That token is implausibly long. Paste only the single line `claude setup-token` printed."
1762
+ );
1763
+ }
1764
+ return token;
1765
+ }
1766
+ function extractOauthToken(output) {
1767
+ const hit = OAUTH_TOKEN_PATTERN.exec(output);
1768
+ return hit ? hit[0] : void 0;
1769
+ }
1770
+ function authReasonText(reason, provider) {
1771
+ switch (reason) {
1772
+ case "explicit-config":
1773
+ return "explicit in .squad/config.yaml";
1774
+ case "auto-login-detected":
1775
+ return "auto \u2014 Claude login detected";
1776
+ case "auto-oauth-token":
1777
+ return "auto \u2014 stored OAuth token";
1778
+ case "auto-fallback-api-key":
1779
+ return "auto \u2014 no Claude login, fell back to an API key";
1780
+ case "provider-is-api-key-only":
1781
+ return `${provider} is API-key only`;
1782
+ }
1783
+ }
1784
+ function runtimeNameFor(provider, planner) {
1785
+ if (provider !== "anthropic") return "vercel";
1786
+ return planner?.runtime?.anthropic ?? "agent-sdk";
1787
+ }
1788
+ function credentialPresent(auth2) {
1789
+ return auth2.mode === "api-key" || auth2.loginHint !== "assumed";
1790
+ }
1791
+ var SUBSCRIPTION_ALTERNATIVE_HINT = "Prefer not to store a token? Run `claude` and use `/login` instead \u2014 squad-kit picks the OS credential store up automatically under `auth: subscription`.";
1792
+
1793
+ // src/commands/auth/login.ts
1794
+ var NON_TTY_LOGIN_MESSAGE = "A browser login needs a terminal. Generate a token elsewhere with `claude setup-token` and pass it with `squad auth login --token <value>`, or set CLAUDE_CODE_OAUTH_TOKEN.";
1795
+ async function runAuthLogin(opts = {}) {
1796
+ if (opts.token && opts.printOnly) {
1797
+ throw new Error(
1798
+ "Pass either --token or --print-only, not both. --print-only runs the browser flow and prints the token it produces; --token consumes one you already have."
1799
+ );
1800
+ }
1801
+ const providedToken = opts.token !== void 0 ? validateOauthToken(opts.token) : void 0;
1802
+ const storing = !opts.printOnly;
1803
+ const workspace = storing ? openWorkspace() : void 0;
1804
+ if (workspace && !await confirmOverwrite(workspace.secrets, Boolean(opts.yes))) return;
1805
+ if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
1806
+ warning(
1807
+ "CLAUDE_CODE_OAUTH_TOKEN is exported in this shell. It outranks the token squad-kit stores, so the stored one is ignored until you unset it."
1808
+ );
1809
+ }
1810
+ const token = providedToken ?? await runBrowserFlow();
1811
+ if (!storing) {
1812
+ blank();
1813
+ success("Login complete. Nothing was stored.");
1814
+ info("Token (copy it somewhere safe \u2014 squad-kit did not save it):");
1815
+ process.stdout.write(`${token}
1816
+ `);
1817
+ blank();
1818
+ info(SUBSCRIPTION_ALTERNATIVE_HINT);
1819
+ info("To store it in this workspace later: `squad auth login --token <value>`.");
1820
+ return;
1821
+ }
1822
+ const ws = workspace;
1823
+ saveSecrets(ws.paths.secretsFile, mergePlannerOauthTokenIntoSecrets(ws.secrets, token));
1824
+ success("OAuth token saved to .squad/secrets.yaml (chmod 0600 on POSIX).");
1825
+ const planner = ws.config.planner;
1826
+ if (planner) {
1827
+ saveConfig(ws.paths.configFile, {
1828
+ ...ws.config,
1829
+ planner: { ...planner, auth: { ...planner.auth, anthropic: "subscription" } }
1830
+ });
1831
+ info("Set `planner.auth.anthropic: subscription` in .squad/config.yaml.");
1832
+ } else {
1833
+ warning(
1834
+ "The direct planner is not configured in this workspace, so no auth mode was written. Run `squad config set planner` and choose Anthropic to finish setup."
1835
+ );
1836
+ }
1837
+ await verifyAndSummarise(planner ? { ...planner, auth: { anthropic: "subscription" } } : void 0);
1838
+ blank();
1839
+ info(SUBSCRIPTION_ALTERNATIVE_HINT);
1840
+ info("Next: `squad auth status` to confirm, then `squad new-plan --api`.");
1841
+ }
1842
+ function openWorkspace() {
1843
+ const paths = buildPaths(requireSquadRoot());
1844
+ return {
1845
+ paths,
1846
+ config: loadConfig(paths.configFile),
1847
+ secrets: fs11.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {}
1848
+ };
1849
+ }
1850
+ async function confirmOverwrite(secrets, yes) {
1851
+ if (!secrets.planner?.anthropicOauthToken) return true;
1852
+ if (yes) return true;
1853
+ if (!isInteractive()) {
1854
+ throw new Error(
1855
+ "A Claude OAuth token is already stored in .squad/secrets.yaml. Pass -y to overwrite it non-interactively."
1856
+ );
1857
+ }
1858
+ const ok = await confirm2({
1859
+ message: "A Claude OAuth token is already stored. Replace it with a new one?",
1860
+ default: false
1861
+ });
1862
+ if (!ok) info("Keeping the existing token; nothing changed.");
1863
+ return ok;
1864
+ }
1865
+ async function runBrowserFlow() {
1866
+ if (!isInteractive()) throw new Error(NON_TTY_LOGIN_MESSAGE);
1867
+ const binary = resolveClaudeBinary();
1868
+ if (!binary) throw new Error(MISSING_CLAUDE_BINARY_MESSAGE);
1869
+ step("Starting the Anthropic browser authorization flow\u2026");
1870
+ info(`Using ${binary.source === "bundled" ? "the bundled Agent SDK binary" : "the `claude` on your PATH"}.`);
1871
+ blank();
1872
+ const result = await spawnSetupToken(binary.path);
1873
+ if (result.signal) {
1874
+ throw new SquadExit(130, "Login cancelled \u2014 nothing was stored.");
1875
+ }
1876
+ if (result.code !== 0) {
1877
+ throw new SquadExit(
1878
+ result.code ?? 1,
1879
+ `\`claude setup-token\` exited with code ${result.code}. Nothing was stored. Re-run \`squad auth login\` to try again.`
1880
+ );
1881
+ }
1882
+ const extracted = extractOauthToken(result.output);
1883
+ if (extracted) return validateOauthToken(extracted);
1884
+ blank();
1885
+ warning("Could not read the token from the login output. Paste it here:");
1886
+ const pasted = await password2({
1887
+ message: "Token (input hidden):",
1888
+ validate: (v) => {
1889
+ try {
1890
+ validateOauthToken(v);
1891
+ return true;
1892
+ } catch (e) {
1893
+ return e instanceof Error ? e.message : "invalid token";
1894
+ }
1895
+ }
1896
+ });
1897
+ return validateOauthToken(pasted);
1898
+ }
1899
+ async function verifyAndSummarise(planner) {
1900
+ if (!planner || skipExternalProbesInAutomation()) return;
1901
+ const auth2 = resolvePlannerAuthForCwd("anthropic", planner);
1902
+ const spin = spinner("verifying the Claude login\u2026");
1903
+ const probe = await probeClaudeAuth(auth2);
1904
+ if (!probe.ok) {
1905
+ spin.fail(`could not verify the login (${probe.kind})`);
1906
+ warning(probe.detail);
1907
+ return;
1140
1908
  }
1141
- info("Re-enable any time with `squad config set planner`.");
1142
- }
1143
-
1144
- // src/core/ci-env.ts
1145
- function skipExternalProbesInAutomation() {
1146
- const c = process.env.CI;
1147
- if (c === "true" || c === "1") return true;
1148
- if (process.env.GITHUB_ACTIONS) return true;
1149
- return false;
1909
+ if (probe.unverifiable) {
1910
+ spin.succeed("login stored");
1911
+ if (probe.credentialSource) kv("credential", probe.credentialSource, 15);
1912
+ info(probe.unverifiable);
1913
+ return;
1914
+ }
1915
+ spin.succeed("login verified");
1916
+ const account = [probe.account?.email, probe.account?.organization, probe.account?.subscriptionType].filter(Boolean).join(" \xB7 ");
1917
+ if (account) kv("account", account, 15);
1918
+ if (probe.apiKeySource) kv("api key source", probe.apiKeySource, 15);
1150
1919
  }
1151
1920
 
1152
1921
  // src/commands/config/set-planner.ts
@@ -1168,7 +1937,7 @@ async function runConfigSetPlanner(opts = {}) {
1168
1937
  const root = requireSquadRoot();
1169
1938
  const paths = buildPaths(root);
1170
1939
  const config2 = loadConfig(paths.configFile);
1171
- const baseSecrets = fs9.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
1940
+ const baseSecrets = fs12.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
1172
1941
  const useYes = Boolean(opts.yes);
1173
1942
  const interactive = !useYes && isInteractive();
1174
1943
  if (interactive && config2.planner?.enabled === true) {
@@ -1205,7 +1974,7 @@ async function runConfigSetPlanner(opts = {}) {
1205
1974
  }
1206
1975
  if (interactive) {
1207
1976
  if (!config2.planner || config2.planner.enabled !== true) {
1208
- const enable = await confirm2({ message: "Enable the direct planner?", default: true });
1977
+ const enable = await confirm3({ message: "Enable the direct planner?", default: true });
1209
1978
  if (!enable) {
1210
1979
  return;
1211
1980
  }
@@ -1218,42 +1987,45 @@ async function runConfigSetPlanner(opts = {}) {
1218
1987
  } else {
1219
1988
  nextPlanner = { ...prev, provider, enabled: true };
1220
1989
  }
1990
+ let authChoice;
1991
+ if (interactive && provider === "anthropic") {
1992
+ authChoice = await select({
1993
+ message: "How should squad-kit authenticate with Anthropic?",
1994
+ choices: [
1995
+ {
1996
+ name: "Use my Claude subscription (browser login) \u2014 recommended, no API key needed",
1997
+ value: "subscription"
1998
+ },
1999
+ { name: "Use an Anthropic API key", value: "api-key" },
2000
+ {
2001
+ name: "Decide automatically (subscription if signed in, otherwise API key)",
2002
+ value: "auto"
2003
+ }
2004
+ ],
2005
+ default: "subscription"
2006
+ });
2007
+ nextPlanner = { ...nextPlanner, auth: { ...nextPlanner.auth, anthropic: authChoice } };
2008
+ }
1221
2009
  if (useYes) {
1222
2010
  if (!resolveProviderKey(provider)) {
1223
2011
  throw credentialError(provider);
1224
2012
  }
1225
2013
  } else {
1226
- const existing = resolveProviderKey(provider);
1227
- if (existing) {
1228
- const shouldUpdate = await confirm2({
1229
- message: "A planner credential is already available. Enter a new key and save it to .squad/secrets.yaml?",
1230
- default: false
1231
- });
1232
- if (shouldUpdate) {
1233
- const envVar = providerEnvVar(provider);
1234
- const key = await password2({
1235
- message: `${envVar} value (input hidden):`,
1236
- validate: (v) => v.length >= 20 ? true : "key looks too short"
1237
- });
1238
- const merged = mergePlannerKeyIntoSecrets(baseSecrets, provider, key);
1239
- saveSecrets(paths.secretsFile, merged);
1240
- success("Planner key saved to .squad/secrets.yaml");
1241
- info(".squad/secrets.yaml updated (chmod 0600 on POSIX)");
1242
- } else {
1243
- info("Keeping the existing planner credential; no change to .squad/secrets.yaml for the key.");
2014
+ if (authChoice === "subscription") {
2015
+ await offerBrowserLogin(baseSecrets);
2016
+ } else if (authChoice === "auto") {
2017
+ info(
2018
+ "`auto` resolves in this order: a detected Claude login first, then a resolvable API key (ANTHROPIC_API_KEY, SQUAD_PLANNER_API_KEY, or .squad/secrets.yaml)."
2019
+ );
2020
+ if (!detectClaudeLogin(baseSecrets).present && !resolveProviderKey(provider)) {
2021
+ warning(
2022
+ "Neither a Claude login nor an API key resolves right now. Run `squad auth login`, or re-run `squad config set planner` and choose the API-key option."
2023
+ );
1244
2024
  }
1245
2025
  } else {
1246
- const envVar = providerEnvVar(provider);
1247
- const key = await password2({
1248
- message: `${envVar} value (input hidden) \u2014 required:`,
1249
- validate: (v) => v.length >= 20 ? true : "key looks too short"
1250
- });
1251
- const merged = mergePlannerKeyIntoSecrets(baseSecrets, provider, key);
1252
- saveSecrets(paths.secretsFile, merged);
1253
- success("Planner key saved to .squad/secrets.yaml");
1254
- info(".squad/secrets.yaml updated (chmod 0600 on POSIX)");
2026
+ await promptForApiKey(provider, paths.secretsFile, baseSecrets);
1255
2027
  }
1256
- const cacheEnabled = await confirm2({
2028
+ const cacheEnabled = await confirm3({
1257
2029
  message: "Enable prompt caching? (Recommended \u2014 reduces billed tokens by ~70% on most providers.)",
1258
2030
  default: nextPlanner.cache?.enabled ?? true
1259
2031
  });
@@ -1261,6 +2033,12 @@ async function runConfigSetPlanner(opts = {}) {
1261
2033
  }
1262
2034
  const next = { ...config2, planner: nextPlanner };
1263
2035
  saveConfig(paths.configFile, next);
2036
+ const auth2 = interactive && provider === "anthropic" ? tryResolveAuth(provider, nextPlanner) : void 0;
2037
+ const configuredMode = nextPlanner.auth?.anthropic;
2038
+ if (auth2?.mode === "subscription" && (configuredMode === "subscription" || configuredMode === "auto")) {
2039
+ await reportSubscriptionSetup(provider, nextPlanner, auth2);
2040
+ return;
2041
+ }
1264
2042
  const cred = resolveProviderKey(provider);
1265
2043
  if (!cred) {
1266
2044
  warning(
@@ -1278,7 +2056,7 @@ async function runConfigSetPlanner(opts = {}) {
1278
2056
  kv("credential", `${cred.source} (${sourceText})`, 10);
1279
2057
  }
1280
2058
  if (skipExternalProbesInAutomation() || !cred) {
1281
- printPlannerNextSteps(Boolean(cred));
2059
+ printPlannerNextSteps(Boolean(cred), "api-key");
1282
2060
  return;
1283
2061
  }
1284
2062
  try {
@@ -1299,11 +2077,90 @@ async function runConfigSetPlanner(opts = {}) {
1299
2077
  `Could not reach provider model list (${msg.slice(0, 160)}). Key was saved; run \`squad doctor\` to verify.`
1300
2078
  );
1301
2079
  }
1302
- printPlannerNextSteps(true);
2080
+ printPlannerNextSteps(true, "api-key");
2081
+ }
2082
+ async function promptForApiKey(provider, secretsFile, baseSecrets) {
2083
+ const envVar = providerEnvVar(provider);
2084
+ const existing = resolveProviderKey(provider);
2085
+ if (existing) {
2086
+ const shouldUpdate = await confirm3({
2087
+ message: "A planner credential is already available. Enter a new key and save it to .squad/secrets.yaml?",
2088
+ default: false
2089
+ });
2090
+ if (!shouldUpdate) {
2091
+ info("Keeping the existing planner credential; no change to .squad/secrets.yaml for the key.");
2092
+ return;
2093
+ }
2094
+ const key2 = await password3({
2095
+ message: `${envVar} value (input hidden):`,
2096
+ validate: (v) => v.length >= 20 ? true : "key looks too short"
2097
+ });
2098
+ saveSecrets(secretsFile, mergePlannerKeyIntoSecrets(baseSecrets, provider, key2));
2099
+ success("Planner key saved to .squad/secrets.yaml");
2100
+ info(".squad/secrets.yaml updated (chmod 0600 on POSIX)");
2101
+ return;
2102
+ }
2103
+ const key = await password3({
2104
+ message: `${envVar} value (input hidden) \u2014 required:`,
2105
+ validate: (v) => v.length >= 20 ? true : "key looks too short"
2106
+ });
2107
+ saveSecrets(secretsFile, mergePlannerKeyIntoSecrets(baseSecrets, provider, key));
2108
+ success("Planner key saved to .squad/secrets.yaml");
2109
+ info(".squad/secrets.yaml updated (chmod 0600 on POSIX)");
2110
+ }
2111
+ async function offerBrowserLogin(baseSecrets) {
2112
+ if (detectClaudeLogin(baseSecrets).present) {
2113
+ info("A Claude login is already available on this machine \u2014 no API key needed.");
2114
+ return;
2115
+ }
2116
+ const now = await confirm3({
2117
+ message: "No Claude login found. Run the browser login now?",
2118
+ default: true
2119
+ });
2120
+ if (!now) {
2121
+ info("Skipped. Run `squad auth login` when you are ready \u2014 planning needs it in subscription mode.");
2122
+ return;
2123
+ }
2124
+ await runAuthLogin({});
2125
+ }
2126
+ function tryResolveAuth(provider, planner) {
2127
+ try {
2128
+ return resolvePlannerAuthForCwd(provider, planner);
2129
+ } catch {
2130
+ return void 0;
2131
+ }
2132
+ }
2133
+ async function reportSubscriptionSetup(provider, planner, auth2) {
2134
+ blank();
2135
+ success("Planner configuration updated.");
2136
+ kv("provider", provider, 10);
2137
+ kv("model (plan)", modelFor(provider, "plan", planner.modelOverride), 10);
2138
+ kv("model (execute)", modelFor(provider, "execute", planner.modelOverride), 10);
2139
+ kv("auth", `subscription (${describeAuth(auth2).credentialHint})`, 10);
2140
+ if (!skipExternalProbesInAutomation()) {
2141
+ const probe = await probeClaudeAuth(auth2);
2142
+ if (probe.ok) {
2143
+ const account = [probe.account?.email, probe.account?.organization, probe.account?.subscriptionType].filter(Boolean).join(" \xB7 ");
2144
+ info(account ? `Credential check: signed in as ${account}.` : "Credential check: the Claude login responded OK.");
2145
+ } else {
2146
+ warning(`Credential check failed (${probe.kind}). ${probe.detail}`);
2147
+ }
2148
+ }
2149
+ printPlannerNextSteps(true, "subscription");
1303
2150
  }
1304
- function printPlannerNextSteps(credentialReady) {
2151
+ function printPlannerNextSteps(credentialReady, mode) {
1305
2152
  blank();
1306
2153
  step("Next:");
2154
+ if (mode === "subscription") {
2155
+ info("1) Confirm the login with `squad auth status` \u2014 it shows the account and how it resolved.");
2156
+ info("2) Verify with `squad doctor` \u2014 every planner check should be green.");
2157
+ info("3) Create a story: squad new-story <feature-slug> (or --no-tracker for a manual story).");
2158
+ info("4) Fill the generated intake.md, then run `squad new-plan --api` to generate the plan.");
2159
+ info(
2160
+ " Planning draws on your Claude usage limits \u2014 there is no per-token API bill. To switch to an API key later, re-run `squad config set planner`."
2161
+ );
2162
+ return;
2163
+ }
1307
2164
  if (!credentialReady) {
1308
2165
  info("1) Save a planner key: re-run `squad config set planner` and paste the key when prompted.");
1309
2166
  info("2) Verify with `squad doctor` \u2014 all planner checks should turn green.");
@@ -1319,7 +2176,7 @@ function printPlannerNextSteps(credentialReady) {
1319
2176
  }
1320
2177
 
1321
2178
  // src/commands/config/set-tracker.ts
1322
- import fs11 from "fs";
2179
+ import fs14 from "fs";
1323
2180
  import { select as select2 } from "@inquirer/prompts";
1324
2181
 
1325
2182
  // src/tracker/types.ts
@@ -1335,13 +2192,13 @@ var TrackerError = class extends Error {
1335
2192
  };
1336
2193
 
1337
2194
  // src/tracker/attachments.ts
1338
- import fs10 from "fs";
1339
- import path5 from "path";
2195
+ import fs13 from "fs";
2196
+ import path6 from "path";
1340
2197
  var DEFAULT_MAX_MB = 10;
1341
2198
  async function downloadAttachmentsWith(refs, targetDir, authHeaders, opts = {}) {
1342
2199
  const maxMb = opts.maxMegabytes ?? DEFAULT_MAX_MB;
1343
2200
  const maxBytes = maxMb * 1024 * 1024;
1344
- fs10.mkdirSync(targetDir, { recursive: true });
2201
+ fs13.mkdirSync(targetDir, { recursive: true });
1345
2202
  const out = [];
1346
2203
  for (const ref of refs) {
1347
2204
  out.push(await one(ref, targetDir, authHeaders, maxBytes, maxMb));
@@ -1387,7 +2244,7 @@ async function one(ref, targetDir, headers, maxBytes, maxMb) {
1387
2244
  skipReason: `${formatMb(buf.byteLength)} after download exceeds cap (${maxMb} MB)`
1388
2245
  };
1389
2246
  }
1390
- fs10.writeFileSync(dest, buf);
2247
+ fs13.writeFileSync(dest, buf);
1391
2248
  return {
1392
2249
  source: ref,
1393
2250
  outcome: "written",
@@ -1397,19 +2254,19 @@ async function one(ref, targetDir, headers, maxBytes, maxMb) {
1397
2254
  }
1398
2255
  function sanitizeFilename(raw) {
1399
2256
  const normalized = raw.replace(/\\/g, "/");
1400
- const base = path5.basename(normalized).replace(/[\\/]/g, "_");
2257
+ const base = path6.basename(normalized).replace(/[\\/]/g, "_");
1401
2258
  const cleaned = base.replace(/[\x00-\x1f]/g, "").replace(/^\.+/, "").replace(/\s+/g, " ").trim();
1402
2259
  if (cleaned.length === 0) return "attachment";
1403
2260
  return cleaned.slice(0, 180);
1404
2261
  }
1405
2262
  function uniqueDestination(dir, name) {
1406
- let candidate = path5.join(dir, name);
1407
- if (!fs10.existsSync(candidate)) return candidate;
1408
- const ext = path5.extname(name);
2263
+ let candidate = path6.join(dir, name);
2264
+ if (!fs13.existsSync(candidate)) return candidate;
2265
+ const ext = path6.extname(name);
1409
2266
  const stem = name.slice(0, name.length - ext.length);
1410
2267
  let n = 1;
1411
- while (fs10.existsSync(candidate) && n < 1e3) {
1412
- candidate = path5.join(dir, `${stem} (${n})${ext}`);
2268
+ while (fs13.existsSync(candidate) && n < 1e3) {
2269
+ candidate = path6.join(dir, `${stem} (${n})${ext}`);
1413
2270
  n += 1;
1414
2271
  }
1415
2272
  return candidate;
@@ -2023,7 +2880,7 @@ async function runConfigSetTracker(opts = {}) {
2023
2880
  const root = requireSquadRoot();
2024
2881
  const paths = buildPaths(root);
2025
2882
  const config2 = loadConfig(paths.configFile);
2026
- const baseSecrets = fs11.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
2883
+ const baseSecrets = fs14.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
2027
2884
  const useYes = Boolean(opts.yes);
2028
2885
  const interactive = !useYes && isInteractive();
2029
2886
  let type;
@@ -2146,7 +3003,7 @@ async function runConfigSetTracker(opts = {}) {
2146
3003
  return;
2147
3004
  }
2148
3005
  const reloaded = loadConfig(paths.configFile);
2149
- const s = fs11.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
3006
+ const s = fs14.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
2150
3007
  if (reloaded.tracker.type === "jira") {
2151
3008
  const r = await probeJiraConnectivity(s, reloaded);
2152
3009
  if (r.ok) {
@@ -2193,8 +3050,8 @@ function printTrackerNextSteps(type) {
2193
3050
  }
2194
3051
 
2195
3052
  // src/commands/config/unset-tracker.ts
2196
- import fs12 from "fs";
2197
- import { confirm as confirm3 } from "@inquirer/prompts";
3053
+ import fs15 from "fs";
3054
+ import { confirm as confirm4 } from "@inquirer/prompts";
2198
3055
  async function runConfigUnsetTracker(opts = {}) {
2199
3056
  const root = requireSquadRoot();
2200
3057
  const paths = buildPaths(root);
@@ -2205,7 +3062,7 @@ async function runConfigUnsetTracker(opts = {}) {
2205
3062
  warning("Pass -y in non-interactive mode to confirm setting the tracker to none.");
2206
3063
  return;
2207
3064
  }
2208
- const ok = await confirm3({ message: "Set tracker to none? (keeps tracker tokens in .squad/secrets.yaml.)", default: false });
3065
+ const ok = await confirm4({ message: "Set tracker to none? (keeps tracker tokens in .squad/secrets.yaml.)", default: false });
2209
3066
  if (!ok) {
2210
3067
  return;
2211
3068
  }
@@ -2213,7 +3070,7 @@ async function runConfigUnsetTracker(opts = {}) {
2213
3070
  const next = { ...config2, tracker: { type: "none" } };
2214
3071
  saveConfig(paths.configFile, next);
2215
3072
  if (opts.removeCredentials) {
2216
- const s = fs12.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
3073
+ const s = fs15.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
2217
3074
  if (s.tracker) {
2218
3075
  delete s.tracker;
2219
3076
  saveSecrets(paths.secretsFile, s);
@@ -2226,8 +3083,8 @@ async function runConfigUnsetTracker(opts = {}) {
2226
3083
  }
2227
3084
 
2228
3085
  // src/commands/config/remove-credential.ts
2229
- import fs13 from "fs";
2230
- import { confirm as confirm4 } from "@inquirer/prompts";
3086
+ import fs16 from "fs";
3087
+ import { confirm as confirm5 } from "@inquirer/prompts";
2231
3088
  async function runConfigRemoveCredential(section, opts = {}) {
2232
3089
  if (section !== "planner" && section !== "tracker") {
2233
3090
  throw new Error(
@@ -2242,7 +3099,7 @@ async function runConfigRemoveCredential(section, opts = {}) {
2242
3099
  warning("Pass -y in non-interactive mode to confirm, or run `squad config remove-credential` in a TTY.");
2243
3100
  return;
2244
3101
  }
2245
- const ok = await confirm4({
3102
+ const ok = await confirm5({
2246
3103
  message: `Delete all ${section} credentials from .squad/secrets.yaml? (config is unchanged)`,
2247
3104
  default: false
2248
3105
  });
@@ -2250,7 +3107,7 @@ async function runConfigRemoveCredential(section, opts = {}) {
2250
3107
  return;
2251
3108
  }
2252
3109
  }
2253
- const s = fs13.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
3110
+ const s = fs16.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
2254
3111
  if (section === "planner") {
2255
3112
  if (s.planner) {
2256
3113
  delete s.planner;
@@ -2268,10 +3125,10 @@ async function runConfigRemoveCredential(section, opts = {}) {
2268
3125
  // src/commands/init.ts
2269
3126
  var SUPPORTED_AGENTS = ["claude-code", "cursor", "copilot", "gemini"];
2270
3127
  var AGENT_INSTALL = {
2271
- "claude-code": { subdir: path6.join(".claude", "commands"), srcDir: "claude-code" },
2272
- cursor: { subdir: path6.join(".cursor", "commands"), srcDir: "cursor" },
2273
- copilot: { subdir: path6.join(".github", "prompts"), srcDir: "copilot" },
2274
- gemini: { subdir: path6.join(".gemini", "commands"), srcDir: "gemini" }
3128
+ "claude-code": { subdir: path7.join(".claude", "commands"), srcDir: "claude-code" },
3129
+ cursor: { subdir: path7.join(".cursor", "commands"), srcDir: "cursor" },
3130
+ copilot: { subdir: path7.join(".github", "prompts"), srcDir: "copilot" },
3131
+ gemini: { subdir: path7.join(".gemini", "commands"), srcDir: "gemini" }
2275
3132
  };
2276
3133
  async function runInit(opts) {
2277
3134
  const root = process.cwd();
@@ -2279,7 +3136,7 @@ async function runInit(opts) {
2279
3136
  if (!opts.yes) {
2280
3137
  banner();
2281
3138
  }
2282
- if (fs14.existsSync(paths.configFile) && !opts.force) {
3139
+ if (fs17.existsSync(paths.configFile) && !opts.force) {
2283
3140
  if (!isInteractive() || opts.yes) {
2284
3141
  warning(
2285
3142
  `A ${SQUAD_DIR}/config.yaml already exists. Run \`squad init --force\` to overwrite, or \`squad config set planner\` / \`squad config set tracker\` to change individual sections.`
@@ -2306,7 +3163,7 @@ async function runInit(opts) {
2306
3163
  }
2307
3164
  }
2308
3165
  const defaults = {
2309
- name: opts.name ?? path6.basename(root),
3166
+ name: opts.name ?? path7.basename(root),
2310
3167
  language: opts.language ?? "typescript",
2311
3168
  tracker: opts.tracker ?? "none",
2312
3169
  agents: parseAgentsFlag(opts.agents)
@@ -2382,11 +3239,11 @@ async function runInit(opts) {
2382
3239
  };
2383
3240
  }
2384
3241
  } else {
2385
- const plannerEnabled = await confirm5({
3242
+ const plannerEnabled2 = await confirm6({
2386
3243
  message: "Enable automatic plan generation? (press Enter to skip \u2014 you can always edit .squad/config.yaml later)",
2387
3244
  default: false
2388
3245
  });
2389
- if (plannerEnabled) {
3246
+ if (plannerEnabled2) {
2390
3247
  const provider = await select3({
2391
3248
  message: "Planner provider:",
2392
3249
  choices: [
@@ -2411,29 +3268,60 @@ async function runInit(opts) {
2411
3268
  }
2412
3269
  }
2413
3270
  if (plannerBlock?.enabled && allowSecretPrompts) {
2414
- const envVar = providerEnvVar(plannerBlock.provider);
2415
- const envPresent = Boolean(process.env[envVar]);
2416
- if (envPresent) {
2417
- info(`Planner key detected in ${envVar}; keeping env-var resolution as the primary source.`);
2418
- } else {
2419
- const storeKey = await select3({
2420
- message: `No ${envVar} in your environment. How do you want to store your planner API key?`,
3271
+ let needsApiKey = true;
3272
+ if (plannerBlock.provider === "anthropic") {
3273
+ const authMode = await select3({
3274
+ message: "How should squad-kit authenticate with Anthropic?",
2421
3275
  choices: [
2422
- { name: `Enter it now and save to .squad/secrets.yaml (git-ignored)`, value: "secrets" },
2423
- { name: `I'll export ${envVar} in my shell profile myself`, value: "env" }
2424
- ]
3276
+ {
3277
+ name: "Use my Claude subscription (browser login) \u2014 recommended, no API key needed",
3278
+ value: "subscription"
3279
+ },
3280
+ { name: "Use an Anthropic API key", value: "api-key" },
3281
+ {
3282
+ name: "Decide automatically (subscription if signed in, otherwise API key)",
3283
+ value: "auto"
3284
+ }
3285
+ ],
3286
+ default: "subscription"
2425
3287
  });
2426
- if (storeKey === "secrets") {
2427
- const key = await password3({
2428
- message: `${envVar} value (input hidden):`,
2429
- validate: (v) => v.length >= 20 ? true : "key looks too short"
2430
- });
2431
- const base = loadSecrets(paths.secretsFile);
2432
- const merged = mergePlannerKeyIntoSecrets(base, plannerBlock.provider, key);
2433
- saveSecrets(paths.secretsFile, merged);
2434
- success("Planner key saved to .squad/secrets.yaml");
3288
+ plannerBlock = { ...plannerBlock, auth: { anthropic: authMode } };
3289
+ needsApiKey = authMode === "api-key";
3290
+ if (authMode === "subscription") {
3291
+ info("No API key needed. Run `squad auth login` after init to sign in with your Claude account.");
3292
+ info("Already signed in through Claude Code (`claude` then `/login`)? squad-kit picks that up automatically.");
3293
+ } else if (authMode === "auto") {
3294
+ info(
3295
+ "`auto` resolves in this order: a detected Claude login first, then a resolvable API key (ANTHROPIC_API_KEY, SQUAD_PLANNER_API_KEY, or .squad/secrets.yaml)."
3296
+ );
3297
+ info("Run `squad auth login` for the subscription, or `squad config set planner` to save a key.");
3298
+ }
3299
+ }
3300
+ if (needsApiKey) {
3301
+ const envVar = providerEnvVar(plannerBlock.provider);
3302
+ const envPresent = Boolean(process.env[envVar]);
3303
+ if (envPresent) {
3304
+ info(`Planner key detected in ${envVar}; keeping env-var resolution as the primary source.`);
2435
3305
  } else {
2436
- info(`Remember to export ${envVar} before running \`squad new-plan\`.`);
3306
+ const storeKey = await select3({
3307
+ message: `No ${envVar} in your environment. How do you want to store your planner API key?`,
3308
+ choices: [
3309
+ { name: `Enter it now and save to .squad/secrets.yaml (git-ignored)`, value: "secrets" },
3310
+ { name: `I'll export ${envVar} in my shell profile myself`, value: "env" }
3311
+ ]
3312
+ });
3313
+ if (storeKey === "secrets") {
3314
+ const key = await password4({
3315
+ message: `${envVar} value (input hidden):`,
3316
+ validate: (v) => v.length >= 20 ? true : "key looks too short"
3317
+ });
3318
+ const base = loadSecrets(paths.secretsFile);
3319
+ const merged = mergePlannerKeyIntoSecrets(base, plannerBlock.provider, key);
3320
+ saveSecrets(paths.secretsFile, merged);
3321
+ success("Planner key saved to .squad/secrets.yaml");
3322
+ } else {
3323
+ info(`Remember to export ${envVar} before running \`squad new-plan\`.`);
3324
+ }
2437
3325
  }
2438
3326
  }
2439
3327
  }
@@ -2450,12 +3338,12 @@ async function runInit(opts) {
2450
3338
  agents: answers.agents,
2451
3339
  planner: plannerBlock
2452
3340
  };
2453
- fs14.mkdirSync(paths.squadDir, { recursive: true });
2454
- fs14.mkdirSync(paths.storiesDir, { recursive: true });
2455
- fs14.mkdirSync(paths.plansDir, { recursive: true });
3341
+ fs17.mkdirSync(paths.squadDir, { recursive: true });
3342
+ fs17.mkdirSync(paths.storiesDir, { recursive: true });
3343
+ fs17.mkdirSync(paths.plansDir, { recursive: true });
2456
3344
  writeFileSafe(
2457
3345
  paths.indexFile,
2458
- readFile(path6.join(templatesDir(), "index.md")),
3346
+ readFile(path7.join(templatesDir(), "index.md")),
2459
3347
  !!opts.force
2460
3348
  );
2461
3349
  saveConfig(paths.configFile, config2);
@@ -2463,7 +3351,7 @@ async function runInit(opts) {
2463
3351
  info("Updated .gitignore with squad-managed patterns (e.g. .squad/secrets.yaml).");
2464
3352
  }
2465
3353
  writeFileSafe(
2466
- path6.join(paths.squadDir, "README.md"),
3354
+ path7.join(paths.squadDir, "README.md"),
2467
3355
  renderSquadReadme(config2),
2468
3356
  !!opts.force
2469
3357
  );
@@ -2480,8 +3368,13 @@ async function runInit(opts) {
2480
3368
  const model = modelFor(plannerBlock.provider, "plan", plannerBlock.modelOverride);
2481
3369
  const overrideNote = plannerBlock.modelOverride?.[plannerBlock.provider] ? " (override)" : "";
2482
3370
  kv("planner", `${plannerBlock.provider}/${model}${overrideNote}`);
2483
- kv("key", hasKey ? `\u2713 (${envVar} or .squad/secrets.yaml)` : `missing (set ${envVar})`);
2484
- if (!hasKey) {
3371
+ const subscriptionMode = plannerBlock.auth?.anthropic === "subscription" && plannerBlock.provider === "anthropic";
3372
+ if (subscriptionMode) {
3373
+ kv("auth", "subscription (Claude login) \u2014 run `squad auth login` if you have not signed in");
3374
+ } else {
3375
+ kv("key", hasKey ? `\u2713 (${envVar} or .squad/secrets.yaml)` : `missing (set ${envVar})`);
3376
+ }
3377
+ if (!hasKey && !subscriptionMode) {
2485
3378
  blank();
2486
3379
  warning("Planner key not found in environment or .squad/secrets.yaml.");
2487
3380
  info("Set one of these before running `squad new-plan`:");
@@ -2503,13 +3396,13 @@ function parseAgentsFlag(flag) {
2503
3396
  }
2504
3397
  async function confirmSafe(message, defaultValue, nonInteractive) {
2505
3398
  if (nonInteractive) return defaultValue;
2506
- return confirm5({ message, default: defaultValue });
3399
+ return confirm6({ message, default: defaultValue });
2507
3400
  }
2508
3401
  function installAgent(root, agent, overwrite) {
2509
3402
  const config2 = AGENT_INSTALL[agent];
2510
- const src = path6.join(templatesDir(), "agents", config2.srcDir);
2511
- const dest = path6.join(root, config2.subdir);
2512
- if (!fs14.existsSync(src)) return;
3403
+ const src = path7.join(templatesDir(), "agents", config2.srcDir);
3404
+ const dest = path7.join(root, config2.subdir);
3405
+ if (!fs17.existsSync(src)) return;
2513
3406
  copyTree(src, dest, overwrite);
2514
3407
  }
2515
3408
  function renderSquadReadme(config2) {
@@ -2535,22 +3428,22 @@ Plan meta-prompts (\`generate-plan.md\`, \`story-skeleton.md\`) ship inside the
2535
3428
  }
2536
3429
 
2537
3430
  // src/commands/new-story.ts
2538
- import fs20 from "fs";
2539
- import path12 from "path";
3431
+ import fs23 from "fs";
3432
+ import path13 from "path";
2540
3433
  import { input as input3, select as select5 } from "@inquirer/prompts";
2541
3434
 
2542
3435
  // src/core/stories.ts
2543
- import fs15 from "fs";
2544
- import path7 from "path";
3436
+ import fs18 from "fs";
3437
+ import path8 from "path";
2545
3438
  var STORY_FILE = /^(\d{2,})-story-.+\.md$/;
2546
3439
  function listFeatures(paths) {
2547
- if (!fs15.existsSync(paths.storiesDir)) return [];
2548
- return fs15.readdirSync(paths.storiesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
3440
+ if (!fs18.existsSync(paths.storiesDir)) return [];
3441
+ return fs18.readdirSync(paths.storiesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
2549
3442
  }
2550
3443
  function listPlanFeatureNames(paths) {
2551
3444
  const names = new Set(listFeatures(paths));
2552
- if (fs15.existsSync(paths.plansDir)) {
2553
- for (const e of fs15.readdirSync(paths.plansDir, { withFileTypes: true })) {
3445
+ if (fs18.existsSync(paths.plansDir)) {
3446
+ for (const e of fs18.readdirSync(paths.plansDir, { withFileTypes: true })) {
2554
3447
  if (e.isDirectory()) names.add(e.name);
2555
3448
  }
2556
3449
  }
@@ -2561,16 +3454,16 @@ function listAllStories(paths) {
2561
3454
  }
2562
3455
  function listStories(paths, opts = {}) {
2563
3456
  const rows = [];
2564
- if (!fs15.existsSync(paths.storiesDir)) return rows;
2565
- for (const feature of fs15.readdirSync(paths.storiesDir, { withFileTypes: true })) {
3457
+ if (!fs18.existsSync(paths.storiesDir)) return rows;
3458
+ for (const feature of fs18.readdirSync(paths.storiesDir, { withFileTypes: true })) {
2566
3459
  if (!feature.isDirectory()) continue;
2567
3460
  if (opts.feature && feature.name !== opts.feature) continue;
2568
- const featureDir = path7.join(paths.storiesDir, feature.name);
2569
- for (const story of fs15.readdirSync(featureDir, { withFileTypes: true })) {
3461
+ const featureDir = path8.join(paths.storiesDir, feature.name);
3462
+ for (const story of fs18.readdirSync(featureDir, { withFileTypes: true })) {
2570
3463
  if (!story.isDirectory()) continue;
2571
- const storyDir = path7.join(featureDir, story.name);
2572
- const intakePath = path7.join(storyDir, "intake.md");
2573
- if (!fs15.existsSync(intakePath)) continue;
3464
+ const storyDir = path8.join(featureDir, story.name);
3465
+ const intakePath = path8.join(storyDir, "intake.md");
3466
+ if (!fs18.existsSync(intakePath)) continue;
2574
3467
  rows.push({
2575
3468
  feature: feature.name,
2576
3469
  id: story.name,
@@ -2584,10 +3477,10 @@ function listStories(paths, opts = {}) {
2584
3477
  return rows;
2585
3478
  }
2586
3479
  function findPlanFor(plansDir, feature, storyId) {
2587
- const featureDir = path7.join(plansDir, feature);
2588
- if (!fs15.existsSync(featureDir)) return void 0;
3480
+ const featureDir = path8.join(plansDir, feature);
3481
+ if (!fs18.existsSync(featureDir)) return void 0;
2589
3482
  const candidates = [];
2590
- for (const entry of fs15.readdirSync(featureDir)) {
3483
+ for (const entry of fs18.readdirSync(featureDir)) {
2591
3484
  if (!STORY_FILE.test(entry)) continue;
2592
3485
  if (entry.includes(storyId)) candidates.push(entry);
2593
3486
  }
@@ -2600,16 +3493,16 @@ function findStoryByIntake(records, intakePath) {
2600
3493
  return records.find((r) => normalizeIntakePath(r.intakePath) === target);
2601
3494
  }
2602
3495
  function normalizeIntakePath(p) {
2603
- const resolved = path7.resolve(p);
3496
+ const resolved = path8.resolve(p);
2604
3497
  try {
2605
- return fs15.realpathSync(resolved);
3498
+ return fs18.realpathSync(resolved);
2606
3499
  } catch {
2607
3500
  return resolved;
2608
3501
  }
2609
3502
  }
2610
3503
  function readTitleHint(intakePath) {
2611
3504
  try {
2612
- const raw = fs15.readFileSync(intakePath, "utf8");
3505
+ const raw = fs18.readFileSync(intakePath, "utf8");
2613
3506
  const head = raw.split("\n").slice(0, 30).join("\n");
2614
3507
  const hintLine = head.match(/>\s*\*\*Title hint \(from CLI\):\*\*\s*(.+?)\s*$/m);
2615
3508
  const fromHint = hintLine?.[1]?.trim();
@@ -2646,7 +3539,7 @@ function parsePlanMetadataLine(line2) {
2646
3539
  }
2647
3540
  function readPlanMetadata(planFileAbsPath) {
2648
3541
  try {
2649
- const raw = fs15.readFileSync(planFileAbsPath, "utf8").replace(/^\uFEFF/, "");
3542
+ const raw = fs18.readFileSync(planFileAbsPath, "utf8").replace(/^\uFEFF/, "");
2650
3543
  const lines = raw.split(/\n/);
2651
3544
  const scan = Math.min(lines.length, 40);
2652
3545
  for (let i = 0; i < scan; i++) {
@@ -2678,8 +3571,8 @@ function trackerIdForFilename(type, id) {
2678
3571
  }
2679
3572
 
2680
3573
  // src/core/story-intake-helpers.ts
2681
- import fs16 from "fs";
2682
- import path8 from "path";
3574
+ import fs19 from "fs";
3575
+ import path9 from "path";
2683
3576
  function buildIntakeTemplateVars(args) {
2684
3577
  const e = escapeTemplateValue;
2685
3578
  const { config: config2, featureSlug, storyFolderName, trackerId, title, fetchedIssue } = args;
@@ -2735,7 +3628,7 @@ function renderAttachmentsTable(downloads) {
2735
3628
  const size = `${sizeNum | 0} KB`;
2736
3629
  switch (d.outcome) {
2737
3630
  case "written":
2738
- return `| \`attachments/${path8.basename(d.absolutePath)}\` | ${size} | downloaded |`;
3631
+ return `| \`attachments/${path9.basename(d.absolutePath)}\` | ${size} | downloaded |`;
2739
3632
  case "skipped-oversize":
2740
3633
  return `| ${d.source.filename} | ${size} | **skipped** \u2014 ${d.skipReason} |`;
2741
3634
  case "skipped-error":
@@ -2764,34 +3657,34 @@ function buildSkippedTrackerPreamble(trackerType) {
2764
3657
  ].join("\n");
2765
3658
  }
2766
3659
  function ensureFeatureOverview(featurePlanDir, slug) {
2767
- fs16.mkdirSync(featurePlanDir, { recursive: true });
2768
- const overviewFile = path8.join(featurePlanDir, "00-overview.md");
2769
- if (fs16.existsSync(overviewFile)) return;
2770
- const template = readFile(path8.join(templatesDir(), "overview.md"));
3660
+ fs19.mkdirSync(featurePlanDir, { recursive: true });
3661
+ const overviewFile = path9.join(featurePlanDir, "00-overview.md");
3662
+ if (fs19.existsSync(overviewFile)) return;
3663
+ const template = readFile(path9.join(templatesDir(), "overview.md"));
2771
3664
  writeFileSafe(overviewFile, render(template, { featureSlug: slug }), false);
2772
3665
  }
2773
3666
 
2774
3667
  // src/core/story-mutations.ts
2775
- import fs19 from "fs";
2776
- import path11 from "path";
3668
+ import fs22 from "fs";
3669
+ import path12 from "path";
2777
3670
 
2778
3671
  // src/commands/rm/shared.ts
2779
- import fs18 from "fs";
2780
- import path10 from "path";
3672
+ import fs21 from "fs";
3673
+ import path11 from "path";
2781
3674
  import { select as select4 } from "@inquirer/prompts";
2782
3675
 
2783
3676
  // src/core/sequence.ts
2784
- import fs17 from "fs";
2785
- import path9 from "path";
3677
+ import fs20 from "fs";
3678
+ import path10 from "path";
2786
3679
  var STORY_FILE2 = /^(\d{2,})-story-.+\.md$/;
2787
3680
  function scanPlans(plansDir) {
2788
3681
  const used = [];
2789
3682
  const seen = /* @__PURE__ */ new Map();
2790
- if (fs17.existsSync(plansDir)) {
2791
- for (const feature of fs17.readdirSync(plansDir, { withFileTypes: true })) {
3683
+ if (fs20.existsSync(plansDir)) {
3684
+ for (const feature of fs20.readdirSync(plansDir, { withFileTypes: true })) {
2792
3685
  if (!feature.isDirectory()) continue;
2793
- const featureDir = path9.join(plansDir, feature.name);
2794
- for (const entry of fs17.readdirSync(featureDir)) {
3686
+ const featureDir = path10.join(plansDir, feature.name);
3687
+ for (const entry of fs20.readdirSync(featureDir)) {
2795
3688
  const match = entry.match(STORY_FILE2);
2796
3689
  if (!match) continue;
2797
3690
  const n = parseInt(match[1], 10);
@@ -2815,9 +3708,9 @@ function canPromptForRm(opts) {
2815
3708
  return isInteractive() && Boolean(process.stdin.isTTY);
2816
3709
  }
2817
3710
  function removeOverviewRow(featurePlanDir, storyId) {
2818
- const overview = path10.join(featurePlanDir, "00-overview.md");
2819
- if (!fs18.existsSync(overview)) return;
2820
- const src = fs18.readFileSync(overview, "utf8");
3711
+ const overview = path11.join(featurePlanDir, "00-overview.md");
3712
+ if (!fs21.existsSync(overview)) return;
3713
+ const src = fs21.readFileSync(overview, "utf8");
2821
3714
  const lines = src.split("\n");
2822
3715
  const filtered = lines.filter((line2) => {
2823
3716
  if (!line2.startsWith("|")) return true;
@@ -2825,12 +3718,12 @@ function removeOverviewRow(featurePlanDir, storyId) {
2825
3718
  if (/^\|\s*-+/.test(line2)) return true;
2826
3719
  return !line2.includes(storyId);
2827
3720
  });
2828
- fs18.writeFileSync(overview, filtered.join("\n"), "utf8");
3721
+ fs21.writeFileSync(overview, filtered.join("\n"), "utf8");
2829
3722
  }
2830
3723
  function removeOverviewRowForPlanFile(featurePlanDir, planBasename) {
2831
- const overview = path10.join(featurePlanDir, "00-overview.md");
2832
- if (!fs18.existsSync(overview)) return;
2833
- const src = fs18.readFileSync(overview, "utf8");
3724
+ const overview = path11.join(featurePlanDir, "00-overview.md");
3725
+ if (!fs21.existsSync(overview)) return;
3726
+ const src = fs21.readFileSync(overview, "utf8");
2834
3727
  const token = `\`${planBasename}\``;
2835
3728
  const lines = src.split("\n");
2836
3729
  const filtered = lines.filter((line2) => {
@@ -2839,25 +3732,25 @@ function removeOverviewRowForPlanFile(featurePlanDir, planBasename) {
2839
3732
  if (/^\|\s*-+/.test(line2)) return true;
2840
3733
  return !line2.includes(token);
2841
3734
  });
2842
- fs18.writeFileSync(overview, filtered.join("\n"), "utf8");
3735
+ fs21.writeFileSync(overview, filtered.join("\n"), "utf8");
2843
3736
  }
2844
3737
  function trashOrDelete(absPaths, trashRoot, useTrash) {
2845
3738
  if (!useTrash) {
2846
- for (const p of absPaths) fs18.rmSync(p, { recursive: true, force: true });
3739
+ for (const p of absPaths) fs21.rmSync(p, { recursive: true, force: true });
2847
3740
  return { removed: absPaths };
2848
3741
  }
2849
3742
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2850
- const bucket = path10.join(trashRoot, ts);
2851
- fs18.mkdirSync(bucket, { recursive: true });
3743
+ const bucket = path11.join(trashRoot, ts);
3744
+ fs21.mkdirSync(bucket, { recursive: true });
2852
3745
  for (const src of absPaths) {
2853
- const rel = path10.basename(src);
2854
- let dst = path10.join(bucket, rel);
3746
+ const rel = path11.basename(src);
3747
+ let dst = path11.join(bucket, rel);
2855
3748
  let n = 0;
2856
- while (fs18.existsSync(dst)) {
3749
+ while (fs21.existsSync(dst)) {
2857
3750
  n += 1;
2858
- dst = path10.join(bucket, `${n}_${rel}`);
3751
+ dst = path11.join(bucket, `${n}_${rel}`);
2859
3752
  }
2860
- fs18.renameSync(src, dst);
3753
+ fs21.renameSync(src, dst);
2861
3754
  }
2862
3755
  return { removed: absPaths, trashed: bucket };
2863
3756
  }
@@ -2868,21 +3761,21 @@ function renderPreview(kind, targets, opts) {
2868
3761
  kv(t.kind, t.label, 6);
2869
3762
  }
2870
3763
  if (opts.trash) {
2871
- info(`Items will move to ${path10.relative(process.cwd(), opts.trashRoot)}/<timestamp>/`);
3764
+ info(`Items will move to ${path11.relative(process.cwd(), opts.trashRoot)}/<timestamp>/`);
2872
3765
  } else if (!opts.dryRun) {
2873
3766
  warning("This is a permanent delete. Pass --trash to move to .squad/.trash/ instead.");
2874
3767
  }
2875
3768
  }
2876
3769
  function listAllPlanListEntries(paths, featureFilter) {
2877
- if (!fs18.existsSync(paths.plansDir)) return [];
3770
+ if (!fs21.existsSync(paths.plansDir)) return [];
2878
3771
  const out = [];
2879
- for (const ent of fs18.readdirSync(paths.plansDir, { withFileTypes: true })) {
3772
+ for (const ent of fs21.readdirSync(paths.plansDir, { withFileTypes: true })) {
2880
3773
  if (!ent.isDirectory()) continue;
2881
3774
  if (featureFilter && ent.name !== featureFilter) continue;
2882
- const featureDir = path10.join(paths.plansDir, ent.name);
2883
- for (const name of fs18.readdirSync(featureDir)) {
3775
+ const featureDir = path11.join(paths.plansDir, ent.name);
3776
+ for (const name of fs21.readdirSync(featureDir)) {
2884
3777
  if (!STORY_PLAN.test(name)) continue;
2885
- out.push({ feature: ent.name, file: name, abs: path10.join(featureDir, name) });
3778
+ out.push({ feature: ent.name, file: name, abs: path11.join(featureDir, name) });
2886
3779
  }
2887
3780
  }
2888
3781
  return out.sort((a, b) => (a.feature + a.file).localeCompare(b.feature + b.file));
@@ -2933,34 +3826,34 @@ function resolveStoryFromArg(arg, stories, root) {
2933
3826
  if (s.id === t) return s;
2934
3827
  if (`${s.feature}/${s.id}` === t) return s;
2935
3828
  }
2936
- const abs = path10.isAbsolute(t) ? t : path10.resolve(root, t);
2937
- const withIntake = t.endsWith("intake.md") ? abs : path10.join(abs, "intake.md");
3829
+ const abs = path11.isAbsolute(t) ? t : path11.resolve(root, t);
3830
+ const withIntake = t.endsWith("intake.md") ? abs : path11.join(abs, "intake.md");
2938
3831
  return findStoryByIntake(stories, withIntake) ?? findStoryByIntake(stories, abs);
2939
3832
  }
2940
3833
  function isSequenceToken(s) {
2941
3834
  return /^\d{1,4}$/.test(s) && s.length > 0;
2942
3835
  }
2943
3836
  function tryParsePlanFilePath(absPath, paths, featureFilter) {
2944
- if (!fs18.existsSync(absPath) || !STORY_PLAN.test(path10.basename(absPath))) return void 0;
2945
- const planRoot = fs18.realpathSync(path10.resolve(paths.plansDir));
2946
- const fileReal = fs18.realpathSync(path10.resolve(absPath));
2947
- const rel = path10.relative(planRoot, fileReal);
2948
- if (rel.startsWith("..") || path10.isAbsolute(rel)) return void 0;
2949
- const segs = rel.split(path10.sep);
3837
+ if (!fs21.existsSync(absPath) || !STORY_PLAN.test(path11.basename(absPath))) return void 0;
3838
+ const planRoot = fs21.realpathSync(path11.resolve(paths.plansDir));
3839
+ const fileReal = fs21.realpathSync(path11.resolve(absPath));
3840
+ const rel = path11.relative(planRoot, fileReal);
3841
+ if (rel.startsWith("..") || path11.isAbsolute(rel)) return void 0;
3842
+ const segs = rel.split(path11.sep);
2950
3843
  if (segs.length < 2) return void 0;
2951
3844
  const feature = segs[0];
2952
- const file = segs.slice(1).join(path10.sep);
3845
+ const file = segs.slice(1).join(path11.sep);
2953
3846
  if (featureFilter && feature !== featureFilter) return void 0;
2954
- return { feature, file, abs: path10.resolve(absPath) };
3847
+ return { feature, file, abs: path11.resolve(absPath) };
2955
3848
  }
2956
3849
  function resolvePlanFromArg(arg, paths, featureFilter) {
2957
3850
  const t = arg.trim();
2958
3851
  if (!t) return void 0;
2959
- const asPath = path10.isAbsolute(t) ? t : path10.resolve(process.cwd(), t);
3852
+ const asPath = path11.isAbsolute(t) ? t : path11.resolve(process.cwd(), t);
2960
3853
  const a = tryParsePlanFilePath(asPath, paths, featureFilter);
2961
3854
  if (a) return a;
2962
- if (!path10.isAbsolute(t)) {
2963
- const under = tryParsePlanFilePath(path10.join(paths.plansDir, t), paths, featureFilter);
3855
+ if (!path11.isAbsolute(t)) {
3856
+ const under = tryParsePlanFilePath(path11.join(paths.plansDir, t), paths, featureFilter);
2964
3857
  if (under) return under;
2965
3858
  }
2966
3859
  if (isSequenceToken(t)) {
@@ -2990,24 +3883,24 @@ function countOverviewDataRows(overviewContent) {
2990
3883
  return n;
2991
3884
  }
2992
3885
  function featureStoriesDirEmpty(paths, feature) {
2993
- const d = path10.join(paths.storiesDir, feature);
2994
- if (!fs18.existsSync(d)) return true;
2995
- return fs18.readdirSync(d).length === 0;
3886
+ const d = path11.join(paths.storiesDir, feature);
3887
+ if (!fs21.existsSync(d)) return true;
3888
+ return fs21.readdirSync(d).length === 0;
2996
3889
  }
2997
3890
 
2998
3891
  // src/core/story-mutations.ts
2999
3892
  function writeNewStoryToDisk(input5) {
3000
3893
  const { paths, config: config2, featureSlug, storyFolderName, id, title, noTracker, sourceBlock, fetchedIssue } = input5;
3001
- const storyDir = path11.join(paths.storiesDir, featureSlug, storyFolderName);
3002
- const intakeFile = path11.join(storyDir, "intake.md");
3003
- const attachmentsDir = path11.join(storyDir, "attachments");
3004
- if (fs19.existsSync(intakeFile)) {
3005
- const rel = path11.relative(paths.root, intakeFile);
3894
+ const storyDir = path12.join(paths.storiesDir, featureSlug, storyFolderName);
3895
+ const intakeFile = path12.join(storyDir, "intake.md");
3896
+ const attachmentsDir = path12.join(storyDir, "attachments");
3897
+ if (fs22.existsSync(intakeFile)) {
3898
+ const rel = path12.relative(paths.root, intakeFile);
3006
3899
  throw new Error(
3007
3900
  `Intake already exists: ${intakeFile}. Run \`squad new-plan ${rel}\` to plan it, or \`squad rm story\` to remove the story first.`
3008
3901
  );
3009
3902
  }
3010
- fs19.mkdirSync(attachmentsDir, { recursive: true });
3903
+ fs22.mkdirSync(attachmentsDir, { recursive: true });
3011
3904
  const template = readBundledPrompt("intake.md");
3012
3905
  const rendered = render(
3013
3906
  template,
@@ -3022,7 +3915,7 @@ function writeNewStoryToDisk(input5) {
3022
3915
  );
3023
3916
  const skippedPreamble = noTracker && config2.tracker.type !== "none" && !sourceBlock ? buildSkippedTrackerPreamble(config2.tracker.type) : "";
3024
3917
  writeFileSafe(intakeFile, skippedPreamble + sourceBlock + rendered, false);
3025
- ensureFeatureOverview(path11.join(paths.plansDir, featureSlug), featureSlug);
3918
+ ensureFeatureOverview(path12.join(paths.plansDir, featureSlug), featureSlug);
3026
3919
  return { storyDir, intakePath: intakeFile, feature: featureSlug, id: storyFolderName };
3027
3920
  }
3028
3921
  function createStoryRecord(input5) {
@@ -3071,16 +3964,16 @@ async function createStoryRecordFromIssue(input5) {
3071
3964
  }
3072
3965
  const title = input5.issue.title;
3073
3966
  const storyFolderName = trackerIdForFilename(input5.config.tracker.type, id);
3074
- const storyDir = path11.join(input5.paths.storiesDir, feature, storyFolderName);
3075
- const intakeFile = path11.join(storyDir, "intake.md");
3076
- const attachmentsDir = path11.join(storyDir, "attachments");
3077
- if (fs19.existsSync(intakeFile)) {
3078
- const rel = path11.relative(input5.paths.root, intakeFile);
3967
+ const storyDir = path12.join(input5.paths.storiesDir, feature, storyFolderName);
3968
+ const intakeFile = path12.join(storyDir, "intake.md");
3969
+ const attachmentsDir = path12.join(storyDir, "attachments");
3970
+ if (fs22.existsSync(intakeFile)) {
3971
+ const rel = path12.relative(input5.paths.root, intakeFile);
3079
3972
  throw new Error(
3080
3973
  `Intake already exists: ${intakeFile}. Run \`squad new-plan ${rel}\` to plan it, or \`squad rm story\` to remove the story first.`
3081
3974
  );
3082
3975
  }
3083
- fs19.mkdirSync(attachmentsDir, { recursive: true });
3976
+ fs22.mkdirSync(attachmentsDir, { recursive: true });
3084
3977
  let downloads = [];
3085
3978
  if (input5.withAttachments && input5.issue.attachments.length > 0) {
3086
3979
  downloads = await input5.client.downloadAttachments(input5.issue.attachments, attachmentsDir);
@@ -3108,10 +4001,10 @@ async function createStoryRecordFromIssue(input5) {
3108
4001
  function deleteStoryRecord(input5) {
3109
4002
  const targets = [input5.story.storyDir];
3110
4003
  if (input5.story.planFile) {
3111
- targets.push(path11.join(input5.paths.plansDir, input5.story.feature, input5.story.planFile));
4004
+ targets.push(path12.join(input5.paths.plansDir, input5.story.feature, input5.story.planFile));
3112
4005
  }
3113
4006
  const result = trashOrDelete(targets, input5.paths.trashDir, input5.trash);
3114
- removeOverviewRow(path11.join(input5.paths.plansDir, input5.story.feature), input5.story.id);
4007
+ removeOverviewRow(path12.join(input5.paths.plansDir, input5.story.feature), input5.story.id);
3115
4008
  return result;
3116
4009
  }
3117
4010
 
@@ -3221,15 +4114,15 @@ async function runNewStory(featureSlug, opts) {
3221
4114
  'Could not derive a story folder name. Pass `--title "<hint>"` when using `--no-tracker`, or `--id <tracker-id>` to name the folder after the tracker id.'
3222
4115
  );
3223
4116
  }
3224
- const storyDir = path12.join(paths.storiesDir, slug, storyFolderName);
3225
- const intakeFile = path12.join(storyDir, "intake.md");
3226
- const attachmentsDir = path12.join(storyDir, "attachments");
3227
- if (fs20.existsSync(intakeFile)) {
4117
+ const storyDir = path13.join(paths.storiesDir, slug, storyFolderName);
4118
+ const intakeFile = path13.join(storyDir, "intake.md");
4119
+ const attachmentsDir = path13.join(storyDir, "attachments");
4120
+ if (fs23.existsSync(intakeFile)) {
3228
4121
  throw new Error(
3229
- `Intake already exists: ${intakeFile}. Run \`squad new-plan ${path12.relative(root, intakeFile)}\` to plan it, or \`squad rm story\` to remove the story first.`
4122
+ `Intake already exists: ${intakeFile}. Run \`squad new-plan ${path13.relative(root, intakeFile)}\` to plan it, or \`squad rm story\` to remove the story first.`
3230
4123
  );
3231
4124
  }
3232
- fs20.mkdirSync(attachmentsDir, { recursive: true });
4125
+ fs23.mkdirSync(attachmentsDir, { recursive: true });
3233
4126
  let sourceBlock = "";
3234
4127
  let fetchedSummary;
3235
4128
  let fetchedIssue;
@@ -3259,14 +4152,14 @@ async function runNewStory(featureSlug, opts) {
3259
4152
  sourceBlock,
3260
4153
  fetchedIssue
3261
4154
  });
3262
- const relativePath = path12.relative(root, intakeFile);
3263
- const relAttachDir = path12.relative(root, attachmentsDir);
4155
+ const relativePath = path13.relative(root, intakeFile);
4156
+ const relAttachDir = path13.relative(root, attachmentsDir);
3264
4157
  success("Created intake");
3265
4158
  if (noTracker && config2.tracker.type !== "none") {
3266
4159
  warning("Created without a tracker link. Use `squad tracker link` later to attach one.");
3267
4160
  }
3268
4161
  kv("intake", relativePath, 11);
3269
- kv("attachments", relAttachDir + path12.sep, 11);
4162
+ kv("attachments", relAttachDir + path13.sep, 11);
3270
4163
  if (fetchedSummary && fetchedSummary.written > 0) {
3271
4164
  kv("fetched", `${fetchedSummary.id} \xB7 ${fetchedSummary.written} attachments saved`, 11);
3272
4165
  }
@@ -3275,7 +4168,7 @@ async function runNewStory(featureSlug, opts) {
3275
4168
  info(`squad new-plan ${relativePath}`);
3276
4169
  }
3277
4170
  async function tryFetchIssue(config2, id, opts, paths, attachmentsDir) {
3278
- const secretsFromFile = fs20.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
4171
+ const secretsFromFile = fs23.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
3279
4172
  const secrets = overlayTrackerEnv(secretsFromFile);
3280
4173
  const resolution = clientFor(config2, secrets);
3281
4174
  if (resolution.error) {
@@ -3332,12 +4225,12 @@ function truncate(s, n) {
3332
4225
  }
3333
4226
 
3334
4227
  // src/commands/new-plan.ts
3335
- import fs32 from "fs";
3336
- import path24 from "path";
3337
- import { confirm as confirm6, select as select6 } from "@inquirer/prompts";
4228
+ import fs35 from "fs";
4229
+ import path25 from "path";
4230
+ import { confirm as confirm7, select as select6 } from "@inquirer/prompts";
3338
4231
 
3339
4232
  // src/utils/clipboard.ts
3340
- import { spawn, spawnSync } from "child_process";
4233
+ import { spawn as spawn2, spawnSync } from "child_process";
3341
4234
  async function copyToClipboard(text) {
3342
4235
  const candidates = clipboardCandidates();
3343
4236
  if (candidates.length === 0) {
@@ -3360,7 +4253,7 @@ function tryCopy(cmd, text) {
3360
4253
  return new Promise((resolve2) => {
3361
4254
  let child;
3362
4255
  try {
3363
- child = spawn(cmd.command, cmd.args, { stdio: ["pipe", "ignore", "pipe"] });
4256
+ child = spawn2(cmd.command, cmd.args, { stdio: ["pipe", "ignore", "pipe"] });
3364
4257
  } catch (err2) {
3365
4258
  resolve2({ ok: false, reason: `${cmd.command} failed to spawn: ${err2.message}` });
3366
4259
  return;
@@ -3425,16 +4318,16 @@ function noCandidateReason() {
3425
4318
  }
3426
4319
 
3427
4320
  // src/planner/loop.ts
3428
- import path20 from "path";
3429
- import fs28 from "fs";
4321
+ import path21 from "path";
4322
+ import fs31 from "fs";
3430
4323
  import { APICallError as APICallError3 } from "ai";
3431
4324
 
3432
4325
  // src/planner/tools/index.ts
3433
4326
  import { randomUUID } from "crypto";
3434
4327
 
3435
4328
  // src/planner/tools/read-file.ts
3436
- import fs21 from "fs";
3437
- import path13 from "path";
4329
+ import fs24 from "fs";
4330
+ import path14 from "path";
3438
4331
  import { tool } from "ai";
3439
4332
  import { z } from "zod";
3440
4333
  var MAX_BYTES_PER_FULL_READ = 32e3;
@@ -3469,15 +4362,15 @@ function readFileTool(root, budget, input5) {
3469
4362
  if (offset === void 0 !== (limit === void 0)) {
3470
4363
  return err("read_file: pass both offset and limit for a ranged read, or neither for a full read.");
3471
4364
  }
3472
- const resolved = path13.resolve(root, raw);
3473
- const relCheck = path13.relative(root, resolved);
3474
- if (relCheck.startsWith("..") || path13.isAbsolute(relCheck)) {
4365
+ const resolved = path14.resolve(root, raw);
4366
+ const relCheck = path14.relative(root, resolved);
4367
+ if (relCheck.startsWith("..") || path14.isAbsolute(relCheck)) {
3475
4368
  return err(`read_file: path "${raw}" escapes the project root and was refused.`);
3476
4369
  }
3477
- if (!fs21.existsSync(resolved)) {
4370
+ if (!fs24.existsSync(resolved)) {
3478
4371
  return err(`read_file: not found "${raw}".`);
3479
4372
  }
3480
- const stat = fs21.statSync(resolved);
4373
+ const stat = fs24.statSync(resolved);
3481
4374
  if (stat.isDirectory()) {
3482
4375
  return err(`read_file: "${raw}" is a directory. Ask for a specific file path.`);
3483
4376
  }
@@ -3495,7 +4388,7 @@ function readFileTool(root, budget, input5) {
3495
4388
  }
3496
4389
  let buf;
3497
4390
  try {
3498
- buf = fs21.readFileSync(resolved);
4391
+ buf = fs24.readFileSync(resolved);
3499
4392
  } catch (e) {
3500
4393
  return err(`read_file: failed to read "${raw}" (${e.message}).`);
3501
4394
  }
@@ -3518,7 +4411,7 @@ function readFileRanged(resolved, rawRel, budget, fileSize, offset, limit) {
3518
4411
  }
3519
4412
  let data;
3520
4413
  try {
3521
- data = fs21.readFileSync(resolved);
4414
+ data = fs24.readFileSync(resolved);
3522
4415
  } catch (e) {
3523
4416
  return err(`read_file: failed to read "${rawRel}" (${e.message}).`);
3524
4417
  }
@@ -3574,14 +4467,14 @@ function looksBinary(buf) {
3574
4467
  }
3575
4468
 
3576
4469
  // src/planner/tools/grep.ts
3577
- import fs23 from "fs";
3578
- import path15 from "path";
4470
+ import fs26 from "fs";
4471
+ import path16 from "path";
3579
4472
  import { tool as tool2 } from "ai";
3580
4473
  import { z as z2 } from "zod";
3581
4474
 
3582
4475
  // src/core/repo-map.ts
3583
- import fs22 from "fs";
3584
- import path14 from "path";
4476
+ import fs25 from "fs";
4477
+ import path15 from "path";
3585
4478
  import ignore from "ignore";
3586
4479
  var DEFAULT_IGNORES = [
3587
4480
  "node_modules",
@@ -3597,10 +4490,10 @@ var DEFAULT_IGNORES = [
3597
4490
  ];
3598
4491
  function buildRepoIgnore(root) {
3599
4492
  const ig = ignore().add(DEFAULT_IGNORES);
3600
- const gitignore = path14.join(root, ".gitignore");
3601
- if (fs22.existsSync(gitignore)) {
4493
+ const gitignore = path15.join(root, ".gitignore");
4494
+ if (fs25.existsSync(gitignore)) {
3602
4495
  try {
3603
- ig.add(fs22.readFileSync(gitignore, "utf8"));
4496
+ ig.add(fs25.readFileSync(gitignore, "utf8"));
3604
4497
  } catch {
3605
4498
  }
3606
4499
  }
@@ -3656,7 +4549,7 @@ function renderDirTree(name, tree, depth) {
3656
4549
  function renderRepoTree(entries) {
3657
4550
  const root = { files: [], dirs: /* @__PURE__ */ new Map() };
3658
4551
  for (const e of entries) {
3659
- const parts = e.rel.split(path14.sep).filter(Boolean);
4552
+ const parts = e.rel.split(path15.sep).filter(Boolean);
3660
4553
  if (parts.length === 0) continue;
3661
4554
  insertTree(root, parts, e.size);
3662
4555
  }
@@ -3666,26 +4559,26 @@ function collectFileEntries(root, dir, ig, out, max, includeHidden) {
3666
4559
  if (out.length >= max) return out;
3667
4560
  let dirents;
3668
4561
  try {
3669
- dirents = fs22.readdirSync(dir, { withFileTypes: true });
4562
+ dirents = fs25.readdirSync(dir, { withFileTypes: true });
3670
4563
  } catch {
3671
4564
  return out;
3672
4565
  }
3673
4566
  for (const entry of dirents) {
3674
4567
  if (out.length >= max) break;
3675
4568
  if (!includeHidden && entry.name.startsWith(".") && entry.name !== ".squad") continue;
3676
- const abs = path14.join(dir, entry.name);
3677
- const rel = path14.relative(root, abs);
4569
+ const abs = path15.join(dir, entry.name);
4570
+ const rel = path15.relative(root, abs);
3678
4571
  if (!rel || ig.ignores(rel) || entry.isDirectory() && ig.ignores(rel + "/")) continue;
3679
4572
  if (entry.isDirectory()) {
3680
4573
  collectFileEntries(root, abs, ig, out, max, includeHidden);
3681
4574
  } else if (entry.isFile()) {
3682
4575
  let size = 0;
3683
4576
  try {
3684
- size = fs22.statSync(abs).size;
4577
+ size = fs25.statSync(abs).size;
3685
4578
  } catch {
3686
4579
  size = 0;
3687
4580
  }
3688
- out.push({ rel: rel.split(path14.sep).join("/"), size });
4581
+ out.push({ rel: rel.split(path15.sep).join("/"), size });
3689
4582
  }
3690
4583
  }
3691
4584
  return out;
@@ -3694,20 +4587,20 @@ function walkFlat(root, dir, ig, out, max, includeHidden) {
3694
4587
  if (out.length >= max) return;
3695
4588
  let entries;
3696
4589
  try {
3697
- entries = fs22.readdirSync(dir, { withFileTypes: true });
4590
+ entries = fs25.readdirSync(dir, { withFileTypes: true });
3698
4591
  } catch {
3699
4592
  return;
3700
4593
  }
3701
4594
  for (const entry of entries) {
3702
4595
  if (out.length >= max) return;
3703
4596
  if (!includeHidden && entry.name.startsWith(".") && entry.name !== ".squad") continue;
3704
- const abs = path14.join(dir, entry.name);
3705
- const rel = path14.relative(root, abs);
4597
+ const abs = path15.join(dir, entry.name);
4598
+ const rel = path15.relative(root, abs);
3706
4599
  if (!rel || ig.ignores(rel) || entry.isDirectory() && ig.ignores(rel + "/")) continue;
3707
4600
  if (entry.isDirectory()) {
3708
4601
  walkFlat(root, abs, ig, out, max, includeHidden);
3709
4602
  } else if (entry.isFile()) {
3710
- out.push(rel.split(path14.sep).join("/"));
4603
+ out.push(rel.split(path15.sep).join("/"));
3711
4604
  }
3712
4605
  }
3713
4606
  }
@@ -3743,12 +4636,12 @@ function runGrep(root, budget, args) {
3743
4636
  return { content: `grep: invalid pattern (${e.message}).`, isError: true };
3744
4637
  }
3745
4638
  const ig = buildRepoIgnore(root);
3746
- const scopeRoot = args.path?.trim() ? path15.resolve(root, args.path) : root;
3747
- const relScope = path15.relative(root, scopeRoot);
3748
- if (relScope.startsWith("..") || path15.isAbsolute(relScope)) {
4639
+ const scopeRoot = args.path?.trim() ? path16.resolve(root, args.path) : root;
4640
+ const relScope = path16.relative(root, scopeRoot);
4641
+ if (relScope.startsWith("..") || path16.isAbsolute(relScope)) {
3749
4642
  return { content: `grep: scope path escapes the project root and was refused.`, isError: true };
3750
4643
  }
3751
- if (!fs23.existsSync(scopeRoot)) {
4644
+ if (!fs26.existsSync(scopeRoot)) {
3752
4645
  return { content: `grep: scope path not found "${args.path}".`, isError: true };
3753
4646
  }
3754
4647
  const matches = [];
@@ -3756,28 +4649,28 @@ function runGrep(root, budget, args) {
3756
4649
  if (matches.length >= MAX_GREP_MATCHES) return;
3757
4650
  let entries;
3758
4651
  try {
3759
- entries = fs23.readdirSync(dir, { withFileTypes: true });
4652
+ entries = fs26.readdirSync(dir, { withFileTypes: true });
3760
4653
  } catch {
3761
4654
  return;
3762
4655
  }
3763
4656
  for (const entry of entries) {
3764
4657
  if (matches.length >= MAX_GREP_MATCHES) return;
3765
- const abs = path15.join(dir, entry.name);
3766
- const rel = path15.relative(root, abs).split(path15.sep).join("/");
4658
+ const abs = path16.join(dir, entry.name);
4659
+ const rel = path16.relative(root, abs).split(path16.sep).join("/");
3767
4660
  if (!rel || ig.ignores(rel) || entry.isDirectory() && ig.ignores(rel + "/")) continue;
3768
4661
  if (entry.isDirectory()) {
3769
4662
  walk2(abs);
3770
4663
  } else if (entry.isFile()) {
3771
4664
  let st;
3772
4665
  try {
3773
- st = fs23.statSync(abs);
4666
+ st = fs26.statSync(abs);
3774
4667
  } catch {
3775
4668
  continue;
3776
4669
  }
3777
4670
  if (st.size > MAX_GREP_FILE_SIZE_BYTES) continue;
3778
4671
  let buf;
3779
4672
  try {
3780
- buf = fs23.readFileSync(abs);
4673
+ buf = fs26.readFileSync(abs);
3781
4674
  } catch {
3782
4675
  continue;
3783
4676
  }
@@ -3796,14 +4689,14 @@ function runGrep(root, budget, args) {
3796
4689
  }
3797
4690
  }
3798
4691
  };
3799
- if (fs23.statSync(scopeRoot).isFile()) {
3800
- const rel = path15.relative(root, scopeRoot).split(path15.sep).join("/");
4692
+ if (fs26.statSync(scopeRoot).isFile()) {
4693
+ const rel = path16.relative(root, scopeRoot).split(path16.sep).join("/");
3801
4694
  if (ig.ignores(rel)) {
3802
4695
  return { content: `grep: file is ignored by repository filters.`, isError: true };
3803
4696
  }
3804
4697
  let st;
3805
4698
  try {
3806
- st = fs23.statSync(scopeRoot);
4699
+ st = fs26.statSync(scopeRoot);
3807
4700
  } catch {
3808
4701
  return { content: `grep: not found.`, isError: true };
3809
4702
  }
@@ -3812,7 +4705,7 @@ function runGrep(root, budget, args) {
3812
4705
  }
3813
4706
  let buf;
3814
4707
  try {
3815
- buf = fs23.readFileSync(scopeRoot);
4708
+ buf = fs26.readFileSync(scopeRoot);
3816
4709
  } catch (e) {
3817
4710
  return { content: `grep: failed to read (${e.message}).`, isError: true };
3818
4711
  }
@@ -3846,8 +4739,8 @@ ${matches.join("\n")}` : `No matches found for "${args.pattern}".`;
3846
4739
  }
3847
4740
 
3848
4741
  // src/planner/tools/list-dir.ts
3849
- import fs24 from "fs";
3850
- import path16 from "path";
4742
+ import fs27 from "fs";
4743
+ import path17 from "path";
3851
4744
  import { tool as tool3 } from "ai";
3852
4745
  import { z as z3 } from "zod";
3853
4746
  var MAX_ENTRIES_PER_LIST = 200;
@@ -3865,44 +4758,44 @@ function runListDir(root, budget, rawPath) {
3865
4758
  };
3866
4759
  }
3867
4760
  const normalized = rawPath.trim() === "" ? "." : rawPath.trim();
3868
- const resolved = path16.resolve(root, normalized);
3869
- const relCheck = path16.relative(root, resolved);
3870
- if (relCheck.startsWith("..") || path16.isAbsolute(relCheck)) {
4761
+ const resolved = path17.resolve(root, normalized);
4762
+ const relCheck = path17.relative(root, resolved);
4763
+ if (relCheck.startsWith("..") || path17.isAbsolute(relCheck)) {
3871
4764
  return {
3872
4765
  content: `list_dir: path "${rawPath}" escapes the project root and was refused.`,
3873
4766
  isError: true
3874
4767
  };
3875
4768
  }
3876
- const relNorm = relCheck.split(path16.sep).join("/");
4769
+ const relNorm = relCheck.split(path17.sep).join("/");
3877
4770
  const ig = buildRepoIgnore(root);
3878
4771
  if (relNorm && (ig.ignores(relNorm) || ig.ignores(`${relNorm}/`))) {
3879
4772
  return { content: "list_dir: (directory ignored by repo-map filter)", isError: true };
3880
4773
  }
3881
- if (!fs24.existsSync(resolved)) {
4774
+ if (!fs27.existsSync(resolved)) {
3882
4775
  return { content: `list_dir: not found "${rawPath}".`, isError: true };
3883
4776
  }
3884
- const st = fs24.statSync(resolved);
4777
+ const st = fs27.statSync(resolved);
3885
4778
  if (!st.isDirectory()) {
3886
4779
  return { content: `list_dir: "${rawPath}" is not a directory.`, isError: true };
3887
4780
  }
3888
4781
  let dirents;
3889
4782
  try {
3890
- dirents = fs24.readdirSync(resolved, { withFileTypes: true });
4783
+ dirents = fs27.readdirSync(resolved, { withFileTypes: true });
3891
4784
  } catch (e) {
3892
4785
  return { content: `list_dir: failed to read (${e.message}).`, isError: true };
3893
4786
  }
3894
4787
  const rows = [];
3895
4788
  for (const d of dirents) {
3896
4789
  if (rows.length >= MAX_ENTRIES_PER_LIST) break;
3897
- const childAbs = path16.join(resolved, d.name);
3898
- const childRel = path16.relative(root, childAbs).split(path16.sep).join("/");
4790
+ const childAbs = path17.join(resolved, d.name);
4791
+ const childRel = path17.relative(root, childAbs).split(path17.sep).join("/");
3899
4792
  if (childRel && (ig.ignores(childRel) || d.isDirectory() && ig.ignores(`${childRel}/`))) continue;
3900
4793
  if (d.isDirectory()) {
3901
4794
  rows.push({ type: "d", name: d.name + "/", size: null });
3902
4795
  } else if (d.isFile()) {
3903
4796
  let size = 0;
3904
4797
  try {
3905
- size = fs24.statSync(childAbs).size;
4798
+ size = fs27.statSync(childAbs).size;
3906
4799
  } catch {
3907
4800
  size = 0;
3908
4801
  }
@@ -4181,96 +5074,9 @@ function vercelToolsFromDefinitions(defs) {
4181
5074
  return out;
4182
5075
  }
4183
5076
 
4184
- // src/planner/provider-errors.ts
4185
- function detectModelNotFound(provider, model, status, rawBody) {
4186
- if (status !== 404) return void 0;
4187
- const body = rawBody.toLowerCase();
4188
- const hints = ["not_found_error", "model_not_found", "not_found", "model:"];
4189
- if (!hints.some((h) => body.includes(h))) return void 0;
4190
- return { provider, model, status, rawBody };
4191
- }
4192
- function modelNotFoundMessage(err2) {
4193
- return [
4194
- `The ${err2.provider} planner model "${err2.model}" is no longer available.`,
4195
- "",
4196
- "Recovery options:",
4197
- ` 1. Run \`squad upgrade\` to install a newer squad-kit (or install manually from npm).`,
4198
- ` 2. Run \`squad config set planner --provider openai\` (or another \`--provider\`) to switch models via a different provider, then save a key when prompted.`,
4199
- ` 3. Run \`squad config set planner\` to set \`planner.modelOverride.${err2.provider}\` to a still-valid id (no raw YAML), then \`squad doctor\` to confirm.`,
4200
- "",
4201
- `Raw provider response: ${err2.rawBody.slice(0, 200)}`
4202
- ].join("\n");
4203
- }
4204
- function detectRateLimit(provider, status, headers, rawBody) {
4205
- if (status !== 429) return void 0;
4206
- return {
4207
- provider,
4208
- status: 429,
4209
- retryAfterSec: parseRetryAfter(headers, rawBody),
4210
- rawBody
4211
- };
4212
- }
4213
- function parseRetryAfterSec(headers) {
4214
- const raw = headerGet(headers, "retry-after");
4215
- if (!raw) return void 0;
4216
- const sec = Number(raw);
4217
- if (Number.isFinite(sec) && sec >= 0) return Math.ceil(sec);
4218
- const asDate = Date.parse(raw);
4219
- if (Number.isFinite(asDate)) {
4220
- return Math.max(0, Math.ceil((asDate - Date.now()) / 1e3));
4221
- }
4222
- return void 0;
4223
- }
4224
- function parseRetryAfter(headers, body) {
4225
- const fromHeader = parseRetryAfterSec(headers);
4226
- if (fromHeader !== void 0) return fromHeader;
4227
- const bodyMatch = /"retryDelay"\s*:\s*"(\d+)(?:\.\d+)?s"/i.exec(body);
4228
- if (bodyMatch) {
4229
- const sec = Number(bodyMatch[1]);
4230
- if (Number.isFinite(sec) && sec >= 0) return sec;
4231
- }
4232
- return void 0;
4233
- }
4234
- function headerGet(h, name) {
4235
- if (typeof h.get === "function") {
4236
- return h.get(name);
4237
- }
4238
- const key = name.toLowerCase();
4239
- const rec = h;
4240
- for (const k of Object.keys(rec)) {
4241
- if (k.toLowerCase() === key) return rec[k] ?? null;
4242
- }
4243
- return null;
4244
- }
4245
- function rateLimitMessage(err2) {
4246
- const waitHint = err2.retryAfterSec ?? 60;
4247
- const limitsUrl = {
4248
- anthropic: "https://console.anthropic.com/settings/limits",
4249
- openai: "https://platform.openai.com/settings/organization/limits",
4250
- google: "https://aistudio.google.com/app/plan_information"
4251
- };
4252
- const headline = err2.retryAfterSec ? `${err2.provider} rate limit hit \u2014 provider asked us to wait ${err2.retryAfterSec}s before retrying.` : `${err2.provider} rate limit hit.`;
4253
- const cap = err2.maxRetrySec ?? 90;
4254
- const retried = err2.retrySkippedReason === "retry_after_too_long" ? `squad-kit did not auto-retry: the provider's ${err2.retryAfterSec}s wait is longer than our ${cap}s cap, so retrying would just burn another request inside the same throttle window.` : err2.retryAlreadyAttempted ? "squad-kit already retried once automatically and was throttled again \u2014 your org is firmly over its per-minute quota." : "squad-kit aborted before retrying.";
4255
- return [
4256
- headline,
4257
- retried,
4258
- "",
4259
- "Recovery options:",
4260
- ` 1. Wait ${waitHint}s and rerun \`squad new-plan --api\`. Per-minute limits reset quickly.`,
4261
- ` 2. Switch to a smaller planner model: \`squad config set planner\` (pick a cheaper model id).`,
4262
- ` 3. Tighten \`planner.budget\` in \`.squad/config.yaml\` (smaller \`maxContextBytes\` / \`maxFileReads\`) so each request carries fewer tokens.`,
4263
- ` 4. Upgrade your ${err2.provider} tier: ${limitsUrl[err2.provider]}.`,
4264
- "",
4265
- "Full runbook: docs/migrating-from-0.1.md (see \xA78. If something goes wrong).",
4266
- "",
4267
- `Raw provider response: ${err2.rawBody.slice(0, 200)}`
4268
- ].join("\n");
4269
- }
4270
-
4271
5077
  // src/core/runs.ts
4272
- import fs26 from "fs/promises";
4273
- import path18 from "path";
5078
+ import fs29 from "fs/promises";
5079
+ import path19 from "path";
4274
5080
  import { randomBytes } from "crypto";
4275
5081
 
4276
5082
  // src/core/run-retention.ts
@@ -4278,9 +5084,9 @@ var RUN_HISTORY_RING_SIZE = 20;
4278
5084
  var RUN_EVENT_JSONL_UNCOMPRESSED_HEAD = 5;
4279
5085
 
4280
5086
  // src/core/run-events-store.ts
4281
- import fs25, { createWriteStream } from "fs";
5087
+ import fs28, { createWriteStream } from "fs";
4282
5088
  import fsPromises from "fs/promises";
4283
- import path17 from "path";
5089
+ import path18 from "path";
4284
5090
  import readline from "readline";
4285
5091
  import { pipeline } from "stream/promises";
4286
5092
  import zlib from "zlib";
@@ -4288,10 +5094,14 @@ var SUFFIX_JSONL = ".events.jsonl";
4288
5094
  var SUFFIX_GZ = ".events.jsonl.gz";
4289
5095
  function redactPlannerEventForDisk(e) {
4290
5096
  if (e.kind === "thinking_delta") return { ...e, delta: "" };
5097
+ if (e.kind === "auth_info") {
5098
+ const { account: _account, ...rest } = e;
5099
+ return rest;
5100
+ }
4291
5101
  return e;
4292
5102
  }
4293
5103
  function runsDir(paths) {
4294
- return path17.join(paths.squadDir, "runs");
5104
+ return path18.join(paths.squadDir, "runs");
4295
5105
  }
4296
5106
  function logAppendErrorOnce(err2) {
4297
5107
  console.error("[run-events-store] append failed:", err2);
@@ -4348,8 +5158,8 @@ var RunEventsStoreImpl = class {
4348
5158
  };
4349
5159
  function createRunEventsStore(paths, runId) {
4350
5160
  const dir = runsDir(paths);
4351
- fs25.mkdirSync(dir, { recursive: true });
4352
- const file = path17.join(dir, `${runId}${SUFFIX_JSONL}`);
5161
+ fs28.mkdirSync(dir, { recursive: true });
5162
+ const file = path18.join(dir, `${runId}${SUFFIX_JSONL}`);
4353
5163
  return new RunEventsStoreImpl(file);
4354
5164
  }
4355
5165
  var RunEventsReaderImpl = class {
@@ -4374,7 +5184,7 @@ var RunEventsReaderImpl = class {
4374
5184
  const limit = opts?.limit ?? Number.POSITIVE_INFINITY;
4375
5185
  let logical = 0;
4376
5186
  let yielded = 0;
4377
- const stream = fs25.createReadStream(this.filePath);
5187
+ const stream = fs28.createReadStream(this.filePath);
4378
5188
  const input5 = this.useGunzip ? stream.pipe(zlib.createGunzip()) : stream;
4379
5189
  const rl = readline.createInterface({ input: input5, crlfDelay: Infinity });
4380
5190
  try {
@@ -4402,8 +5212,8 @@ var RunEventsReaderImpl = class {
4402
5212
  };
4403
5213
  async function openRunEventsReader(paths, runId) {
4404
5214
  const dir = runsDir(paths);
4405
- const jsonl = path17.join(dir, `${runId}${SUFFIX_JSONL}`);
4406
- const gz = path17.join(dir, `${runId}${SUFFIX_GZ}`);
5215
+ const jsonl = path18.join(dir, `${runId}${SUFFIX_JSONL}`);
5216
+ const gz = path18.join(dir, `${runId}${SUFFIX_GZ}`);
4407
5217
  let jsonlStat = null;
4408
5218
  let gzStat = null;
4409
5219
  try {
@@ -4435,10 +5245,10 @@ function collectEventLogs(dir, names) {
4435
5245
  kind = "jsonl";
4436
5246
  }
4437
5247
  if (!runId || !kind) continue;
4438
- const full = path17.join(dir, name);
5248
+ const full = path18.join(dir, name);
4439
5249
  let st;
4440
5250
  try {
4441
- st = { mtimeMs: fs25.statSync(full).mtimeMs };
5251
+ st = { mtimeMs: fs28.statSync(full).mtimeMs };
4442
5252
  } catch {
4443
5253
  continue;
4444
5254
  }
@@ -4466,7 +5276,7 @@ async function fsyncFile(filePath) {
4466
5276
  async function gzipAtomic(jsonlPath, gzPath) {
4467
5277
  const gzTmp = `${gzPath}.tmp`;
4468
5278
  await fsPromises.rm(gzTmp, { force: true });
4469
- await pipeline(fs25.createReadStream(jsonlPath), zlib.createGzip(), fs25.createWriteStream(gzTmp));
5279
+ await pipeline(fs28.createReadStream(jsonlPath), zlib.createGzip(), fs28.createWriteStream(gzTmp));
4470
5280
  await fsyncFile(gzTmp);
4471
5281
  await fsPromises.rename(gzTmp, gzPath);
4472
5282
  await fsPromises.unlink(jsonlPath);
@@ -4481,14 +5291,14 @@ async function rotateRunEventsUnlocked(paths) {
4481
5291
  }
4482
5292
  for (const name of names) {
4483
5293
  if (name.endsWith(`${SUFFIX_GZ}.tmp`)) {
4484
- await fsPromises.rm(path17.join(dir, name), { force: true });
5294
+ await fsPromises.rm(path18.join(dir, name), { force: true });
4485
5295
  }
4486
5296
  }
4487
5297
  names = await fsPromises.readdir(dir);
4488
5298
  const ordered = collectEventLogs(dir, names);
4489
5299
  for (let i = RUN_HISTORY_RING_SIZE; i < ordered.length; i++) {
4490
5300
  const e = ordered[i];
4491
- const summary = path17.join(dir, `${e.runId}.json`);
5301
+ const summary = path18.join(dir, `${e.runId}.json`);
4492
5302
  await fsPromises.rm(summary, { force: true });
4493
5303
  if (e.jsonlPath) await fsPromises.rm(e.jsonlPath, { force: true });
4494
5304
  if (e.gzPath) await fsPromises.rm(e.gzPath, { force: true });
@@ -4498,7 +5308,7 @@ async function rotateRunEventsUnlocked(paths) {
4498
5308
  const e = keep[i];
4499
5309
  const jsonl = e.jsonlPath;
4500
5310
  if (!jsonl) continue;
4501
- const gz = path17.join(dir, `${e.runId}${SUFFIX_GZ}`);
5311
+ const gz = path18.join(dir, `${e.runId}${SUFFIX_GZ}`);
4502
5312
  await gzipAtomic(jsonl, gz);
4503
5313
  }
4504
5314
  }
@@ -4515,34 +5325,34 @@ function newRunId() {
4515
5325
  return `${ts}-${rnd}`;
4516
5326
  }
4517
5327
  async function pruneOldRuns(paths) {
4518
- const dir = path18.join(paths.squadDir, "runs");
4519
- const entries = (await fs26.readdir(dir)).filter((n) => n.endsWith(".json"));
5328
+ const dir = path19.join(paths.squadDir, "runs");
5329
+ const entries = (await fs29.readdir(dir)).filter((n) => n.endsWith(".json"));
4520
5330
  if (entries.length <= RUN_HISTORY_RING_SIZE) return;
4521
5331
  entries.sort();
4522
5332
  const drop = entries.slice(0, entries.length - RUN_HISTORY_RING_SIZE);
4523
5333
  await Promise.all(
4524
5334
  drop.map(async (n) => {
4525
- const runPath = path18.join(dir, n);
5335
+ const runPath = path19.join(dir, n);
4526
5336
  const runId = n.replace(/\.json$/u, "");
4527
- await fs26.rm(runPath, { force: true });
4528
- await fs26.rm(path18.join(dir, `${runId}.events.jsonl`), { force: true });
4529
- await fs26.rm(path18.join(dir, `${runId}.events.jsonl.gz`), { force: true });
5337
+ await fs29.rm(runPath, { force: true });
5338
+ await fs29.rm(path19.join(dir, `${runId}.events.jsonl`), { force: true });
5339
+ await fs29.rm(path19.join(dir, `${runId}.events.jsonl.gz`), { force: true });
4530
5340
  })
4531
5341
  );
4532
5342
  }
4533
5343
  async function appendRun(paths, rec) {
4534
- const dir = path18.join(paths.squadDir, "runs");
4535
- await fs26.mkdir(dir, { recursive: true });
4536
- const file = path18.join(dir, `${rec.runId}.json`);
4537
- await fs26.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
5344
+ const dir = path19.join(paths.squadDir, "runs");
5345
+ await fs29.mkdir(dir, { recursive: true });
5346
+ const file = path19.join(dir, `${rec.runId}.json`);
5347
+ await fs29.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
4538
5348
  await pruneOldRuns(paths);
4539
5349
  await rotateRunEvents(paths);
4540
5350
  }
4541
5351
  async function listRuns(paths) {
4542
- const dir = path18.join(paths.squadDir, "runs");
5352
+ const dir = path19.join(paths.squadDir, "runs");
4543
5353
  let entries;
4544
5354
  try {
4545
- entries = await fs26.readdir(dir);
5355
+ entries = await fs29.readdir(dir);
4546
5356
  } catch {
4547
5357
  return [];
4548
5358
  }
@@ -4550,7 +5360,7 @@ async function listRuns(paths) {
4550
5360
  const out = [];
4551
5361
  for (const f of files) {
4552
5362
  try {
4553
- const raw = await fs26.readFile(path18.join(dir, f), "utf8");
5363
+ const raw = await fs29.readFile(path19.join(dir, f), "utf8");
4554
5364
  const parsed = JSON.parse(raw);
4555
5365
  if (parsed.version === 1) out.push(parsed);
4556
5366
  } catch {
@@ -4887,6 +5697,11 @@ var VercelRuntime = class {
4887
5697
  if (typeof apiKeyOrModel !== "string") {
4888
5698
  throw new Error("VercelRuntime: apiKey must be a string unless injectModel is true");
4889
5699
  }
5700
+ if (!apiKeyOrModel) {
5701
+ throw new Error(
5702
+ `VercelRuntime: a non-empty ${provider} API key is required. Run \`squad config set planner\` to save one.`
5703
+ );
5704
+ }
4890
5705
  this.languageModel = resolveModel(provider, modelId, apiKeyOrModel).model;
4891
5706
  }
4892
5707
  async runScout(input5) {
@@ -5339,8 +6154,8 @@ async function runScout(input5) {
5339
6154
  }
5340
6155
 
5341
6156
  // src/planner/validation.ts
5342
- import fs27 from "fs";
5343
- import path19 from "path";
6157
+ import fs30 from "fs";
6158
+ import path20 from "path";
5344
6159
  var CODE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|md|json|ya?ml|css|html|py|go|rs|cs|java|kt|kts|swift|rb|php|vue|svelte)$/i;
5345
6160
  function summariseIssuesByKind(issues) {
5346
6161
  const out = {};
@@ -5410,21 +6225,21 @@ function trigramSimilarity(a, b) {
5410
6225
  return inter / Math.sqrt(A.size * B.size);
5411
6226
  }
5412
6227
  function suggestPaths(root, missingRel, max = 3) {
5413
- const targetBase = path19.basename(missingRel).toLowerCase();
6228
+ const targetBase = path20.basename(missingRel).toLowerCase();
5414
6229
  const ig = buildRepoIgnore(root);
5415
6230
  const candidates = [];
5416
6231
  const walk2 = (dir) => {
5417
6232
  if (candidates.length >= 2e3) return;
5418
6233
  let ents;
5419
6234
  try {
5420
- ents = fs27.readdirSync(dir, { withFileTypes: true });
6235
+ ents = fs30.readdirSync(dir, { withFileTypes: true });
5421
6236
  } catch {
5422
6237
  return;
5423
6238
  }
5424
6239
  for (const ent of ents) {
5425
6240
  if (candidates.length >= 2e3) return;
5426
- const abs = path19.join(dir, ent.name);
5427
- const rel = path19.relative(root, abs).split(path19.sep).join("/");
6241
+ const abs = path20.join(dir, ent.name);
6242
+ const rel = path20.relative(root, abs).split(path20.sep).join("/");
5428
6243
  if (!rel || ig.ignores(rel) || ent.isDirectory() && ig.ignores(`${rel}/`)) continue;
5429
6244
  if (ent.isDirectory()) walk2(abs);
5430
6245
  else if (ent.isFile()) candidates.push(rel);
@@ -5433,7 +6248,7 @@ function suggestPaths(root, missingRel, max = 3) {
5433
6248
  walk2(root);
5434
6249
  const rows = [];
5435
6250
  for (const rel of candidates) {
5436
- const base = path19.basename(rel).toLowerCase();
6251
+ const base = path20.basename(rel).toLowerCase();
5437
6252
  let score = 0;
5438
6253
  if (base === targetBase) score = 1e3;
5439
6254
  else if (base.startsWith(targetBase) || targetBase.startsWith(base)) score = 500;
@@ -5511,12 +6326,12 @@ function validatePlan(input5) {
5511
6326
  const cache = /* @__PURE__ */ new Map();
5512
6327
  const readCached = (rel) => {
5513
6328
  if (cache.has(rel)) return cache.get(rel);
5514
- const resolved = path19.resolve(input5.root, rel);
5515
- const rc = path19.relative(input5.root, resolved);
5516
- if (rc.startsWith("..") || path19.isAbsolute(rc)) return void 0;
5517
- if (!fs27.existsSync(resolved) || !fs27.statSync(resolved).isFile()) return void 0;
6329
+ const resolved = path20.resolve(input5.root, rel);
6330
+ const rc = path20.relative(input5.root, resolved);
6331
+ if (rc.startsWith("..") || path20.isAbsolute(rc)) return void 0;
6332
+ if (!fs30.existsSync(resolved) || !fs30.statSync(resolved).isFile()) return void 0;
5518
6333
  try {
5519
- const buf = fs27.readFileSync(resolved);
6334
+ const buf = fs30.readFileSync(resolved);
5520
6335
  const isBinary = buf.includes(0);
5521
6336
  const text = isBinary ? "" : buf.toString("utf8");
5522
6337
  const lines = isBinary ? 0 : text.split(/\r?\n/).length;
@@ -5530,10 +6345,10 @@ function validatePlan(input5) {
5530
6345
  const pathClaims = mergePathClaims(input5.planText);
5531
6346
  for (const c of pathClaims) {
5532
6347
  if (c.isCreate) continue;
5533
- const resolved = path19.resolve(input5.root, c.path);
5534
- const rel = path19.relative(input5.root, resolved);
5535
- if (rel.startsWith("..") || path19.isAbsolute(rel)) continue;
5536
- if (!fs27.existsSync(resolved)) {
6348
+ const resolved = path20.resolve(input5.root, c.path);
6349
+ const rel = path20.relative(input5.root, resolved);
6350
+ if (rel.startsWith("..") || path20.isAbsolute(rel)) continue;
6351
+ if (!fs30.existsSync(resolved)) {
5537
6352
  const suggestions = suggestPaths(input5.root, c.path);
5538
6353
  issues.push({
5539
6354
  severity: "warning",
@@ -5553,8 +6368,8 @@ function validatePlan(input5) {
5553
6368
  if (!Number.isFinite(lo) || !Number.isFinite(hi)) continue;
5554
6369
  const filePath = pathNear(rangeText, rm2.index);
5555
6370
  if (!filePath) continue;
5556
- const cached = readCached(filePath);
5557
- const lines = cached?.isBinary ? void 0 : cached?.lines;
6371
+ const cached2 = readCached(filePath);
6372
+ const lines = cached2?.isBinary ? void 0 : cached2?.lines;
5558
6373
  if (lines !== void 0 && hi > lines) {
5559
6374
  issues.push({
5560
6375
  severity: "warning",
@@ -5571,8 +6386,8 @@ function validatePlan(input5) {
5571
6386
  if (!Number.isFinite(n)) continue;
5572
6387
  const filePath = pathNear(rangeText, rm2.index);
5573
6388
  if (!filePath) continue;
5574
- const cached = readCached(filePath);
5575
- const lines = cached?.isBinary ? void 0 : cached?.lines;
6389
+ const cached2 = readCached(filePath);
6390
+ const lines = cached2?.isBinary ? void 0 : cached2?.lines;
5576
6391
  if (lines !== void 0 && n > lines) {
5577
6392
  issues.push({
5578
6393
  severity: "warning",
@@ -5588,8 +6403,8 @@ function validatePlan(input5) {
5588
6403
  const fp = rm2[1].trim().replace(/^\.\//, "");
5589
6404
  const n = parseInt(rm2[2], 10);
5590
6405
  if (!Number.isFinite(n)) continue;
5591
- const cached = readCached(fp);
5592
- const lines = cached?.isBinary ? void 0 : cached?.lines;
6406
+ const cached2 = readCached(fp);
6407
+ const lines = cached2?.isBinary ? void 0 : cached2?.lines;
5593
6408
  if (lines !== void 0 && n > lines) {
5594
6409
  issues.push({
5595
6410
  severity: "warning",
@@ -5607,8 +6422,8 @@ function validatePlan(input5) {
5607
6422
  const pathNearCell = near.match(/`([^`\n/]+\/[^`\n]+?\.[a-z0-9]{1,6})`/i);
5608
6423
  if (!pathNearCell) continue;
5609
6424
  const fp = pathNearCell[1].replace(/^\.\//, "");
5610
- const cached = readCached(fp);
5611
- if (!cached || cached.isBinary) continue;
6425
+ const cached2 = readCached(fp);
6426
+ if (!cached2 || cached2.isBinary) continue;
5612
6427
  const symRe = /`([A-Za-z_][A-Za-z0-9_]{2,50})`/g;
5613
6428
  let sm;
5614
6429
  while ((sm = symRe.exec(line2)) !== null) {
@@ -5618,8 +6433,8 @@ function validatePlan(input5) {
5618
6433
  )) {
5619
6434
  continue;
5620
6435
  }
5621
- if (!cached.text.includes(sym)) {
5622
- const suggestions = suggestSymbols(cached.text, sym);
6436
+ if (!cached2.text.includes(sym)) {
6437
+ const suggestions = suggestSymbols(cached2.text, sym);
5623
6438
  issues.push({
5624
6439
  severity: "warning",
5625
6440
  kind: "symbol_not_found",
@@ -5700,9 +6515,9 @@ ${result.content}
5700
6515
  }
5701
6516
  continue;
5702
6517
  }
5703
- const resolved = path20.resolve(root, cleaned);
5704
- const rc = path20.relative(root, resolved);
5705
- if (rc.startsWith("..") || path20.isAbsolute(rc)) {
6518
+ const resolved = path21.resolve(root, cleaned);
6519
+ const rc = path21.relative(root, resolved);
6520
+ if (rc.startsWith("..") || path21.isAbsolute(rc)) {
5706
6521
  bus.emit({
5707
6522
  kind: "validation_issue",
5708
6523
  runId,
@@ -5713,7 +6528,7 @@ ${result.content}
5713
6528
  });
5714
6529
  continue;
5715
6530
  }
5716
- if (!fs28.existsSync(resolved)) {
6531
+ if (!fs31.existsSync(resolved)) {
5717
6532
  bus.emit({
5718
6533
  kind: "validation_issue",
5719
6534
  runId,
@@ -5724,7 +6539,7 @@ ${result.content}
5724
6539
  });
5725
6540
  continue;
5726
6541
  }
5727
- const st = fs28.statSync(resolved);
6542
+ const st = fs31.statSync(resolved);
5728
6543
  if (!st.isFile()) continue;
5729
6544
  const n = Math.min(st.size, MAX_BYTES_PER_FULL_READ);
5730
6545
  const cap = budget.canRead(n);
@@ -5738,7 +6553,7 @@ _(skipped: ${cap.reason})_
5738
6553
  }
5739
6554
  let buf;
5740
6555
  try {
5741
- buf = fs28.readFileSync(resolved);
6556
+ buf = fs31.readFileSync(resolved);
5742
6557
  } catch {
5743
6558
  continue;
5744
6559
  }
@@ -5931,6 +6746,9 @@ async function runPlanner(input5) {
5931
6746
  }
5932
6747
  } : void 0
5933
6748
  });
6749
+ if (input5.auth) {
6750
+ bus.emit({ kind: "auth_info", runId, ...describeAuth(input5.auth) });
6751
+ }
5934
6752
  const merged = { ...input5, events: bus, runId };
5935
6753
  const systemPrompt = input5.systemPrompt;
5936
6754
  let scoutSummary;
@@ -6071,8 +6889,16 @@ async function runPlanner(input5) {
6071
6889
  }
6072
6890
 
6073
6891
  // src/planner/planner-limit-messages.ts
6074
- function printPlannerApiCostNotice() {
6892
+ function printPlannerApiCostNotice(authMode = "api-key") {
6075
6893
  blank();
6894
+ if (authMode === "subscription") {
6895
+ step("Usage");
6896
+ info(
6897
+ "`squad new-plan --api` runs on your Claude subscription \u2014 there is no per-token API bill. Agent SDK usage draws on the same usage limits as Claude and Claude Code, so a long planning run consumes part of your window. Hitting the limit means waiting for the reset, not a larger invoice."
6898
+ );
6899
+ blank();
6900
+ return;
6901
+ }
6076
6902
  step("Billing");
6077
6903
  info(
6078
6904
  "`squad new-plan --api` sends your intake and repo context to the configured provider. Each model round is billed like any other API usage (input tokens, output tokens, and cache-related tokens when applicable). Choosing \u201CContinue\u201D after a limit runs more rounds and usually increases cost."
@@ -6095,12 +6921,12 @@ function tokenSummary(ctx) {
6095
6921
  const snap = ctx.budgetSnapshot;
6096
6922
  return `Tokens so far: ${snap.usage.inputTokens} in / ${snap.usage.outputTokens} out for this session.`;
6097
6923
  }
6098
- function kindDetail(ctx) {
6924
+ function kindDetail(ctx, authMode) {
6099
6925
  switch (ctx.kind) {
6100
6926
  case "max_output_tokens":
6101
6927
  return [
6102
6928
  `The model hit the per-request output cap (${ctx.maxOutputTokens} completion tokens). Long plans can stop mid-markdown even though the session is otherwise healthy.`,
6103
- "You can continue: squad-kit will ask the model to append the rest of the plan in a follow-up request (extra tokens apply).",
6929
+ "You can continue: squad-kit will ask the model to append the rest of the plan in a follow-up request " + (authMode === "subscription" ? "(it draws further on your Claude usage limits)." : "(extra tokens apply)."),
6104
6930
  tokenSummary(ctx)
6105
6931
  ];
6106
6932
  case "max_iterations":
@@ -6123,9 +6949,9 @@ function kindDetail(ctx) {
6123
6949
  ];
6124
6950
  }
6125
6951
  }
6126
- function printPlannerLimitExplanation(ctx) {
6952
+ function printPlannerLimitExplanation(ctx, authMode = "api-key") {
6127
6953
  warning(kindTitle(ctx.kind));
6128
- for (const line2 of kindDetail(ctx)) {
6954
+ for (const line2 of kindDetail(ctx, authMode)) {
6129
6955
  info(line2);
6130
6956
  }
6131
6957
  kv("session so far", `${snapSummary(ctx)}`, 18);
@@ -6176,6 +7002,23 @@ function thinkingConfigFromProviderSpecific(ps) {
6176
7002
  }
6177
7003
 
6178
7004
  // src/planner/runtimes/agent-sdk-runtime.ts
7005
+ var ACCOUNT_INFO_TIMEOUT_MS = 2e3;
7006
+ var MAX_RATE_LIMIT_RETRY_SEC2 = 90;
7007
+ var enrichedAuthInfoRuns = /* @__PURE__ */ new WeakMap();
7008
+ function markAuthInfoEmitted(bus, runId) {
7009
+ let seen = enrichedAuthInfoRuns.get(bus);
7010
+ if (!seen) {
7011
+ seen = /* @__PURE__ */ new Set();
7012
+ enrichedAuthInfoRuns.set(bus, seen);
7013
+ }
7014
+ if (seen.has(runId)) return false;
7015
+ seen.add(runId);
7016
+ return true;
7017
+ }
7018
+ var API_KEY_SOURCES = ["user", "project", "org", "temporary", "oauth"];
7019
+ function asApiKeySource(v) {
7020
+ return API_KEY_SOURCES.find((s) => s === v);
7021
+ }
6179
7022
  var BUILTIN_TOOL_DENY = [
6180
7023
  "Read",
6181
7024
  "Write",
@@ -6347,15 +7190,96 @@ function handleDecodedStreamEvent(decoded, input5) {
6347
7190
  }
6348
7191
  }
6349
7192
  }
7193
+ async function readSdkAccountInfo(q) {
7194
+ const fn = q?.accountInfo;
7195
+ if (typeof fn !== "function") return void 0;
7196
+ let timer;
7197
+ try {
7198
+ const info2 = await Promise.race([
7199
+ fn.call(q),
7200
+ new Promise((resolve2) => {
7201
+ timer = setTimeout(() => resolve2(void 0), ACCOUNT_INFO_TIMEOUT_MS);
7202
+ })
7203
+ ]);
7204
+ if (!info2 || typeof info2 !== "object") return void 0;
7205
+ const o = info2;
7206
+ const out = {};
7207
+ if (typeof o.email === "string") out.email = o.email;
7208
+ if (typeof o.organization === "string") out.organization = o.organization;
7209
+ if (typeof o.subscriptionType === "string") out.subscriptionType = o.subscriptionType;
7210
+ return Object.keys(out).length > 0 ? out : void 0;
7211
+ } catch {
7212
+ return void 0;
7213
+ } finally {
7214
+ if (timer) clearTimeout(timer);
7215
+ }
7216
+ }
6350
7217
  var AgentSdkRuntime = class {
6351
- constructor(modelId, apiKey) {
7218
+ constructor(modelId, auth2) {
6352
7219
  this.modelId = modelId;
6353
- this.apiKey = apiKey;
7220
+ this.auth = auth2;
6354
7221
  }
6355
7222
  modelId;
6356
- apiKey;
7223
+ auth;
6357
7224
  kind = "agent-sdk";
6358
7225
  providerName = "anthropic";
7226
+ /** Safe-to-log auth projection — never carries the key or the token. */
7227
+ authDescriptor() {
7228
+ return describeAuth(this.auth);
7229
+ }
7230
+ /**
7231
+ * Handle a `system` message. On `init` this emits the enriched `auth_info` (once per run, even
7232
+ * though scout and draft are separate instances); on `api_retry` it forwards a rate limit through
7233
+ * the existing 0.8.0 rate-limit channel so the console countdown and CLI spinner keep working.
7234
+ */
7235
+ async handleSystemMessage(m, input5, ctx) {
7236
+ if (m.subtype === "init") {
7237
+ const apiKeySource = asApiKeySource(m.apiKeySource);
7238
+ if (!apiKeySource || !markAuthInfoEmitted(input5.bus, input5.runId)) return;
7239
+ const account = ctx.withAccountInfo && this.auth.mode === "subscription" ? await readSdkAccountInfo(ctx.query) : void 0;
7240
+ input5.bus.emit({
7241
+ kind: "auth_info",
7242
+ runId: input5.runId,
7243
+ ...this.authDescriptor(),
7244
+ apiKeySource,
7245
+ ...account ? { account } : {}
7246
+ });
7247
+ return;
7248
+ }
7249
+ if (m.subtype === "api_retry" && detectAuthShapedSdkError(m.error) === "rate_limit") {
7250
+ const delayMs = typeof m.retry_delay_ms === "number" ? m.retry_delay_ms : 0;
7251
+ const waitSec = Math.max(1, Math.ceil(delayMs / 1e3));
7252
+ input5.bus.emit({
7253
+ kind: "rate_limit",
7254
+ runId: input5.runId,
7255
+ turn: ctx.turn,
7256
+ retryAfterSec: waitSec,
7257
+ waitSec,
7258
+ capSec: MAX_RATE_LIMIT_RETRY_SEC2,
7259
+ phase: "retrying",
7260
+ provider: "anthropic"
7261
+ });
7262
+ }
7263
+ }
7264
+ /**
7265
+ * Turn an auth-shaped SDK failure into an actionable error. Rate limits also emit the terminal
7266
+ * `rate_limit` event so the UI closes its countdown before the throw.
7267
+ */
7268
+ authError(signal, input5, turn) {
7269
+ if (signal === "rate_limit") {
7270
+ input5.bus.emit({
7271
+ kind: "rate_limit",
7272
+ runId: input5.runId,
7273
+ turn,
7274
+ retryAfterSec: void 0,
7275
+ waitSec: 0,
7276
+ capSec: MAX_RATE_LIMIT_RETRY_SEC2,
7277
+ phase: "aborted",
7278
+ provider: "anthropic"
7279
+ });
7280
+ }
7281
+ return new Error(authErrorMessage(signal, authErrorContextFrom(this.auth)));
7282
+ }
6359
7283
  async runDraft(input5) {
6360
7284
  const { query, createSdkMcpServer, tool: tool5 } = await import("@anthropic-ai/claude-agent-sdk");
6361
7285
  const sdkTools = input5.tools.map(
@@ -6390,7 +7314,7 @@ var AgentSdkRuntime = class {
6390
7314
  persistSession: false,
6391
7315
  settingSources: [],
6392
7316
  abortController: ac,
6393
- env: { ...process.env, ANTHROPIC_API_KEY: this.apiKey },
7317
+ env: buildSdkEnv(this.auth),
6394
7318
  thinking,
6395
7319
  effort
6396
7320
  };
@@ -6400,6 +7324,7 @@ var AgentSdkRuntime = class {
6400
7324
  let finishedNormally = false;
6401
7325
  let incompleteKind;
6402
7326
  let userCancelled = false;
7327
+ let authSignal;
6403
7328
  const think = initialThinkingState();
6404
7329
  try {
6405
7330
  const q = query({
@@ -6421,6 +7346,7 @@ var AgentSdkRuntime = class {
6421
7346
  break;
6422
7347
  }
6423
7348
  case "assistant": {
7349
+ authSignal = detectAuthShapedSdkError(m.error) ?? authSignal;
6424
7350
  think.blockIndexCounter = 0;
6425
7351
  think.inThinkingBlock = false;
6426
7352
  think.thinkingBlockIndex = -1;
@@ -6447,10 +7373,17 @@ var AgentSdkRuntime = class {
6447
7373
  finishedNormally = false;
6448
7374
  } else if (typeof sub === "string" && sub.startsWith("error")) {
6449
7375
  finishedNormally = false;
7376
+ const signal = authSignal ?? detectAuthShapedSdkError(m.errors);
7377
+ if (signal) throw this.authError(signal, input5, assistantTurns);
6450
7378
  }
6451
7379
  break;
6452
7380
  }
6453
7381
  case "system":
7382
+ await this.handleSystemMessage(m, input5, {
7383
+ query: q,
7384
+ withAccountInfo: true,
7385
+ turn: assistantTurns
7386
+ });
6454
7387
  break;
6455
7388
  default:
6456
7389
  break;
@@ -6509,12 +7442,13 @@ var AgentSdkRuntime = class {
6509
7442
  includePartialMessages: true,
6510
7443
  persistSession: false,
6511
7444
  settingSources: [],
6512
- env: { ...process.env, ANTHROPIC_API_KEY: this.apiKey },
7445
+ env: buildSdkEnv(this.auth),
6513
7446
  thinking,
6514
7447
  effort
6515
7448
  };
6516
7449
  let usage = { inputTokens: 0, outputTokens: 0 };
6517
7450
  let assistantTurns = 0;
7451
+ let authSignal;
6518
7452
  const think = initialThinkingState();
6519
7453
  const ac = new AbortController();
6520
7454
  const onAbort = () => ac.abort();
@@ -6539,6 +7473,7 @@ var AgentSdkRuntime = class {
6539
7473
  });
6540
7474
  break;
6541
7475
  case "assistant":
7476
+ authSignal = detectAuthShapedSdkError(m.error) ?? authSignal;
6542
7477
  think.blockIndexCounter = 0;
6543
7478
  think.inThinkingBlock = false;
6544
7479
  think.thinkingBlockIndex = -1;
@@ -6549,8 +7484,17 @@ var AgentSdkRuntime = class {
6549
7484
  usage = usageFromAgentSdkResult(
6550
7485
  m.usage
6551
7486
  );
7487
+ if (typeof m.subtype === "string" && m.subtype.startsWith("error")) {
7488
+ const signal = authSignal ?? detectAuthShapedSdkError(m.errors);
7489
+ if (signal) throw this.authError(signal, input5, assistantTurns);
7490
+ }
6552
7491
  break;
6553
7492
  case "system":
7493
+ await this.handleSystemMessage(m, input5, {
7494
+ query: q,
7495
+ withAccountInfo: false,
7496
+ turn: assistantTurns
7497
+ });
6554
7498
  break;
6555
7499
  default:
6556
7500
  break;
@@ -6572,14 +7516,40 @@ var AgentSdkRuntime = class {
6572
7516
  }
6573
7517
  };
6574
7518
 
7519
+ // src/planner/runtimes/auth-runtime-mismatch.ts
7520
+ var SUBSCRIPTION_NEEDS_AGENT_SDK_LEAD = "Subscription auth needs the Claude Agent SDK runtime.";
7521
+ var SUBSCRIPTION_VERCEL_RUNTIME_HINT = "Remove `planner.runtime.anthropic: vercel` from .squad/config.yaml (the default is agent-sdk), or set `planner.auth.anthropic: api-key` and provide an ANTHROPIC_API_KEY.";
7522
+ function subscriptionProviderMismatchHint(provider) {
7523
+ return `The planner provider is \`${provider}\`, which is API-key only. Switch to Anthropic with \`squad config set planner\`, or set \`planner.auth.anthropic: api-key\` and provide an ANTHROPIC_API_KEY.`;
7524
+ }
7525
+ function plannerAuthRuntimeMismatchMessage(provider) {
7526
+ return `${SUBSCRIPTION_NEEDS_AGENT_SDK_LEAD} ${provider === "anthropic" ? SUBSCRIPTION_VERCEL_RUNTIME_HINT : subscriptionProviderMismatchHint(provider)}`;
7527
+ }
7528
+
6575
7529
  // src/planner/runtimes/index.ts
7530
+ var PlannerAuthRuntimeMismatchError = class extends Error {
7531
+ constructor(provider, runtimeChoice) {
7532
+ super(plannerAuthRuntimeMismatchMessage(provider));
7533
+ this.provider = provider;
7534
+ this.runtimeChoice = runtimeChoice;
7535
+ this.name = "PlannerAuthRuntimeMismatchError";
7536
+ }
7537
+ provider;
7538
+ runtimeChoice;
7539
+ };
6576
7540
  function resolveRuntime(input5) {
6577
- if (input5.provider === "anthropic") {
6578
- const choice = input5.anthropicRuntime ?? "agent-sdk";
6579
- if (choice === "agent-sdk") return new AgentSdkRuntime(input5.modelId, input5.apiKey);
6580
- return new VercelRuntime("anthropic", input5.modelId, input5.apiKey);
7541
+ const choice = input5.provider === "anthropic" ? input5.anthropicRuntime ?? "agent-sdk" : "vercel";
7542
+ const auth2 = input5.auth;
7543
+ if (auth2.mode === "subscription") {
7544
+ if (input5.provider !== "anthropic" || choice !== "agent-sdk") {
7545
+ throw new PlannerAuthRuntimeMismatchError(input5.provider, choice);
7546
+ }
7547
+ return new AgentSdkRuntime(input5.modelId, auth2);
7548
+ }
7549
+ if (input5.provider === "anthropic" && choice === "agent-sdk") {
7550
+ return new AgentSdkRuntime(input5.modelId, auth2);
6581
7551
  }
6582
- return new VercelRuntime(input5.provider, input5.modelId, input5.apiKey);
7552
+ return new VercelRuntime(input5.provider, input5.modelId, auth2.key);
6583
7553
  }
6584
7554
 
6585
7555
  // src/planner/budget.ts
@@ -6651,8 +7621,8 @@ var Budget = class {
6651
7621
  };
6652
7622
 
6653
7623
  // src/planner/writer.ts
6654
- import fs29 from "fs";
6655
- import path21 from "path";
7624
+ import fs32 from "fs";
7625
+ import path22 from "path";
6656
7626
  function planFileSlugParts(config2, story) {
6657
7627
  const baseSlug = slugify(story.id);
6658
7628
  const trackerId = config2.naming.includeTrackerId && config2.tracker.type !== "none" ? trackerIdForFilename(config2.tracker.type, story.id) : "";
@@ -6661,8 +7631,8 @@ function planFileSlugParts(config2, story) {
6661
7631
  }
6662
7632
  function writePlanFile(input5) {
6663
7633
  const { paths, config: config2, story } = input5;
6664
- const featurePlanDir = path21.join(paths.plansDir, story.feature);
6665
- fs29.mkdirSync(featurePlanDir, { recursive: true });
7634
+ const featurePlanDir = path22.join(paths.plansDir, story.feature);
7635
+ fs32.mkdirSync(featurePlanDir, { recursive: true });
6666
7636
  if (input5.partial) {
6667
7637
  return writePartialPlanFile(input5, featurePlanDir, paths, config2, story);
6668
7638
  }
@@ -6677,14 +7647,14 @@ function writePlanFile(input5) {
6677
7647
  } else {
6678
7648
  nn = scanPlans(paths.plansDir).nextGlobal;
6679
7649
  const { baseSlug, trackerPart } = planFileSlugParts(config2, story);
6680
- targetPath = path21.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.md`);
7650
+ targetPath = path22.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.md`);
6681
7651
  }
6682
7652
  const body = `${input5.metadataHeader.trimEnd()}
6683
7653
 
6684
7654
  ${input5.planBodyMarkdown.trimStart()}
6685
7655
  `;
6686
- fs29.writeFileSync(targetPath, body, "utf8");
6687
- upsertOverviewRow(featurePlanDir, story.feature, nn, path21.basename(targetPath), story, false);
7656
+ fs32.writeFileSync(targetPath, body, "utf8");
7657
+ upsertOverviewRow(featurePlanDir, story.feature, nn, path22.basename(targetPath), story, false);
6688
7658
  return { planFile: targetPath, sequenceNumber: nn, overwrote };
6689
7659
  }
6690
7660
  function writePartialPlanFile(input5, featurePlanDir, paths, config2, story) {
@@ -6699,42 +7669,42 @@ function writePartialPlanFile(input5, featurePlanDir, paths, config2, story) {
6699
7669
  } else {
6700
7670
  nn = scanPlans(paths.plansDir).nextGlobal;
6701
7671
  const { baseSlug, trackerPart } = planFileSlugParts(config2, story);
6702
- targetPath = path21.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.partial.md`);
7672
+ targetPath = path22.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.partial.md`);
6703
7673
  }
6704
7674
  const body = `${input5.metadataHeader.trimEnd()}
6705
7675
 
6706
7676
  ${input5.planBodyMarkdown.trimStart()}
6707
7677
  `;
6708
- fs29.writeFileSync(targetPath, body, "utf8");
6709
- upsertOverviewRow(featurePlanDir, story.feature, nn, path21.basename(targetPath), story, true);
7678
+ fs32.writeFileSync(targetPath, body, "utf8");
7679
+ upsertOverviewRow(featurePlanDir, story.feature, nn, path22.basename(targetPath), story, true);
6710
7680
  return { planFile: targetPath, sequenceNumber: nn, overwrote };
6711
7681
  }
6712
7682
  function findExistingPlan(featureDir, storyId) {
6713
- if (!fs29.existsSync(featureDir)) return void 0;
7683
+ if (!fs32.existsSync(featureDir)) return void 0;
6714
7684
  const pattern = /^(\d{2,})-story-.+\.md$/;
6715
- for (const entry of fs29.readdirSync(featureDir)) {
7685
+ for (const entry of fs32.readdirSync(featureDir)) {
6716
7686
  if (entry.endsWith(".partial.md")) continue;
6717
7687
  const m = entry.match(pattern);
6718
7688
  if (!m) continue;
6719
- if (entry.includes(storyId)) return { absPath: path21.join(featureDir, entry), nn: parseInt(m[1], 10) };
7689
+ if (entry.includes(storyId)) return { absPath: path22.join(featureDir, entry), nn: parseInt(m[1], 10) };
6720
7690
  }
6721
7691
  return void 0;
6722
7692
  }
6723
7693
  function findExistingPartialPlan(featureDir, storyId) {
6724
- if (!fs29.existsSync(featureDir)) return void 0;
7694
+ if (!fs32.existsSync(featureDir)) return void 0;
6725
7695
  const pattern = /^(\d{2,})-story-.+\.partial\.md$/;
6726
- for (const entry of fs29.readdirSync(featureDir)) {
7696
+ for (const entry of fs32.readdirSync(featureDir)) {
6727
7697
  const m = entry.match(pattern);
6728
7698
  if (!m) continue;
6729
- if (entry.includes(storyId)) return { absPath: path21.join(featureDir, entry), nn: parseInt(m[1], 10) };
7699
+ if (entry.includes(storyId)) return { absPath: path22.join(featureDir, entry), nn: parseInt(m[1], 10) };
6730
7700
  }
6731
7701
  return void 0;
6732
7702
  }
6733
7703
  function upsertOverviewRow(featureDir, feature, nn, filename, story, isPartial) {
6734
- const overviewPath = path21.join(featureDir, "00-overview.md");
7704
+ const overviewPath = path22.join(featureDir, "00-overview.md");
6735
7705
  const titleCell = isPartial ? `${story.titleHint ?? story.id} (partial)` : story.titleHint ?? story.id;
6736
7706
  const row = `| ${formatSequence(nn)} | \`${filename}\` | ${titleCell} | ${story.id} | \u2014 |`;
6737
- if (!fs29.existsSync(overviewPath)) {
7707
+ if (!fs32.existsSync(overviewPath)) {
6738
7708
  const content = `# ${feature} \u2014 plan overview
6739
7709
 
6740
7710
  Entry point for the **${feature}** feature. Stories execute in order by their \`NN\` prefix.
@@ -6745,10 +7715,10 @@ Entry point for the **${feature}** feature. Stories execute in order by their \`
6745
7715
  |----|------|-------|------------|------------|
6746
7716
  ${row}
6747
7717
  `;
6748
- fs29.writeFileSync(overviewPath, content, "utf8");
7718
+ fs32.writeFileSync(overviewPath, content, "utf8");
6749
7719
  return;
6750
7720
  }
6751
- const current = fs29.readFileSync(overviewPath, "utf8");
7721
+ const current = fs32.readFileSync(overviewPath, "utf8");
6752
7722
  const lines = current.split("\n");
6753
7723
  const existingIdx = lines.findIndex((l) => l.includes(`\`${filename}\``) || l.includes(`| ${formatSequence(nn)} |`));
6754
7724
  if (existingIdx >= 0) {
@@ -6757,7 +7727,7 @@ ${row}
6757
7727
  const tableEnd = findTableEnd(lines);
6758
7728
  lines.splice(tableEnd, 0, row);
6759
7729
  }
6760
- fs29.writeFileSync(overviewPath, lines.join("\n"), "utf8");
7730
+ fs32.writeFileSync(overviewPath, lines.join("\n"), "utf8");
6761
7731
  }
6762
7732
  function findTableEnd(lines) {
6763
7733
  let inTable = false;
@@ -6810,16 +7780,16 @@ function buildCopyPlanPromptMarkdown(config2, intakeContent) {
6810
7780
  }
6811
7781
 
6812
7782
  // src/core/last-run.ts
6813
- import fs30 from "fs/promises";
6814
- import path22 from "path";
7783
+ import fs33 from "fs/promises";
7784
+ import path23 from "path";
6815
7785
  async function writeLastRun(paths, rec) {
6816
- const file = path22.join(paths.squadDir, ".last-run.json");
6817
- await fs30.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
7786
+ const file = path23.join(paths.squadDir, ".last-run.json");
7787
+ await fs33.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
6818
7788
  }
6819
7789
  async function readLastRun(paths) {
6820
- const file = path22.join(paths.squadDir, ".last-run.json");
7790
+ const file = path23.join(paths.squadDir, ".last-run.json");
6821
7791
  try {
6822
- const raw = await fs30.readFile(file, "utf8");
7792
+ const raw = await fs33.readFile(file, "utf8");
6823
7793
  const parsed = JSON.parse(raw);
6824
7794
  if (parsed.version !== 1) return void 0;
6825
7795
  return parsed;
@@ -6852,8 +7822,8 @@ function formatPlannerCacheLine(opts) {
6852
7822
  }
6853
7823
 
6854
7824
  // src/core/plans-index.ts
6855
- import fs31 from "fs";
6856
- import path23 from "path";
7825
+ import fs34 from "fs";
7826
+ import path24 from "path";
6857
7827
  var MAX_ENTRIES = 30;
6858
7828
  function firstMarkdownH1(src) {
6859
7829
  const lines = src.split(/\r?\n/);
@@ -6868,12 +7838,12 @@ function collectPlanMarkdownFiles(plansDir) {
6868
7838
  const walk2 = (dir) => {
6869
7839
  let ents;
6870
7840
  try {
6871
- ents = fs31.readdirSync(dir, { withFileTypes: true });
7841
+ ents = fs34.readdirSync(dir, { withFileTypes: true });
6872
7842
  } catch {
6873
7843
  return;
6874
7844
  }
6875
7845
  for (const ent of ents) {
6876
- const abs = path23.join(dir, ent.name);
7846
+ const abs = path24.join(dir, ent.name);
6877
7847
  if (ent.isDirectory()) {
6878
7848
  walk2(abs);
6879
7849
  } else if (ent.isFile() && ent.name.endsWith(".md")) {
@@ -6882,7 +7852,7 @@ function collectPlanMarkdownFiles(plansDir) {
6882
7852
  }
6883
7853
  }
6884
7854
  };
6885
- if (fs31.existsSync(plansDir)) walk2(plansDir);
7855
+ if (fs34.existsSync(plansDir)) walk2(plansDir);
6886
7856
  out.sort((a, b) => a.localeCompare(b));
6887
7857
  return out.slice(0, MAX_ENTRIES);
6888
7858
  }
@@ -6891,13 +7861,13 @@ function buildPlansIndex(paths) {
6891
7861
  if (files.length === 0) return "(no prior plans yet)";
6892
7862
  const bullets = [];
6893
7863
  for (const abs of files) {
6894
- const rel = path23.relative(paths.root, abs).split(path23.sep).join("/");
7864
+ const rel = path24.relative(paths.root, abs).split(path24.sep).join("/");
6895
7865
  let title = "";
6896
7866
  try {
6897
- const raw = fs31.readFileSync(abs, "utf8");
6898
- title = firstMarkdownH1(raw) ?? path23.basename(abs);
7867
+ const raw = fs34.readFileSync(abs, "utf8");
7868
+ title = firstMarkdownH1(raw) ?? path24.basename(abs);
6899
7869
  } catch {
6900
- title = path23.basename(abs);
7870
+ title = path24.basename(abs);
6901
7871
  }
6902
7872
  bullets.push(`- ${rel} \u2014 "${title}"`);
6903
7873
  }
@@ -7157,9 +8127,10 @@ function decideMode(opts, config2) {
7157
8127
  }
7158
8128
  if (opts.api) return "api";
7159
8129
  if (opts.copy) return "copy";
7160
- const enabled = config2.planner?.enabled;
7161
- const key = enabled && config2.planner?.provider ? readProviderKey(config2.planner.provider) : void 0;
7162
- return enabled && key ? "api" : "copy";
8130
+ const planner = config2.planner;
8131
+ if (!planner?.enabled || !planner.provider) return "copy";
8132
+ if (planner.provider === "anthropic" && planner.auth?.anthropic === "subscription") return "api";
8133
+ return readProviderKey(planner.provider) ? "api" : "copy";
7163
8134
  }
7164
8135
  async function runNewPlan(intakePath, opts) {
7165
8136
  const root = requireSquadRoot();
@@ -7170,7 +8141,7 @@ async function runNewPlan(intakePath, opts) {
7170
8141
  const story = intakePath ? resolveFromPath(intakePath, stories, root) : await pickStory(stories, { all: !!opts.all, interactive, feature: opts.feature });
7171
8142
  if (!story) return;
7172
8143
  if (story.planFile) {
7173
- const proceed = await confirmOverwrite(story, interactive, !!opts.yes);
8144
+ const proceed = await confirmOverwrite2(story, interactive, !!opts.yes);
7174
8145
  if (!proceed) return;
7175
8146
  }
7176
8147
  const mode = decideMode(opts, config2);
@@ -7181,8 +8152,8 @@ async function runNewPlan(intakePath, opts) {
7181
8152
  }
7182
8153
  }
7183
8154
  function resolveFromPath(intakePath, stories, root) {
7184
- const resolved = path24.resolve(intakePath.trim());
7185
- if (!fs32.existsSync(resolved)) {
8155
+ const resolved = path25.resolve(intakePath.trim());
8156
+ if (!fs35.existsSync(resolved)) {
7186
8157
  throw new Error(
7187
8158
  `Intake not found: ${resolved}. Run \`squad list\` to see intakes, or \`squad new-story\` to create one first.`
7188
8159
  );
@@ -7194,7 +8165,7 @@ function resolveFromPath(intakePath, stories, root) {
7194
8165
  );
7195
8166
  }
7196
8167
  function relativeStoriesDir(root) {
7197
- return path24.relative(root, path24.join(root, ".squad", "stories"));
8168
+ return path25.relative(root, path25.join(root, ".squad", "stories"));
7198
8169
  }
7199
8170
  async function pickStory(stories, opts) {
7200
8171
  const candidates = opts.all ? stories : stories.filter((s) => !s.planFile);
@@ -7225,7 +8196,7 @@ async function pickStory(stories, opts) {
7225
8196
  value: s.intakePath
7226
8197
  }))
7227
8198
  });
7228
- return candidates.find((s) => path24.resolve(s.intakePath) === path24.resolve(pick));
8199
+ return candidates.find((s) => path25.resolve(s.intakePath) === path25.resolve(pick));
7229
8200
  }
7230
8201
  function formatLabel(s) {
7231
8202
  const head = `${s.feature} / ${s.id}`;
@@ -7233,7 +8204,7 @@ function formatLabel(s) {
7233
8204
  const plannedTag = s.planFile ? ` ${theme_exports.warn("(planned)")}` : "";
7234
8205
  return head + tail + plannedTag;
7235
8206
  }
7236
- async function confirmOverwrite(story, interactive, yes) {
8207
+ async function confirmOverwrite2(story, interactive, yes) {
7237
8208
  warning(`A plan already exists for this intake:`);
7238
8209
  kv("plan", `.squad/plans/${story.feature}/${story.planFile}`);
7239
8210
  info("Regenerating will replace it when your planner writes the new version.");
@@ -7243,7 +8214,7 @@ async function confirmOverwrite(story, interactive, yes) {
7243
8214
  `Plan already exists (.squad/plans/${story.feature}/${story.planFile}). Run \`squad new-plan\` with \`--yes\` to overwrite non-interactively, or in a TTY to confirm in the prompt.`
7244
8215
  );
7245
8216
  }
7246
- const go = await confirm6({ message: "Proceed and regenerate?", default: false });
8217
+ const go = await confirm7({ message: "Proceed and regenerate?", default: false });
7247
8218
  return go;
7248
8219
  }
7249
8220
  function printPlannerRunIncompleteSummary(result, relPlanFile) {
@@ -7258,6 +8229,10 @@ function printPlannerRunIncompleteSummary(result, relPlanFile) {
7258
8229
  info("The file uses the `.partial.md` suffix and YAML `squad-kit-plan-status: partial`.");
7259
8230
  info("Raise limits in `.squad/config.yaml` (`planner.budget`, `planner.maxOutputTokens`) or re-run when ready.");
7260
8231
  }
8232
+ function formatAuthRow(auth2) {
8233
+ if (auth2.mode === "subscription") return "subscription (Claude login)";
8234
+ return `api-key (${describeAuth(auth2).credentialHint})`;
8235
+ }
7261
8236
  async function emitViaApi(story, paths, config2, opts) {
7262
8237
  const planner = config2.planner;
7263
8238
  if (!planner?.enabled) {
@@ -7265,18 +8240,13 @@ async function emitViaApi(story, paths, config2, opts) {
7265
8240
  "Direct planner API is not configured. Run `squad init --force` to enable it, or `squad new-plan --copy` for the manual copy-paste flow."
7266
8241
  );
7267
8242
  }
7268
- const apiKey = readProviderKey(planner.provider);
7269
- if (!apiKey) {
7270
- throw new Error(
7271
- `Missing ${providerEnvVar(planner.provider)}. Run \`squad config set planner\` to save a key to secrets, or export the env var, or run \`squad new-plan --copy\` without the API.`
7272
- );
7273
- }
8243
+ const auth2 = resolvePlannerAuthForCwd(planner.provider, planner);
7274
8244
  const modelId = modelFor(planner.provider, "plan", planner.modelOverride);
7275
8245
  const anthropicRuntimeChoice = opts.anthropicRuntime ?? planner.runtime?.anthropic ?? "agent-sdk";
7276
8246
  const draftRuntime = resolveRuntime({
7277
8247
  provider: planner.provider,
7278
8248
  modelId,
7279
- apiKey,
8249
+ auth: auth2,
7280
8250
  anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
7281
8251
  });
7282
8252
  const thinkingCli = opts.noThinking ? "off" : void 0;
@@ -7295,10 +8265,11 @@ async function emitViaApi(story, paths, config2, opts) {
7295
8265
  step(`planning ${story.feature} / ${story.id}`);
7296
8266
  kv("provider", planner.provider);
7297
8267
  kv("model", modelId);
8268
+ kv("auth", formatAuthRow(auth2));
7298
8269
  blank();
7299
8270
  const interactive = !opts.yes && Boolean(process.stdin.isTTY);
7300
8271
  if (interactive) {
7301
- printPlannerApiCostNotice();
8272
+ printPlannerApiCostNotice(auth2.mode);
7302
8273
  }
7303
8274
  const mapSpinner = spinner("building repo map\u2026");
7304
8275
  const repoMap = buildRepoMap(paths.root, { format: "tree" });
@@ -7330,7 +8301,7 @@ async function emitViaApi(story, paths, config2, opts) {
7330
8301
  scoutRuntime = resolveRuntime({
7331
8302
  provider: planner.provider,
7332
8303
  modelId: scoutModelId,
7333
- apiKey,
8304
+ auth: auth2,
7334
8305
  anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
7335
8306
  });
7336
8307
  scoutSystemPrompt = composeScoutSystemPrompt({
@@ -7369,9 +8340,9 @@ async function emitViaApi(story, paths, config2, opts) {
7369
8340
  dispatchNewPlanApiPlannerEvent(apiUiCtx, e);
7370
8341
  });
7371
8342
  const decideOnLimit = interactive ? async (ctx) => {
7372
- printPlannerLimitExplanation(ctx);
8343
+ printPlannerLimitExplanation(ctx, auth2.mode);
7373
8344
  const ans = await select6({
7374
- message: "Continue this planning session? (Continuing sends more API requests and is billed.)",
8345
+ message: auth2.mode === "subscription" ? "Continue this planning session? (Continuing runs more model rounds against your Claude usage limits.)" : "Continue this planning session? (Continuing sends more API requests and is billed.)",
7375
8346
  choices: [
7376
8347
  { name: "Continue \u2014 extend limits for this run", value: "continue" },
7377
8348
  { name: "Stop \u2014 save partial plan and exit", value: "cancel" }
@@ -7387,6 +8358,7 @@ async function emitViaApi(story, paths, config2, opts) {
7387
8358
  runtime: draftRuntime,
7388
8359
  provider: planner.provider,
7389
8360
  modelId,
8361
+ auth: auth2,
7390
8362
  anthropicProviderSpecific,
7391
8363
  systemPrompt,
7392
8364
  userPrompt,
@@ -7452,7 +8424,7 @@ async function emitViaApi(story, paths, config2, opts) {
7452
8424
  metadataHeader: header,
7453
8425
  partial: !success2
7454
8426
  });
7455
- const relPlan = path24.relative(paths.root, planFile);
8427
+ const relPlan = path25.relative(paths.root, planFile);
7456
8428
  try {
7457
8429
  await writeLastRun(paths, {
7458
8430
  stats: result.stats,
@@ -7487,6 +8459,7 @@ async function emitViaApi(story, paths, config2, opts) {
7487
8459
  durationMs: result.validation?.durationMs
7488
8460
  },
7489
8461
  plannerRuntime: { kind: draftRuntime.kind, provider: planner.provider },
8462
+ authMode: describeAuth(auth2).mode,
7490
8463
  providerOptionsSnapshot: anthropicProviderSpecific ? { anthropic: anthropicProviderSpecific } : void 0
7491
8464
  });
7492
8465
  } catch {
@@ -7588,14 +8561,14 @@ async function emitViaApi(story, paths, config2, opts) {
7588
8561
  info("Note: file-read budget was reached; the model finalised without further reads.");
7589
8562
  }
7590
8563
  blank();
7591
- info(`next \u2192 open a new agent chat and attach only ${path24.basename(planFile)}`);
8564
+ info(`next \u2192 open a new agent chat and attach only ${path25.basename(planFile)}`);
7592
8565
  }
7593
8566
  async function emitCopyPrompt(story, paths, config2, clipboard) {
7594
8567
  const intakeContent = readFile(story.intakePath);
7595
8568
  const composed = buildCopyPlanPromptMarkdown(config2, intakeContent);
7596
- const promptFile = path24.join(paths.squadDir, ".last-copy-prompt.md");
7597
- fs32.writeFileSync(promptFile, composed, "utf8");
7598
- const relPromptFile = path24.relative(paths.root, promptFile);
8569
+ const promptFile = path25.join(paths.squadDir, ".last-copy-prompt.md");
8570
+ fs35.writeFileSync(promptFile, composed, "utf8");
8571
+ const relPromptFile = path25.relative(paths.root, promptFile);
7599
8572
  const bytes = Buffer.byteLength(composed, "utf8");
7600
8573
  const estTokens = Math.round(bytes / 4);
7601
8574
  const clipResult = clipboard ? await copyToClipboard(composed) : { ok: false, reason: "clipboard disabled (--no-clipboard)" };
@@ -7653,7 +8626,7 @@ function printModelBanner(config2) {
7653
8626
  }
7654
8627
 
7655
8628
  // src/commands/status.ts
7656
- import fs33 from "fs";
8629
+ import fs36 from "fs";
7657
8630
  var kw = 28;
7658
8631
  function probeTrackerCredential(config2, overlay) {
7659
8632
  const { client, error } = clientFor(config2, overlay);
@@ -7679,7 +8652,7 @@ async function runStatus() {
7679
8652
  const scan = scanPlans(paths.plansDir);
7680
8653
  const storyCount = listStories(paths).length;
7681
8654
  const planCount = scan.usedNumbers.length;
7682
- const secrets = fs33.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
8655
+ const secrets = fs36.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
7683
8656
  const overlay = overlayTrackerEnv(secrets);
7684
8657
  banner();
7685
8658
  step("squad-kit status");
@@ -7695,12 +8668,7 @@ async function runStatus() {
7695
8668
  const model = modelFor(config2.planner.provider, "plan", config2.planner.modelOverride);
7696
8669
  const overrideNote = config2.planner.modelOverride?.[config2.planner.provider] ? " (override)" : "";
7697
8670
  kv("planner", `${config2.planner.provider}/${model}${overrideNote}`, kw);
7698
- const cred = resolveProviderKey(config2.planner.provider);
7699
- kv(
7700
- "planner key",
7701
- cred ? `set via ${cred.detail}` : `missing \u2014 set ${providerEnvVar(config2.planner.provider)} or run \`squad init\``,
7702
- kw
7703
- );
8671
+ kv("planner auth", buildPlannerAuthLine(config2.planner), kw);
7704
8672
  } else {
7705
8673
  kv("planner", "disabled (copy-paste flow)", kw);
7706
8674
  }
@@ -7711,6 +8679,15 @@ async function runStatus() {
7711
8679
  warning(`duplicate NN numbers: ${scan.duplicates.map(formatSequence).join(", ")}`);
7712
8680
  }
7713
8681
  }
8682
+ function buildPlannerAuthLine(planner) {
8683
+ try {
8684
+ const described = describeAuth(resolvePlannerAuthForCwd(planner.provider, planner));
8685
+ return `${described.mode} \xB7 ${described.credentialHint} (${authReasonText(described.reason, planner.provider)})`;
8686
+ } catch (err2) {
8687
+ if (!(err2 instanceof PlannerAuthUnavailableError)) throw err2;
8688
+ return planner.provider === "anthropic" ? "missing \u2014 run `squad auth login` for your Claude subscription, or `squad config set planner` for an API key" : `missing \u2014 set ${providerEnvVar(planner.provider)} or run \`squad config set planner\``;
8689
+ }
8690
+ }
7714
8691
  function buildTrackerLine(config2, overlay) {
7715
8692
  if (config2.tracker.type === "none") return "none";
7716
8693
  let trackerLine = config2.tracker.type;
@@ -7733,13 +8710,13 @@ function buildTrackerLine(config2, overlay) {
7733
8710
  }
7734
8711
 
7735
8712
  // src/commands/list.ts
7736
- import path25 from "path";
8713
+ import path26 from "path";
7737
8714
  async function runList(opts) {
7738
8715
  const root = requireSquadRoot();
7739
8716
  const paths = buildPaths(root);
7740
8717
  const records = listStories(paths, { feature: opts.feature });
7741
8718
  const rows = records.map((r) => {
7742
- const planAbs = r.planFile ? path25.join(paths.plansDir, r.feature, r.planFile) : void 0;
8719
+ const planAbs = r.planFile ? path26.join(paths.plansDir, r.feature, r.planFile) : void 0;
7743
8720
  const meta = planAbs ? readPlanMetadata(planAbs) : void 0;
7744
8721
  return {
7745
8722
  feature: r.feature,
@@ -7778,8 +8755,8 @@ function stripAnsi(s) {
7778
8755
  }
7779
8756
 
7780
8757
  // src/commands/tracker-link.ts
7781
- import fs34 from "fs";
7782
- import path26 from "path";
8758
+ import fs37 from "fs";
8759
+ import path27 from "path";
7783
8760
  import { input as input4, select as select7 } from "@inquirer/prompts";
7784
8761
  async function runTrackerLink(storyPath, trackerId, opts = {}) {
7785
8762
  const root = requireSquadRoot();
@@ -7826,17 +8803,17 @@ async function runTrackerLink(storyPath, trackerId, opts = {}) {
7826
8803
  `Invalid tracker id "${resolvedTrackerId}" for tracker type "${config2.tracker.type}". Run \`squad config show\` to review the expected format, or \`squad config set tracker\` if the type is wrong.`
7827
8804
  );
7828
8805
  }
7829
- const resolved = path26.resolve(resolvedStoryPath);
7830
- const intakeFile = resolved.endsWith("intake.md") ? resolved : path26.join(resolved, "intake.md");
7831
- if (!fs34.existsSync(intakeFile)) {
8806
+ const resolved = path27.resolve(resolvedStoryPath);
8807
+ const intakeFile = resolved.endsWith("intake.md") ? resolved : path27.join(resolved, "intake.md");
8808
+ if (!fs37.existsSync(intakeFile)) {
7832
8809
  throw new Error(
7833
8810
  `intake.md not found at ${intakeFile}. Run \`squad list\` to see valid paths, or \`squad new-story\` to create a story first.`
7834
8811
  );
7835
8812
  }
7836
- const content = fs34.readFileSync(intakeFile, "utf8");
8813
+ const content = fs37.readFileSync(intakeFile, "utf8");
7837
8814
  const updated = upsertTrackerId(content, resolvedTrackerId);
7838
- fs34.writeFileSync(intakeFile, updated, "utf8");
7839
- success(`Linked tracker id ${resolvedTrackerId} \u2192 ${path26.relative(root, intakeFile)}`);
8815
+ fs37.writeFileSync(intakeFile, updated, "utf8");
8816
+ success(`Linked tracker id ${resolvedTrackerId} \u2192 ${path27.relative(root, intakeFile)}`);
7840
8817
  blank();
7841
8818
  step("Next:");
7842
8819
  info("1) Review the intake \u2014 the Source block at the top now carries the tracker id.");
@@ -7854,8 +8831,8 @@ function upsertTrackerId(content, id) {
7854
8831
  }
7855
8832
 
7856
8833
  // src/commands/doctor-engine.ts
7857
- import fs35 from "fs";
7858
- import path27 from "path";
8834
+ import fs38 from "fs";
8835
+ import path28 from "path";
7859
8836
  async function gatherContext(paths) {
7860
8837
  const ctx = { hasLegacyPromptsDir: false };
7861
8838
  try {
@@ -7864,29 +8841,30 @@ async function gatherContext(paths) {
7864
8841
  ctx.configError = err2;
7865
8842
  }
7866
8843
  try {
7867
- ctx.secrets = fs35.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
8844
+ ctx.secrets = fs38.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
7868
8845
  } catch (err2) {
7869
8846
  ctx.secretsError = err2;
7870
8847
  }
7871
- ctx.hasLegacyPromptsDir = fs35.existsSync(paths.promptsDir);
8848
+ ctx.hasLegacyPromptsDir = fs38.existsSync(paths.promptsDir);
7872
8849
  return ctx;
7873
8850
  }
7874
8851
  function gitignoreHasManagedBlock(repoRoot) {
7875
- const gitignore = path27.join(repoRoot, ".gitignore");
7876
- return fs35.existsSync(gitignore) && fs35.readFileSync(gitignore, "utf8").includes(".squad/secrets.yaml");
8852
+ const gitignore = path28.join(repoRoot, ".gitignore");
8853
+ return fs38.existsSync(gitignore) && fs38.readFileSync(gitignore, "utf8").includes(".squad/secrets.yaml");
7877
8854
  }
7878
8855
  function gitignoreHasTrashPattern(repoRoot) {
7879
- const gitignore = path27.join(repoRoot, ".gitignore");
7880
- return fs35.existsSync(gitignore) && fs35.readFileSync(gitignore, "utf8").includes(SQUAD_TRASH_PATTERN);
8856
+ const gitignore = path28.join(repoRoot, ".gitignore");
8857
+ return fs38.existsSync(gitignore) && fs38.readFileSync(gitignore, "utf8").includes(SQUAD_TRASH_PATTERN);
7881
8858
  }
8859
+ var DOCTOR_AUTH_PROBE_TIMEOUT_MS = 25e3;
7882
8860
  async function checkDirStructure(paths, _ctx, fix) {
7883
- const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !fs35.existsSync(p));
8861
+ const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !fs38.existsSync(p));
7884
8862
  if (need.length === 0) {
7885
8863
  return { id: "dirs", name: ".squad/ directory structure", status: "ok" };
7886
8864
  }
7887
8865
  if (fix) {
7888
8866
  for (const p of need) {
7889
- fs35.mkdirSync(p, { recursive: true });
8867
+ fs38.mkdirSync(p, { recursive: true });
7890
8868
  }
7891
8869
  return { id: "dirs", name: ".squad/ directory structure", status: "ok", detail: "repaired" };
7892
8870
  }
@@ -7894,7 +8872,7 @@ async function checkDirStructure(paths, _ctx, fix) {
7894
8872
  id: "dirs",
7895
8873
  name: ".squad/ directory structure",
7896
8874
  status: "warn",
7897
- detail: `missing: ${need.map((p) => path27.relative(paths.root, p)).join(", ")}`,
8875
+ detail: `missing: ${need.map((p) => path28.relative(paths.root, p)).join(", ")}`,
7898
8876
  fixable: true,
7899
8877
  fixHint: "squad doctor --fix"
7900
8878
  };
@@ -7949,15 +8927,15 @@ async function checkSecretsPermissions(paths, _ctx, fix) {
7949
8927
  if (process.platform === "win32") {
7950
8928
  return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "skip", detail: "Windows" };
7951
8929
  }
7952
- if (!fs35.existsSync(paths.secretsFile)) {
8930
+ if (!fs38.existsSync(paths.secretsFile)) {
7953
8931
  return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "not present" };
7954
8932
  }
7955
- const mode = fs35.statSync(paths.secretsFile).mode & 511;
8933
+ const mode = fs38.statSync(paths.secretsFile).mode & 511;
7956
8934
  if (mode === 384) {
7957
8935
  return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok" };
7958
8936
  }
7959
8937
  if (fix) {
7960
- fs35.chmodSync(paths.secretsFile, 384);
8938
+ fs38.chmodSync(paths.secretsFile, 384);
7961
8939
  return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "repaired" };
7962
8940
  }
7963
8941
  return {
@@ -7978,7 +8956,7 @@ async function checkSecretsParseable(_paths, ctx) {
7978
8956
  detail: ctx.secretsError.message
7979
8957
  };
7980
8958
  }
7981
- if (!fs35.existsSync(_paths.secretsFile)) {
8959
+ if (!fs38.existsSync(_paths.secretsFile)) {
7982
8960
  return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok", detail: "not present" };
7983
8961
  }
7984
8962
  return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok" };
@@ -8040,36 +9018,142 @@ async function checkPlannerConfig(_paths, ctx) {
8040
9018
  }
8041
9019
  return { id: "planner-config", name: "planner configuration", status: "ok" };
8042
9020
  }
8043
- async function checkPlannerCredential(_paths, ctx) {
8044
- if (!ctx.config || ctx.config.planner?.enabled !== true) {
8045
- return { id: "planner-cred", name: "planner credential resolves", status: "skip", detail: "planner disabled" };
9021
+ var LOGIN_FIX_HINT = "Run `squad auth login` to sign in with your Claude account.";
9022
+ var ORG_NOT_ALLOWED_FIX_HINT = "That Claude account is signed in, but its organization policy blocks this kind of access. Ask an administrator to allow Claude Agent SDK usage, or set `planner.auth.anthropic: api-key` in .squad/config.yaml and provide an ANTHROPIC_API_KEY.";
9023
+ function plannerEnabled(ctx) {
9024
+ return ctx.config?.planner?.enabled === true;
9025
+ }
9026
+ function authFor(paths, ctx, provider) {
9027
+ const memo = ctx.authMemo ??= {};
9028
+ if (!memo.resolution) {
9029
+ try {
9030
+ memo.resolution = { ok: true, auth: resolvePlannerAuthForPaths(paths, provider, ctx.config?.planner) };
9031
+ } catch (err2) {
9032
+ memo.resolution = { ok: false, error: err2 };
9033
+ }
9034
+ }
9035
+ return memo.resolution;
9036
+ }
9037
+ async function probeAuthOnce(ctx, auth2) {
9038
+ const memo = ctx.authMemo ??= {};
9039
+ if (!memo.probe) {
9040
+ memo.probe = skipExternalProbesInAutomation() ? { skipped: true } : await probeClaudeAuth(auth2, { timeoutMs: DOCTOR_AUTH_PROBE_TIMEOUT_MS });
9041
+ }
9042
+ return memo.probe;
9043
+ }
9044
+ function probeWasSkipped(outcome) {
9045
+ return "skipped" in outcome;
9046
+ }
9047
+ async function checkPlannerAuthMode(paths, ctx, _fix) {
9048
+ const id = "planner-auth-mode";
9049
+ const name = "planner auth mode";
9050
+ if (!plannerEnabled(ctx) || !ctx.config?.planner) {
9051
+ return { id, name, status: "skip", detail: "planner disabled" };
9052
+ }
9053
+ const planner = ctx.config.planner;
9054
+ const provider = planner.provider;
9055
+ if (provider !== "anthropic") {
9056
+ return { id, name, status: "skip", detail: `api key only for ${provider}` };
9057
+ }
9058
+ const resolution = authFor(paths, ctx, provider);
9059
+ if (!resolution.ok) {
9060
+ return { id, name, status: "fail", detail: resolution.error.message, fixHint: LOGIN_FIX_HINT };
9061
+ }
9062
+ const auth2 = resolution.auth;
9063
+ const described = describeAuth(auth2);
9064
+ let detail = `${described.mode} (${authReasonText(described.reason, provider)}) \u2192 ${described.credentialHint}`;
9065
+ if (auth2.mode === "subscription") {
9066
+ const ignoredKey = resolveProviderKeyForPaths(paths, provider);
9067
+ if (ignoredKey) detail += ` \xB7 ignoring the API key from ${ignoredKey.detail}`;
9068
+ }
9069
+ const implicitApiKey = (planner.auth?.anthropic ?? "auto") === "auto" && auth2.reason === "auto-fallback-api-key";
9070
+ if (implicitApiKey) {
9071
+ return {
9072
+ id,
9073
+ name,
9074
+ status: "ok",
9075
+ detail,
9076
+ fixHint: "Resolving by fallback. Set `planner.auth.anthropic` explicitly (`squad config set planner`) if you want to pin this, or run `squad auth login` to plan on your Claude subscription instead."
9077
+ };
9078
+ }
9079
+ return { id, name, status: "ok", detail };
9080
+ }
9081
+ function describeProbeAccount(probe) {
9082
+ const bits = [];
9083
+ const email = probe.account?.email;
9084
+ const plan = probe.account?.subscriptionType;
9085
+ if (email) {
9086
+ bits.push(`signed in as ${email}${plan ? ` (${plan})` : ""}`);
9087
+ } else if (probe.unverifiable) {
9088
+ bits.push(`credential resolved (${probe.credentialSource ?? "token auth"}) \xB7 not verifiable without a run`);
9089
+ } else {
9090
+ bits.push("Claude login verified");
9091
+ }
9092
+ if (probe.account?.organization) bits.push(probe.account.organization);
9093
+ if (probe.apiKeySource) bits.push(`apiKeySource=${probe.apiKeySource}`);
9094
+ return bits.join(" \xB7 ");
9095
+ }
9096
+ async function checkPlannerCredential(paths, ctx) {
9097
+ const id = "planner-cred";
9098
+ const name = "planner credential resolves";
9099
+ if (!plannerEnabled(ctx) || !ctx.config?.planner) {
9100
+ return { id, name, status: "skip", detail: "planner disabled" };
8046
9101
  }
8047
9102
  const provider = ctx.config.planner.provider;
8048
- const resolved = resolveProviderKey(provider);
8049
- if (!resolved) {
9103
+ const resolution = authFor(paths, ctx, provider);
9104
+ if (!resolution.ok) {
8050
9105
  const envVar = providerEnvVar(provider);
8051
9106
  return {
8052
- id: "planner-cred",
8053
- name: "planner credential resolves",
9107
+ id,
9108
+ name,
8054
9109
  status: "fail",
8055
9110
  detail: `no API key found (${envVar} or .squad/secrets.yaml)`,
8056
- fixHint: `Set ${envVar} or save a key via 'squad init' interactive setup`
9111
+ fixHint: resolution.error.message
8057
9112
  };
8058
9113
  }
8059
- return {
8060
- id: "planner-cred",
8061
- name: "planner credential resolves",
8062
- status: "ok",
8063
- detail: `source=${resolved.source} (${resolved.detail})`
8064
- };
9114
+ const auth2 = resolution.auth;
9115
+ if (auth2.mode === "api-key") {
9116
+ return { id, name, status: "ok", detail: `source=${auth2.source} (${auth2.detail})` };
9117
+ }
9118
+ const credentialHint = describeAuth(auth2).credentialHint;
9119
+ const probe = await probeAuthOnce(ctx, auth2);
9120
+ if (probeWasSkipped(probe)) {
9121
+ return credentialPresent(auth2) ? { id, name, status: "ok", detail: `${credentialHint} \xB7 offline check only` } : {
9122
+ id,
9123
+ name,
9124
+ status: "warn",
9125
+ detail: "no Claude login detected \xB7 offline check only",
9126
+ fixHint: LOGIN_FIX_HINT
9127
+ };
9128
+ }
9129
+ if (probe.ok) {
9130
+ return { id, name, status: "ok", detail: describeProbeAccount(probe) };
9131
+ }
9132
+ switch (probe.kind) {
9133
+ case "not-logged-in":
9134
+ case "expired":
9135
+ return { id, name, status: "fail", detail: probe.detail, fixHint: LOGIN_FIX_HINT };
9136
+ case "org-not-allowed":
9137
+ return { id, name, status: "fail", detail: probe.detail, fixHint: ORG_NOT_ALLOWED_FIX_HINT };
9138
+ case "no-binary":
9139
+ return { id, name, status: "warn", detail: probe.detail, fixHint: MISSING_CLAUDE_BINARY_MESSAGE };
9140
+ default:
9141
+ return credentialPresent(auth2) ? { id, name, status: "warn", detail: `${probe.kind}: ${probe.detail}` } : {
9142
+ id,
9143
+ name,
9144
+ status: "fail",
9145
+ detail: `no Claude login detected (live check: ${probe.kind})`,
9146
+ fixHint: LOGIN_FIX_HINT
9147
+ };
9148
+ }
8065
9149
  }
8066
- async function checkPlannerModel(_paths, ctx) {
8067
- if (!ctx.config || ctx.config.planner?.enabled !== true) {
9150
+ async function checkPlannerModel(paths, ctx) {
9151
+ if (!plannerEnabled(ctx) || !ctx.config?.planner) {
8068
9152
  return { id: "planner-model", name: "planner model resolves at provider", status: "skip", detail: "planner disabled" };
8069
9153
  }
8070
9154
  const provider = ctx.config.planner.provider;
8071
- const cred = resolveProviderKey(provider);
8072
- if (!cred) {
9155
+ const resolution = authFor(paths, ctx, provider);
9156
+ if (!resolution.ok) {
8073
9157
  return {
8074
9158
  id: "planner-model",
8075
9159
  name: "planner model resolves at provider",
@@ -8077,9 +9161,18 @@ async function checkPlannerModel(_paths, ctx) {
8077
9161
  detail: "no credential"
8078
9162
  };
8079
9163
  }
9164
+ const auth2 = resolution.auth;
9165
+ if (auth2.mode === "subscription") {
9166
+ return {
9167
+ id: "planner-model",
9168
+ name: "planner model resolves at provider",
9169
+ status: "skip",
9170
+ detail: "model list needs an API key; skipped in subscription mode"
9171
+ };
9172
+ }
8080
9173
  const model = modelFor(provider, "plan", ctx.config.planner.modelOverride);
8081
9174
  try {
8082
- const listed = await fetchProviderModelIds(provider, cred.value);
9175
+ const listed = await fetchProviderModelIds(provider, auth2.key);
8083
9176
  if (!listed.ok) {
8084
9177
  const st = listed.status;
8085
9178
  if (st === 401 || st === 403) {
@@ -8121,7 +9214,7 @@ async function checkPlannerModel(_paths, ctx) {
8121
9214
  };
8122
9215
  }
8123
9216
  }
8124
- async function checkPlannerTierAwareness(_paths, ctx) {
9217
+ async function checkPlannerTierAwareness(paths, ctx) {
8125
9218
  const id = "planner-tier";
8126
9219
  const name = "planner tier vs. model";
8127
9220
  if (!ctx.config || ctx.config.planner?.enabled !== true) {
@@ -8131,6 +9224,10 @@ async function checkPlannerTierAwareness(_paths, ctx) {
8131
9224
  if (provider !== "anthropic") {
8132
9225
  return { id, name, status: "skip", detail: `not applicable for ${provider}` };
8133
9226
  }
9227
+ const tierAuth = authFor(paths, ctx, provider);
9228
+ if (tierAuth.ok && tierAuth.auth.mode === "subscription") {
9229
+ return { id, name, status: "skip", detail: "not applicable on subscription auth (no API rate tier)" };
9230
+ }
8134
9231
  const planModel = modelFor(provider, "plan", ctx.config.planner.modelOverride);
8135
9232
  if (!/opus/i.test(planModel)) {
8136
9233
  return { id, name, status: "ok", detail: `${planModel} is comfortably under Tier 1` };
@@ -8259,18 +9356,57 @@ async function checkPlannerAnthropicRuntimeModelFit(_paths, ctx) {
8259
9356
  fixHint: "Remove planner.runtime.anthropic: vercel (default is agent-sdk), or set planner.modelOverride.anthropic to a pre-4.7 id (e.g. claude-sonnet-4-5-20250929)."
8260
9357
  };
8261
9358
  }
8262
- async function checkAgentSdkBinaryPresent(_paths, ctx) {
9359
+ async function checkPlannerAuthRuntimeFit(paths, ctx) {
9360
+ const id = "planner-auth-runtime-fit";
9361
+ const name = "planner auth vs. runtime";
9362
+ if (!plannerEnabled(ctx) || !ctx.config?.planner) {
9363
+ return { id, name, status: "skip", detail: "planner disabled" };
9364
+ }
9365
+ const planner = ctx.config.planner;
9366
+ const provider = planner.provider;
9367
+ const configuredMode = planner.auth?.anthropic ?? "auto";
9368
+ if (provider !== "anthropic") {
9369
+ if (configuredMode === "subscription") {
9370
+ return {
9371
+ id,
9372
+ name,
9373
+ status: "fail",
9374
+ detail: `planner.auth.anthropic: subscription with planner.provider: ${provider}`,
9375
+ fixHint: plannerAuthRuntimeMismatchMessage(provider)
9376
+ };
9377
+ }
9378
+ return { id, name, status: "skip", detail: `api key only for ${provider}` };
9379
+ }
9380
+ const runtime = runtimeNameFor(provider, planner);
9381
+ const resolution = authFor(paths, ctx, provider);
9382
+ if (!resolution.ok) {
9383
+ return { id, name, status: "skip", detail: "auth unresolved" };
9384
+ }
9385
+ const mode = resolution.auth.mode;
9386
+ if (mode === "subscription" && runtime !== "agent-sdk") {
9387
+ return {
9388
+ id,
9389
+ name,
9390
+ status: "fail",
9391
+ detail: `subscription auth with planner.runtime.anthropic: ${runtime}`,
9392
+ fixHint: plannerAuthRuntimeMismatchMessage(provider)
9393
+ };
9394
+ }
9395
+ return { id, name, status: "ok", detail: `${mode} + ${runtime} runtime` };
9396
+ }
9397
+ async function checkAgentSdkBinaryPresent(paths, ctx) {
8263
9398
  const id = "agent-sdk-binary-present";
8264
9399
  const name = "Anthropic Agent SDK install";
8265
9400
  if (!ctx.config?.planner?.enabled || ctx.config.planner.provider !== "anthropic") {
8266
9401
  return { id, name, status: "skip", detail: "not applicable" };
8267
9402
  }
8268
- if ((ctx.config.planner.runtime?.anthropic ?? "agent-sdk") === "vercel") {
9403
+ const resolution = authFor(paths, ctx, "anthropic");
9404
+ const subscription = resolution.ok && resolution.auth.mode === "subscription";
9405
+ if ((ctx.config.planner.runtime?.anthropic ?? "agent-sdk") === "vercel" && !subscription) {
8269
9406
  return { id, name, status: "skip", detail: "vercel runtime selected" };
8270
9407
  }
8271
9408
  try {
8272
9409
  await import("@anthropic-ai/claude-agent-sdk");
8273
- return { id, name, status: "ok", detail: "package resolves" };
8274
9410
  } catch (e) {
8275
9411
  return {
8276
9412
  id,
@@ -8280,6 +9416,22 @@ async function checkAgentSdkBinaryPresent(_paths, ctx) {
8280
9416
  fixHint: "Run pnpm install / npm install in the squad-kit package so the Agent SDK (and its platform binary) is present."
8281
9417
  };
8282
9418
  }
9419
+ const binary = resolveClaudeBinary();
9420
+ if (!binary) {
9421
+ return {
9422
+ id,
9423
+ name,
9424
+ status: subscription ? "fail" : "warn",
9425
+ detail: "package resolves; no `claude` executable found",
9426
+ fixHint: MISSING_CLAUDE_BINARY_MESSAGE
9427
+ };
9428
+ }
9429
+ return {
9430
+ id,
9431
+ name,
9432
+ status: "ok",
9433
+ detail: `package + ${binary.source === "bundled" ? "bundled" : "PATH"} binary`
9434
+ };
8283
9435
  }
8284
9436
  async function checkTrackerConfig(_paths, ctx) {
8285
9437
  if (!ctx.config) {
@@ -8418,11 +9570,13 @@ async function runAllChecks(paths, ctx, fix) {
8418
9570
  await add(".squad/secrets.yaml parseable", () => checkSecretsParseable(paths, ctx));
8419
9571
  await add("legacy .squad/prompts/ directory", () => checkLegacyPrompts(paths, ctx));
8420
9572
  await add("planner configuration", () => checkPlannerConfig(paths, ctx));
9573
+ await add("planner auth mode", () => checkPlannerAuthMode(paths, ctx, fix));
8421
9574
  await add("planner credential resolves", () => checkPlannerCredential(paths, ctx));
8422
9575
  await add("planner model resolves at provider", () => checkPlannerModel(paths, ctx));
8423
9576
  await add("planner tier vs. model", () => checkPlannerTierAwareness(paths, ctx));
8424
9577
  await add("planner cache effectiveness", () => checkPlannerCache(paths, ctx));
8425
9578
  await add("planner runtime (resolved)", () => checkPlannerRuntimeInfo(paths, ctx));
9579
+ await add("planner auth vs. runtime", () => checkPlannerAuthRuntimeFit(paths, ctx));
8426
9580
  await add("Anthropic Opus 4.7+ vs Vercel runtime", () => checkPlannerAnthropicRuntimeModelFit(paths, ctx));
8427
9581
  await add("Anthropic Agent SDK install", () => checkAgentSdkBinaryPresent(paths, ctx));
8428
9582
  await add("tracker configuration", () => checkTrackerConfig(paths, ctx));
@@ -8532,23 +9686,23 @@ async function runDoctor(opts) {
8532
9686
  }
8533
9687
 
8534
9688
  // src/commands/migrate.ts
8535
- import fs36 from "fs";
8536
- import path28 from "path";
8537
- import { confirm as confirm7 } from "@inquirer/prompts";
9689
+ import fs39 from "fs";
9690
+ import path29 from "path";
9691
+ import { confirm as confirm8 } from "@inquirer/prompts";
8538
9692
  var LEGACY_PROMPTS_MIGRATION = {
8539
9693
  id: "legacy-prompts",
8540
9694
  name: "Remove legacy .squad/prompts/",
8541
- isApplied: (paths) => !fs36.existsSync(paths.promptsDir),
8542
- describe: (paths) => `delete ${path28.relative(paths.root, paths.promptsDir)} (prompts are now bundled in squad-kit)`,
9695
+ isApplied: (paths) => !fs39.existsSync(paths.promptsDir),
9696
+ describe: (paths) => `delete ${path29.relative(paths.root, paths.promptsDir)} (prompts are now bundled in squad-kit)`,
8543
9697
  apply: (paths) => {
8544
9698
  const target = paths.promptsDir;
8545
- if (!target.includes(path28.sep + ".squad" + path28.sep + "prompts")) {
9699
+ if (!target.includes(path29.sep + ".squad" + path29.sep + "prompts")) {
8546
9700
  throw new Error(
8547
9701
  `Refusing to delete suspicious path: ${target}. This is a safety check \u2014 run \`squad doctor\` if .squad/ paths look wrong.`
8548
9702
  );
8549
9703
  }
8550
- if (fs36.existsSync(target)) {
8551
- fs36.rmSync(target, { recursive: true, force: true });
9704
+ if (fs39.existsSync(target)) {
9705
+ fs39.rmSync(target, { recursive: true, force: true });
8552
9706
  }
8553
9707
  }
8554
9708
  };
@@ -8556,8 +9710,8 @@ var GITIGNORE_MIGRATION = {
8556
9710
  id: "gitignore",
8557
9711
  name: "Ensure .gitignore managed block",
8558
9712
  isApplied: (paths) => {
8559
- const f = path28.join(paths.root, ".gitignore");
8560
- return fs36.existsSync(f) && fs36.readFileSync(f, "utf8").includes(".squad/secrets.yaml");
9713
+ const f = path29.join(paths.root, ".gitignore");
9714
+ return fs39.existsSync(f) && fs39.readFileSync(f, "utf8").includes(".squad/secrets.yaml");
8561
9715
  },
8562
9716
  describe: () => "add squad-kit managed block to .gitignore",
8563
9717
  apply: (paths) => {
@@ -8569,23 +9723,23 @@ var SECRETS_PERMS_MIGRATION = {
8569
9723
  name: "Tighten .squad/secrets.yaml permissions",
8570
9724
  isApplied: (paths) => {
8571
9725
  if (process.platform === "win32") return true;
8572
- if (!fs36.existsSync(paths.secretsFile)) return true;
8573
- const st = fs36.statSync(paths.secretsFile);
9726
+ if (!fs39.existsSync(paths.secretsFile)) return true;
9727
+ const st = fs39.statSync(paths.secretsFile);
8574
9728
  return (st.mode & 511) === 384;
8575
9729
  },
8576
- describe: (paths) => `chmod 0600 ${path28.relative(paths.root, paths.secretsFile)}`,
9730
+ describe: (paths) => `chmod 0600 ${path29.relative(paths.root, paths.secretsFile)}`,
8577
9731
  apply: (paths) => {
8578
9732
  if (process.platform === "win32") return;
8579
- if (fs36.existsSync(paths.secretsFile)) fs36.chmodSync(paths.secretsFile, 384);
9733
+ if (fs39.existsSync(paths.secretsFile)) fs39.chmodSync(paths.secretsFile, 384);
8580
9734
  }
8581
9735
  };
8582
9736
  var PLANNER_BUDGET_DEFAULTS_MIGRATION = {
8583
9737
  id: "planner-budget-defaults",
8584
9738
  name: "Backfill planner.budget defaults in config.yaml",
8585
9739
  isApplied: (paths) => {
8586
- if (!fs36.existsSync(paths.configFile)) return true;
9740
+ if (!fs39.existsSync(paths.configFile)) return true;
8587
9741
  try {
8588
- const raw = fs36.readFileSync(paths.configFile, "utf8");
9742
+ const raw = fs39.readFileSync(paths.configFile, "utf8");
8589
9743
  if (!/^\s*planner:/m.test(raw)) return true;
8590
9744
  if (/^\s*budget:/m.test(raw)) return true;
8591
9745
  const cfg = loadConfig(paths.configFile);
@@ -8604,9 +9758,9 @@ var CONFIG_NORMALIZE_MIGRATION = {
8604
9758
  id: "config-normalize",
8605
9759
  name: "Normalise .squad/config.yaml formatting",
8606
9760
  isApplied: (paths) => {
8607
- if (!fs36.existsSync(paths.configFile)) return true;
9761
+ if (!fs39.existsSync(paths.configFile)) return true;
8608
9762
  try {
8609
- const raw = fs36.readFileSync(paths.configFile, "utf8");
9763
+ const raw = fs39.readFileSync(paths.configFile, "utf8");
8610
9764
  const cfg = loadConfig(paths.configFile);
8611
9765
  return raw.trim() === serializeConfig(cfg).trim();
8612
9766
  } catch {
@@ -8615,7 +9769,7 @@ var CONFIG_NORMALIZE_MIGRATION = {
8615
9769
  },
8616
9770
  describe: () => "rewrite config.yaml with canonical YAML formatting (drops comments)",
8617
9771
  apply: (paths) => {
8618
- if (!fs36.existsSync(paths.configFile)) return;
9772
+ if (!fs39.existsSync(paths.configFile)) return;
8619
9773
  const cfg = loadConfig(paths.configFile);
8620
9774
  saveConfig(paths.configFile, cfg);
8621
9775
  }
@@ -8663,7 +9817,7 @@ async function runMigrate(opts) {
8663
9817
  );
8664
9818
  process.exit(1);
8665
9819
  }
8666
- const go = await confirm7({ message: `Apply ${pending.length} migration(s)?`, default: true });
9820
+ const go = await confirm8({ message: `Apply ${pending.length} migration(s)?`, default: true });
8667
9821
  if (!go) {
8668
9822
  info("Cancelled. No changes applied.");
8669
9823
  return;
@@ -8691,13 +9845,13 @@ async function runMigrate(opts) {
8691
9845
  }
8692
9846
 
8693
9847
  // src/commands/upgrade.ts
8694
- import { spawn as spawn2 } from "child_process";
9848
+ import { spawn as spawn3 } from "child_process";
8695
9849
  import process3 from "process";
8696
- import { confirm as confirm8 } from "@inquirer/prompts";
9850
+ import { confirm as confirm9 } from "@inquirer/prompts";
8697
9851
 
8698
9852
  // src/core/package-info.ts
8699
- import fs37 from "fs";
8700
- import path29 from "path";
9853
+ import fs40 from "fs";
9854
+ import path30 from "path";
8701
9855
  function changelogBlobUrl(repositoryUrl, version) {
8702
9856
  let base = repositoryUrl;
8703
9857
  if (!base) base = "https://github.com/AzmSquad/squad-kit";
@@ -8708,9 +9862,9 @@ function changelogBlobUrl(repositoryUrl, version) {
8708
9862
  }
8709
9863
  function readInstalledPackage() {
8710
9864
  const root = packageRoot();
8711
- const pkgFile = path29.join(root, "package.json");
8712
- const pkg = JSON.parse(fs37.readFileSync(pkgFile, "utf8"));
8713
- const isDevInstall = fs37.existsSync(path29.join(root, "src"));
9865
+ const pkgFile = path30.join(root, "package.json");
9866
+ const pkg = JSON.parse(fs40.readFileSync(pkgFile, "utf8"));
9867
+ const isDevInstall = fs40.existsSync(path30.join(root, "src"));
8714
9868
  return {
8715
9869
  name: pkg.name,
8716
9870
  version: pkg.version,
@@ -8750,7 +9904,7 @@ function isNewer(a, b) {
8750
9904
  }
8751
9905
 
8752
9906
  // src/core/package-manager.ts
8753
- import path30 from "path";
9907
+ import path31 from "path";
8754
9908
  import process2 from "process";
8755
9909
  function detectPackageManager(installRoot) {
8756
9910
  const ua = process2.env.npm_config_user_agent;
@@ -8761,11 +9915,11 @@ function detectPackageManager(installRoot) {
8761
9915
  if (ua.startsWith("npm/")) return { pm: "npm", reason: "npm_config_user_agent" };
8762
9916
  }
8763
9917
  const root = installRoot.toLowerCase();
8764
- if (root.includes(`${path30.sep}pnpm${path30.sep}`) || root.includes(`${path30.sep}.pnpm${path30.sep}`))
9918
+ if (root.includes(`${path31.sep}pnpm${path31.sep}`) || root.includes(`${path31.sep}.pnpm${path31.sep}`))
8765
9919
  return { pm: "pnpm", reason: `install path contains pnpm` };
8766
- if (root.includes(`${path30.sep}.yarn${path30.sep}`) || root.includes(`${path30.sep}yarn${path30.sep}`))
9920
+ if (root.includes(`${path31.sep}.yarn${path31.sep}`) || root.includes(`${path31.sep}yarn${path31.sep}`))
8767
9921
  return { pm: "yarn", reason: `install path contains yarn` };
8768
- if (root.includes(`${path30.sep}.bun${path30.sep}`) || root.includes(`${path30.sep}bun${path30.sep}`))
9922
+ if (root.includes(`${path31.sep}.bun${path31.sep}`) || root.includes(`${path31.sep}bun${path31.sep}`))
8769
9923
  return { pm: "bun", reason: `install path contains bun` };
8770
9924
  return { pm: "npm", reason: "default fallback" };
8771
9925
  }
@@ -8838,7 +9992,7 @@ async function runUpgrade(opts) {
8838
9992
  );
8839
9993
  process3.exit(1);
8840
9994
  }
8841
- const go = await confirm8({ message: `Run "${cmd} ${args.join(" ")}"?`, default: true });
9995
+ const go = await confirm9({ message: `Run "${cmd} ${args.join(" ")}"?`, default: true });
8842
9996
  if (!go) {
8843
9997
  info("Cancelled. No changes applied.");
8844
9998
  return;
@@ -8859,7 +10013,7 @@ async function runUpgrade(opts) {
8859
10013
  }
8860
10014
  function spawnInherit(cmd, args) {
8861
10015
  return new Promise((resolve2, reject) => {
8862
- const child = spawn2(cmd, args, { stdio: "inherit", shell: process3.platform === "win32" });
10016
+ const child = spawn3(cmd, args, { stdio: "inherit", shell: process3.platform === "win32" });
8863
10017
  child.on("exit", (code) => resolve2(code ?? 0));
8864
10018
  child.on("error", (err2) => reject(err2));
8865
10019
  });
@@ -8878,14 +10032,14 @@ import { cors } from "hono/cors";
8878
10032
  import { secureHeaders } from "hono/secure-headers";
8879
10033
 
8880
10034
  // src/console/api/stories.ts
8881
- import fs38 from "fs";
10035
+ import fs41 from "fs";
8882
10036
  import { z as z6 } from "zod";
8883
10037
 
8884
10038
  // src/utils/path-safety.ts
8885
- import path31 from "path";
10039
+ import path32 from "path";
8886
10040
  function isInside(abs, parent) {
8887
- const a = path31.resolve(abs) + path31.sep;
8888
- const p = path31.resolve(parent) + path31.sep;
10041
+ const a = path32.resolve(abs) + path32.sep;
10042
+ const p = path32.resolve(parent) + path32.sep;
8889
10043
  return a.startsWith(p);
8890
10044
  }
8891
10045
 
@@ -8925,7 +10079,7 @@ function mountStoriesApi(app, opts) {
8925
10079
  if (!isInside(hit.intakePath, opts.paths.storiesDir)) return c.json({ error: "forbidden" }, 403);
8926
10080
  let intakeContent = "";
8927
10081
  try {
8928
- intakeContent = fs38.readFileSync(hit.intakePath, "utf8");
10082
+ intakeContent = fs41.readFileSync(hit.intakePath, "utf8");
8929
10083
  } catch {
8930
10084
  }
8931
10085
  return c.json({
@@ -8969,7 +10123,7 @@ function mountStoriesApi(app, opts) {
8969
10123
  const hit = stories.find((s) => s.id === id);
8970
10124
  if (!hit) return c.json({ error: "not_found" }, 404);
8971
10125
  if (!isInside(hit.intakePath, opts.paths.storiesDir)) return c.json({ error: "forbidden" }, 403);
8972
- fs38.writeFileSync(hit.intakePath, body.data.intakeContent, "utf8");
10126
+ fs41.writeFileSync(hit.intakePath, body.data.intakeContent, "utf8");
8973
10127
  return c.json({ ok: true });
8974
10128
  });
8975
10129
  app.delete("/api/stories/:feature/:id", (c) => {
@@ -8986,8 +10140,8 @@ function mountStoriesApi(app, opts) {
8986
10140
  }
8987
10141
 
8988
10142
  // src/console/api/plans.ts
8989
- import fs39 from "fs";
8990
- import path32 from "path";
10143
+ import fs42 from "fs";
10144
+ import path33 from "path";
8991
10145
  import { z as z7 } from "zod";
8992
10146
  import { diffLines } from "diff";
8993
10147
  var DiffQuery = z7.object({
@@ -9002,11 +10156,11 @@ function mountPlansApi(app, opts) {
9002
10156
  app.get("/api/plans", (c) => {
9003
10157
  const out = [];
9004
10158
  for (const feature of listPlanFeatureNames(opts.paths)) {
9005
- const featureDir = path32.join(opts.paths.plansDir, feature);
9006
- if (!fs39.existsSync(featureDir)) continue;
9007
- for (const entry of fs39.readdirSync(featureDir)) {
10159
+ const featureDir = path33.join(opts.paths.plansDir, feature);
10160
+ if (!fs42.existsSync(featureDir)) continue;
10161
+ for (const entry of fs42.readdirSync(featureDir)) {
9008
10162
  if (!entry.endsWith(".md") || entry === "00-index.md") continue;
9009
- const abs = path32.join(featureDir, entry);
10163
+ const abs = path33.join(featureDir, entry);
9010
10164
  out.push({ feature, planFile: entry, metadata: readPlanMetadata(abs) });
9011
10165
  }
9012
10166
  }
@@ -9015,10 +10169,10 @@ function mountPlansApi(app, opts) {
9015
10169
  app.get("/api/plans/:feature/:planFile", (c) => {
9016
10170
  const { feature, planFile } = c.req.param();
9017
10171
  if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
9018
- const abs = path32.resolve(path32.join(opts.paths.plansDir, feature, planFile));
10172
+ const abs = path33.resolve(path33.join(opts.paths.plansDir, feature, planFile));
9019
10173
  if (!isInside(abs, opts.paths.plansDir)) return c.json({ error: "forbidden" }, 403);
9020
- if (!fs39.existsSync(abs)) return c.json({ error: "not_found" }, 404);
9021
- const content = fs39.readFileSync(abs, "utf8");
10174
+ if (!fs42.existsSync(abs)) return c.json({ error: "not_found" }, 404);
10175
+ const content = fs42.readFileSync(abs, "utf8");
9022
10176
  return c.json({
9023
10177
  feature,
9024
10178
  planFile,
@@ -9036,15 +10190,15 @@ function mountPlansApi(app, opts) {
9036
10190
  if (!parse.success) return c.json({ error: "invalid_query", issues: parse.error.issues }, 400);
9037
10191
  const { feature, a, b } = parse.data;
9038
10192
  if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
9039
- const featureDir = path32.join(opts.paths.plansDir, feature);
9040
- const aAbs = path32.join(featureDir, a);
9041
- const bAbs = path32.join(featureDir, b);
10193
+ const featureDir = path33.join(opts.paths.plansDir, feature);
10194
+ const aAbs = path33.join(featureDir, a);
10195
+ const bAbs = path33.join(featureDir, b);
9042
10196
  if (!isInside(aAbs, opts.paths.plansDir) || !isInside(bAbs, opts.paths.plansDir)) {
9043
10197
  return c.json({ error: "forbidden" }, 403);
9044
10198
  }
9045
- if (!fs39.existsSync(aAbs) || !fs39.existsSync(bAbs)) return c.json({ error: "not_found" }, 404);
9046
- const aText = fs39.readFileSync(aAbs, "utf8");
9047
- const bText = fs39.readFileSync(bAbs, "utf8");
10199
+ if (!fs42.existsSync(aAbs) || !fs42.existsSync(bAbs)) return c.json({ error: "not_found" }, 404);
10200
+ const aText = fs42.readFileSync(aAbs, "utf8");
10201
+ const bText = fs42.readFileSync(bAbs, "utf8");
9048
10202
  const changes = diffLines(aText, bText);
9049
10203
  return c.json({ feature, a, b, changes });
9050
10204
  });
@@ -9052,16 +10206,16 @@ function mountPlansApi(app, opts) {
9052
10206
  const trash = c.req.query("trash") === "1";
9053
10207
  const { feature, planFile } = c.req.param();
9054
10208
  if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
9055
- const abs = path32.join(opts.paths.plansDir, feature, planFile);
10209
+ const abs = path33.join(opts.paths.plansDir, feature, planFile);
9056
10210
  if (!isInside(abs, opts.paths.plansDir)) return c.json({ error: "forbidden" }, 403);
9057
- if (!fs39.existsSync(abs)) return c.json({ error: "not_found" }, 404);
10211
+ if (!fs42.existsSync(abs)) return c.json({ error: "not_found" }, 404);
9058
10212
  if (trash) {
9059
10213
  const result = trashOrDelete([abs], opts.paths.trashDir, true);
9060
- removeOverviewRowForPlanFile(path32.join(opts.paths.plansDir, feature), planFile);
10214
+ removeOverviewRowForPlanFile(path33.join(opts.paths.plansDir, feature), planFile);
9061
10215
  return c.json({ ok: true, trashed: result.trashed });
9062
10216
  }
9063
- fs39.rmSync(abs);
9064
- removeOverviewRowForPlanFile(path32.join(opts.paths.plansDir, feature), planFile);
10217
+ fs42.rmSync(abs);
10218
+ removeOverviewRowForPlanFile(path33.join(opts.paths.plansDir, feature), planFile);
9065
10219
  return c.json({ ok: true });
9066
10220
  });
9067
10221
  }
@@ -9109,8 +10263,8 @@ function mountMetaApi(app, opts) {
9109
10263
  // src/console/api/runs.ts
9110
10264
  import { streamSSE } from "hono/streaming";
9111
10265
  import { z as z9 } from "zod";
9112
- import path33 from "path";
9113
- import fs40 from "fs";
10266
+ import path34 from "path";
10267
+ import fs43 from "fs";
9114
10268
  var active = /* @__PURE__ */ new Map();
9115
10269
  var EVENT_BUF_CAP = 4096;
9116
10270
  var StartBody = z9.object({
@@ -9146,15 +10300,21 @@ function mountRunsApi(app, opts) {
9146
10300
  if (!cfg.planner?.enabled) {
9147
10301
  return c.json({ error: "planner_disabled", detail: "Run `squad config set planner` first." }, 400);
9148
10302
  }
9149
- const apiKey = readProviderKeyForPaths(opts.paths, cfg.planner.provider);
9150
- if (!apiKey) {
9151
- return c.json(
9152
- {
9153
- error: "missing_credentials",
9154
- detail: `Set ${providerEnvVar(cfg.planner.provider)} or save a key via the Secrets tab.`
9155
- },
9156
- 400
9157
- );
10303
+ let auth2;
10304
+ try {
10305
+ auth2 = resolvePlannerAuthForPaths(opts.paths, cfg.planner.provider, cfg.planner);
10306
+ } catch (err2) {
10307
+ if (err2 instanceof PlannerAuthUnavailableError) {
10308
+ return c.json(
10309
+ {
10310
+ error: "auth_unavailable",
10311
+ mode: cfg.planner.auth?.anthropic ?? "auto",
10312
+ detail: err2.message
10313
+ },
10314
+ 400
10315
+ );
10316
+ }
10317
+ throw err2;
9158
10318
  }
9159
10319
  const stories = listStories(opts.paths, { feature: body.data.feature });
9160
10320
  const story = stories.find((s) => s.id === body.data.storyId);
@@ -9176,7 +10336,7 @@ function mountRunsApi(app, opts) {
9176
10336
  const draftRuntime = resolveRuntime({
9177
10337
  provider: planner.provider,
9178
10338
  modelId,
9179
- apiKey,
10339
+ auth: auth2,
9180
10340
  anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
9181
10341
  });
9182
10342
  const anthropicProviderSpecific = planner.provider === "anthropic" ? {
@@ -9202,7 +10362,7 @@ function mountRunsApi(app, opts) {
9202
10362
  scoutRuntime = resolveRuntime({
9203
10363
  provider: planner.provider,
9204
10364
  modelId: scoutModelId,
9205
- apiKey,
10365
+ auth: auth2,
9206
10366
  anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
9207
10367
  });
9208
10368
  }
@@ -9223,7 +10383,7 @@ function mountRunsApi(app, opts) {
9223
10383
  repoMap
9224
10384
  });
9225
10385
  const userPrompt = composeUserPrompt({
9226
- intakeContent: fs40.readFileSync(story.intakePath, "utf8")
10386
+ intakeContent: fs43.readFileSync(story.intakePath, "utf8")
9227
10387
  });
9228
10388
  let result;
9229
10389
  try {
@@ -9233,6 +10393,7 @@ function mountRunsApi(app, opts) {
9233
10393
  runtime: draftRuntime,
9234
10394
  provider: planner.provider,
9235
10395
  modelId,
10396
+ auth: auth2,
9236
10397
  anthropicProviderSpecific,
9237
10398
  systemPrompt,
9238
10399
  userPrompt,
@@ -9302,7 +10463,7 @@ function mountRunsApi(app, opts) {
9302
10463
  metadataHeader: header,
9303
10464
  partial: !runSuccess
9304
10465
  });
9305
- planFile = path33.relative(opts.paths.root, out.planFile);
10466
+ planFile = path34.relative(opts.paths.root, out.planFile);
9306
10467
  }
9307
10468
  try {
9308
10469
  await writeLastRun(opts.paths, {
@@ -9338,6 +10499,7 @@ function mountRunsApi(app, opts) {
9338
10499
  durationMs: result.validation?.durationMs
9339
10500
  },
9340
10501
  plannerRuntime: { kind: draftRuntime.kind, provider: planner.provider },
10502
+ authMode: describeAuth(auth2).mode,
9341
10503
  providerOptionsSnapshot: anthropicProviderSpecific ? { anthropic: anthropicProviderSpecific } : void 0
9342
10504
  });
9343
10505
  } catch {
@@ -9469,7 +10631,13 @@ function mountSecretsApi(app, opts) {
9469
10631
  planner: {
9470
10632
  anthropic: maskToken(s.planner?.anthropic),
9471
10633
  openai: maskToken(s.planner?.openai),
9472
- google: maskToken(s.planner?.google)
10634
+ google: maskToken(s.planner?.google),
10635
+ /**
10636
+ * `claude setup-token` output. Masked exactly like the API keys — the Claude account
10637
+ * card shows it so a user can tell "a token is stored here" from "the OS login store
10638
+ * is being used". Never returned in full, and never by `/api/planner-auth`.
10639
+ */
10640
+ anthropicOauthToken: maskToken(s.planner?.anthropicOauthToken)
9473
10641
  },
9474
10642
  tracker: {
9475
10643
  jira: {
@@ -9493,7 +10661,8 @@ function mountSecretsApi(app, opts) {
9493
10661
  planner: z10.object({
9494
10662
  anthropic: z10.string().optional(),
9495
10663
  openai: z10.string().optional(),
9496
- google: z10.string().optional()
10664
+ google: z10.string().optional(),
10665
+ anthropicOauthToken: z10.string().optional()
9497
10666
  }).optional(),
9498
10667
  tracker: z10.object({
9499
10668
  jira: z10.object({
@@ -9517,6 +10686,9 @@ function mountSecretsApi(app, opts) {
9517
10686
  if (!parse.success) return c.json({ error: "invalid_body", issues: parse.error.issues }, 400);
9518
10687
  const current = loadSecrets(opts.paths.secretsFile);
9519
10688
  const merged = mergeSecrets(current, parse.data);
10689
+ if (parse.data.planner?.anthropicOauthToken === "" && merged.planner) {
10690
+ delete merged.planner.anthropicOauthToken;
10691
+ }
9520
10692
  saveSecrets(opts.paths.secretsFile, merged);
9521
10693
  return c.json({ ok: true });
9522
10694
  });
@@ -9547,13 +10719,166 @@ function mountSecretsApi(app, opts) {
9547
10719
  });
9548
10720
  }
9549
10721
 
9550
- // src/console/api/tracker.ts
10722
+ // src/console/api/planner-auth.ts
9551
10723
  import { z as z11 } from "zod";
9552
- var SearchQuery = z11.object({ q: z11.string().max(500) });
9553
- var ImportBody = z11.object({
9554
- issueId: z11.string().min(1),
9555
- feature: z11.string().min(1).max(80),
9556
- withAttachments: z11.boolean().optional()
10724
+
10725
+ // src/commands/auth/status.ts
10726
+ async function runAuthStatus(opts = {}) {
10727
+ const paths = buildPaths(requireSquadRoot());
10728
+ const config2 = loadConfig(paths.configFile);
10729
+ const planner = config2.planner;
10730
+ const provider = planner?.provider ?? "anthropic";
10731
+ const auth2 = resolvePlannerAuthForCwd(provider, planner);
10732
+ const described = describeAuth(auth2);
10733
+ const probe = shouldProbe(opts, provider) ? await probeClaudeAuth(auth2) : void 0;
10734
+ const payload = buildPayload2({ auth: described, provider, planner, probe, fallbackLoggedIn: credentialPresent(auth2) });
10735
+ if (opts.json) {
10736
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
10737
+ `);
10738
+ return;
10739
+ }
10740
+ const kw2 = 15;
10741
+ step("squad-kit auth");
10742
+ blank();
10743
+ kv("auth mode", `${payload.mode} (${payload.reason})`, kw2);
10744
+ kv("credential", payload.credentialHint, kw2);
10745
+ if (payload.account) {
10746
+ const account = [payload.account.email, payload.account.organization, payload.account.subscriptionType].filter(Boolean).join(" \xB7 ");
10747
+ if (account) kv("account", account, kw2);
10748
+ }
10749
+ if (payload.apiKeySource) kv("api key source", payload.apiKeySource, kw2);
10750
+ kv("runtime", payload.runtime, kw2);
10751
+ if (probe && !probe.ok) {
10752
+ blank();
10753
+ warning(`Live check failed (${probe.kind}).`);
10754
+ info(probe.detail);
10755
+ if (probe.kind === "not-logged-in" || probe.kind === "expired") {
10756
+ info("Run `squad auth login` to sign in again.");
10757
+ }
10758
+ } else if (!probe) {
10759
+ blank();
10760
+ info("Account details skipped (offline). Re-run without --offline to check the live login.");
10761
+ }
10762
+ }
10763
+ function shouldProbe(opts, provider) {
10764
+ if (opts.offline) return false;
10765
+ if (skipExternalProbesInAutomation()) return false;
10766
+ return provider === "anthropic";
10767
+ }
10768
+ function buildPayload2(input5) {
10769
+ const { auth: auth2, provider, planner, probe } = input5;
10770
+ const payload = {
10771
+ mode: auth2.mode,
10772
+ reason: authReasonText(auth2.reason, provider),
10773
+ credentialHint: auth2.credentialHint,
10774
+ runtime: runtimeNameFor(provider, planner),
10775
+ // Without a probe the honest answer is "a credential resolved locally"; with one, the live
10776
+ // result wins. Either way the key is always present so scripts can rely on the shape.
10777
+ loggedIn: probe ? probe.ok : input5.fallbackLoggedIn
10778
+ };
10779
+ if (probe?.ok) {
10780
+ if (probe.account) payload.account = probe.account;
10781
+ if (probe.apiKeySource) payload.apiKeySource = probe.apiKeySource;
10782
+ }
10783
+ return payload;
10784
+ }
10785
+
10786
+ // src/console/api/planner-auth.ts
10787
+ var PLANNER_AUTH_LOGIN_COMMAND = "squad auth login";
10788
+ var PROBE_CACHE_TTL_MS = 6e4;
10789
+ var probeCache;
10790
+ function probeCacheKey(auth2) {
10791
+ const described = describeAuth(auth2);
10792
+ return `${described.mode}:${described.credentialHint}`;
10793
+ }
10794
+ async function probeWithCache(auth2, now) {
10795
+ const key = probeCacheKey(auth2);
10796
+ if (probeCache && probeCache.key === key && now - probeCache.at < PROBE_CACHE_TTL_MS) {
10797
+ return probeCache.result;
10798
+ }
10799
+ const result = await probeClaudeAuth(auth2);
10800
+ if (result.ok) probeCache = { key, at: now, result };
10801
+ return result;
10802
+ }
10803
+ function resetPlannerAuthProbeCache() {
10804
+ probeCache = void 0;
10805
+ }
10806
+ function subscriptionRuntimeConflict(provider, planner) {
10807
+ if (provider !== "anthropic") return plannerAuthRuntimeMismatchMessage(provider);
10808
+ if (runtimeNameFor(provider, planner) === "vercel") return plannerAuthRuntimeMismatchMessage(provider);
10809
+ return null;
10810
+ }
10811
+ function mountPlannerAuthApi(app, opts) {
10812
+ app.get("/api/planner-auth", async (c) => {
10813
+ const config2 = loadConfig(opts.paths.configFile);
10814
+ const planner = config2.planner;
10815
+ const provider = planner?.provider ?? "anthropic";
10816
+ const configuredMode = planner?.auth?.anthropic ?? "auto";
10817
+ const loginProbe = detectClaudeLogin(loadSecrets(opts.paths.secretsFile));
10818
+ const binary = resolveClaudeBinary();
10819
+ let auth2;
10820
+ let error = null;
10821
+ try {
10822
+ auth2 = resolvePlannerAuthForPaths(opts.paths, provider, planner);
10823
+ } catch (err2) {
10824
+ if (!(err2 instanceof PlannerAuthUnavailableError)) throw err2;
10825
+ error = err2.message;
10826
+ }
10827
+ const wantProbe = c.req.query("probe") === "1" && provider === "anthropic" && auth2 !== void 0;
10828
+ const probe = wantProbe ? await probeWithCache(auth2, Date.now()) : void 0;
10829
+ const status = auth2 ? buildPayload2({
10830
+ auth: describeAuth(auth2),
10831
+ provider,
10832
+ planner,
10833
+ probe,
10834
+ fallbackLoggedIn: credentialPresent(auth2)
10835
+ }) : void 0;
10836
+ const payload = {
10837
+ provider,
10838
+ configuredMode,
10839
+ resolved: status ? { mode: status.mode, reason: status.reason, credentialHint: status.credentialHint } : null,
10840
+ error,
10841
+ login: { present: loginProbe.present, hint: loginProbe.hint, detail: loginProbe.detail },
10842
+ account: status?.account ?? null,
10843
+ apiKeySource: status?.apiKeySource ?? null,
10844
+ runtime: runtimeNameFor(provider, planner) === "vercel" ? "vercel" : "agent-sdk",
10845
+ binary: { found: Boolean(binary), source: binary?.source ?? null },
10846
+ loginCommand: PLANNER_AUTH_LOGIN_COMMAND,
10847
+ loggedIn: status?.loggedIn ?? false,
10848
+ probe: probe ? { ran: true, ok: probe.ok, ...probe.ok ? {} : { kind: probe.kind, detail: probe.detail } } : null,
10849
+ runtimeConflict: subscriptionRuntimeConflict(provider, planner)
10850
+ };
10851
+ return c.json(payload);
10852
+ });
10853
+ const ModeBody = z11.object({ mode: z11.string() });
10854
+ app.post("/api/planner-auth/mode", async (c) => {
10855
+ const parsed = ModeBody.safeParse(await c.req.json().catch(() => null));
10856
+ if (!parsed.success) return c.json({ error: "invalid_body", issues: parsed.error.issues }, 400);
10857
+ const config2 = loadConfig(opts.paths.configFile);
10858
+ const next = {
10859
+ ...config2,
10860
+ planner: {
10861
+ ...config2.planner ?? {},
10862
+ auth: { ...config2.planner?.auth ?? {}, anthropic: parsed.data.mode }
10863
+ }
10864
+ };
10865
+ try {
10866
+ saveConfig(opts.paths.configFile, next);
10867
+ } catch (err2) {
10868
+ return c.json({ error: "invalid_auth_mode", detail: err2.message }, 400);
10869
+ }
10870
+ resetPlannerAuthProbeCache();
10871
+ return c.json({ ok: true, mode: parsed.data.mode });
10872
+ });
10873
+ }
10874
+
10875
+ // src/console/api/tracker.ts
10876
+ import { z as z12 } from "zod";
10877
+ var SearchQuery = z12.object({ q: z12.string().max(500) });
10878
+ var ImportBody = z12.object({
10879
+ issueId: z12.string().min(1),
10880
+ feature: z12.string().min(1).max(80),
10881
+ withAttachments: z12.boolean().optional()
9557
10882
  });
9558
10883
  function mountTrackerApi(app, opts) {
9559
10884
  app.get("/api/tracker/search", async (c) => {
@@ -9694,17 +11019,17 @@ function countStories(stories) {
9694
11019
  }
9695
11020
 
9696
11021
  // src/console/api/projects.ts
9697
- import fs41 from "fs/promises";
9698
- import path34 from "path";
9699
- import os from "os";
11022
+ import fs44 from "fs/promises";
11023
+ import path35 from "path";
11024
+ import os2 from "os";
9700
11025
  var MAX = 10;
9701
11026
  function recentFilePath() {
9702
- return path34.join(os.homedir(), ".squad", "recent-projects.json");
11027
+ return path35.join(os2.homedir(), ".squad", "recent-projects.json");
9703
11028
  }
9704
11029
  async function loadRecent() {
9705
11030
  const RECENT_FILE = recentFilePath();
9706
11031
  try {
9707
- const raw = await fs41.readFile(RECENT_FILE, "utf8");
11032
+ const raw = await fs44.readFile(RECENT_FILE, "utf8");
9708
11033
  const parsed = JSON.parse(raw);
9709
11034
  return Array.isArray(parsed) ? parsed.slice(0, MAX) : [];
9710
11035
  } catch {
@@ -9713,8 +11038,8 @@ async function loadRecent() {
9713
11038
  }
9714
11039
  async function saveRecent(list) {
9715
11040
  const RECENT_FILE = recentFilePath();
9716
- await fs41.mkdir(path34.dirname(RECENT_FILE), { recursive: true });
9717
- await fs41.writeFile(RECENT_FILE, JSON.stringify(list.slice(0, MAX), null, 2) + "\n", "utf8");
11041
+ await fs44.mkdir(path35.dirname(RECENT_FILE), { recursive: true });
11042
+ await fs44.writeFile(RECENT_FILE, JSON.stringify(list.slice(0, MAX), null, 2) + "\n", "utf8");
9718
11043
  }
9719
11044
  function mountProjectsApi(app, opts) {
9720
11045
  app.get("/api/projects/recent", async (c) => c.json(await loadRecent()));
@@ -9737,14 +11062,15 @@ function mountApi(app, opts) {
9737
11062
  mountConfigApi(app, opts);
9738
11063
  mountRunsApi(app, opts);
9739
11064
  mountSecretsApi(app, opts);
11065
+ mountPlannerAuthApi(app, opts);
9740
11066
  mountTrackerApi(app, opts);
9741
11067
  mountDoctorApi(app, opts);
9742
11068
  mountCopyPlanPromptApi(app, opts);
9743
11069
  }
9744
11070
 
9745
11071
  // src/console/static-assets.ts
9746
- import fs42 from "fs";
9747
- import path35 from "path";
11072
+ import fs45 from "fs";
11073
+ import path36 from "path";
9748
11074
  import { fileURLToPath as fileURLToPath2 } from "url";
9749
11075
  import { Readable } from "stream";
9750
11076
  function pathnameForStatic(c) {
@@ -9774,18 +11100,18 @@ function mountStaticAssets(app, _opts) {
9774
11100
  }
9775
11101
  if (c.req.path.startsWith("/api/") || c.req.path.startsWith("/healthz")) return c.notFound();
9776
11102
  if (!distDir) return c.text("UI not built. Run `pnpm -C console-ui build`.", 404);
9777
- const indexHtml = path35.join(distDir, "index.html");
9778
- if (!fs42.existsSync(indexHtml)) return c.text("UI not built", 404);
9779
- return c.html(fs42.readFileSync(indexHtml, "utf8"), 200, { "cache-control": "no-store" });
11103
+ const indexHtml = path36.join(distDir, "index.html");
11104
+ if (!fs45.existsSync(indexHtml)) return c.text("UI not built", 404);
11105
+ return c.html(fs45.readFileSync(indexHtml, "utf8"), 200, { "cache-control": "no-store" });
9780
11106
  });
9781
11107
  }
9782
11108
  function assetResponse(c, distDir, pathname) {
9783
11109
  if (!distDir) return c.text("UI not built", 404);
9784
11110
  const rel = pathname.replace(/^\//, "");
9785
- const abs = path35.normalize(path35.join(distDir, rel));
11111
+ const abs = path36.normalize(path36.join(distDir, rel));
9786
11112
  if (!isInside2(abs, distDir)) return c.text("forbidden", 403);
9787
- if (!fs42.existsSync(abs) || !fs42.statSync(abs).isFile()) return c.text("not found", 404);
9788
- const body = Readable.toWeb(fs42.createReadStream(abs));
11113
+ if (!fs45.existsSync(abs) || !fs45.statSync(abs).isFile()) return c.text("not found", 404);
11114
+ const body = Readable.toWeb(fs45.createReadStream(abs));
9789
11115
  return new Response(body, {
9790
11116
  headers: {
9791
11117
  "content-type": contentTypeFor(abs),
@@ -9795,9 +11121,9 @@ function assetResponse(c, distDir, pathname) {
9795
11121
  }
9796
11122
  function serveFile(c, distDir, name, ct) {
9797
11123
  if (!distDir) return c.text("UI not built", 404);
9798
- const abs = path35.join(distDir, name);
9799
- if (!fs42.existsSync(abs)) return c.text("not found", 404);
9800
- return new Response(fs42.readFileSync(abs), {
11124
+ const abs = path36.join(distDir, name);
11125
+ if (!fs45.existsSync(abs)) return c.text("not found", 404);
11126
+ return new Response(fs45.readFileSync(abs), {
9801
11127
  headers: { "content-type": ct, "cache-control": "public, max-age=86400" }
9802
11128
  });
9803
11129
  }
@@ -9812,13 +11138,13 @@ function contentTypeFor(p) {
9812
11138
  return "application/octet-stream";
9813
11139
  }
9814
11140
  function isInside2(abs, parent) {
9815
- const a = path35.resolve(abs) + path35.sep;
9816
- const p = path35.resolve(parent) + path35.sep;
11141
+ const a = path36.resolve(abs) + path36.sep;
11142
+ const p = path36.resolve(parent) + path36.sep;
9817
11143
  return a.startsWith(p);
9818
11144
  }
9819
11145
  function resolveConsoleUiDist() {
9820
11146
  const fromEnv = process.env.SQUAD_CONSOLE_UI_DIST;
9821
- if (fromEnv && fs42.existsSync(path35.join(fromEnv, "index.html"))) return fromEnv;
11147
+ if (fromEnv && fs45.existsSync(path36.join(fromEnv, "index.html"))) return fromEnv;
9822
11148
  const candidates = [
9823
11149
  fileURLToPath2(new URL("./console-ui/", import.meta.url)),
9824
11150
  // dist/cli.js → dist/console-ui/
@@ -9830,7 +11156,7 @@ function resolveConsoleUiDist() {
9830
11156
  // src/console/*.ts → project/console-ui/dist
9831
11157
  ];
9832
11158
  for (const c of candidates) {
9833
- if (fs42.existsSync(path35.join(c, "index.html"))) return c;
11159
+ if (fs45.existsSync(path36.join(c, "index.html"))) return c;
9834
11160
  }
9835
11161
  return null;
9836
11162
  }
@@ -9846,9 +11172,9 @@ function authMiddleware(expected) {
9846
11172
  };
9847
11173
  }
9848
11174
  function readPresentedToken(req) {
9849
- const auth = req.headers.get("authorization");
9850
- if (auth && auth.toLowerCase().startsWith("bearer ")) {
9851
- return auth.slice("bearer ".length).trim();
11175
+ const auth2 = req.headers.get("authorization");
11176
+ if (auth2 && auth2.toLowerCase().startsWith("bearer ")) {
11177
+ return auth2.slice("bearer ".length).trim();
9852
11178
  }
9853
11179
  const url = new URL(req.url);
9854
11180
  return url.searchParams.get("t");
@@ -10012,18 +11338,18 @@ async function runConsole(opts) {
10012
11338
  }
10013
11339
 
10014
11340
  // src/commands/rm/story.ts
10015
- import fs44 from "fs";
10016
- import path37 from "path";
10017
- import { confirm as confirm10 } from "@inquirer/prompts";
11341
+ import fs47 from "fs";
11342
+ import path38 from "path";
11343
+ import { confirm as confirm11 } from "@inquirer/prompts";
10018
11344
 
10019
11345
  // src/commands/rm/feature.ts
10020
- import fs43 from "fs";
10021
- import path36 from "path";
10022
- import { confirm as confirm9 } from "@inquirer/prompts";
11346
+ import fs46 from "fs";
11347
+ import path37 from "path";
11348
+ import { confirm as confirm10 } from "@inquirer/prompts";
10023
11349
  function countStoryDirs(storiesDir, feature) {
10024
- const d = path36.join(storiesDir, feature);
10025
- if (!fs43.existsSync(d)) return 0;
10026
- return fs43.readdirSync(d, { withFileTypes: true }).filter((x) => x.isDirectory()).length;
11350
+ const d = path37.join(storiesDir, feature);
11351
+ if (!fs46.existsSync(d)) return 0;
11352
+ return fs46.readdirSync(d, { withFileTypes: true }).filter((x) => x.isDirectory()).length;
10027
11353
  }
10028
11354
  async function runRmFeature(featureSlug, opts = {}) {
10029
11355
  const root = requireSquadRoot();
@@ -10057,17 +11383,17 @@ async function runRmFeature(featureSlug, opts = {}) {
10057
11383
  feature = await pickFeatureInteractive(paths);
10058
11384
  }
10059
11385
  }
10060
- const storyDir = path36.join(paths.storiesDir, feature);
10061
- const planDir = path36.join(paths.plansDir, feature);
11386
+ const storyDir = path37.join(paths.storiesDir, feature);
11387
+ const planDir = path37.join(paths.plansDir, feature);
10062
11388
  const nStories = countStoryDirs(paths.storiesDir, feature);
10063
11389
  const nPlans = listAllPlanListEntries(paths, feature).length;
10064
- const hasOverview = fs43.existsSync(path36.join(planDir, "00-overview.md")) ? 1 : 0;
11390
+ const hasOverview = fs46.existsSync(path37.join(planDir, "00-overview.md")) ? 1 : 0;
10065
11391
  const targets = [];
10066
- if (fs43.existsSync(storyDir)) {
10067
- targets.push({ label: path36.relative(root, storyDir), absPath: storyDir, kind: "dir" });
11392
+ if (fs46.existsSync(storyDir)) {
11393
+ targets.push({ label: path37.relative(root, storyDir), absPath: storyDir, kind: "dir" });
10068
11394
  }
10069
- if (fs43.existsSync(planDir)) {
10070
- targets.push({ label: path36.relative(root, planDir), absPath: planDir, kind: "dir" });
11395
+ if (fs46.existsSync(planDir)) {
11396
+ targets.push({ label: path37.relative(root, planDir), absPath: planDir, kind: "dir" });
10071
11397
  }
10072
11398
  divider("squad rm feature");
10073
11399
  info(`Feature: ${feature}`);
@@ -10082,7 +11408,7 @@ async function runRmFeature(featureSlug, opts = {}) {
10082
11408
  kv(t.kind, t.label, 6);
10083
11409
  }
10084
11410
  if (useTrash) {
10085
- info(`Items will move to ${path36.relative(process.cwd(), paths.trashDir)}/<timestamp>/`);
11411
+ info(`Items will move to ${path37.relative(process.cwd(), paths.trashDir)}/<timestamp>/`);
10086
11412
  } else if (!dryRun) {
10087
11413
  warning("This is a permanent delete. Pass --trash to move to .squad/.trash/ instead.");
10088
11414
  }
@@ -10100,7 +11426,7 @@ async function runRmFeature(featureSlug, opts = {}) {
10100
11426
  "This command removes an entire feature. Run `squad rm feature --yes` in CI, or `squad rm feature` in a TTY to confirm the prompt interactively."
10101
11427
  );
10102
11428
  }
10103
- const ok = await confirm9({
11429
+ const ok = await confirm10({
10104
11430
  message: `Delete all of feature "${feature}" (stories + plans)?`,
10105
11431
  default: false
10106
11432
  });
@@ -10115,7 +11441,7 @@ async function runRmFeature(featureSlug, opts = {}) {
10115
11441
  useTrash
10116
11442
  );
10117
11443
  if (result.trashed) {
10118
- success(`Feature removed. Items moved to ${path36.relative(root, result.trashed)}.`);
11444
+ success(`Feature removed. Items moved to ${path37.relative(root, result.trashed)}.`);
10119
11445
  } else {
10120
11446
  success("Feature removed (stories and plans directories).");
10121
11447
  }
@@ -10156,11 +11482,11 @@ async function runRmStory(storyPathOrId, opts = {}) {
10156
11482
  }
10157
11483
  }
10158
11484
  const targets = [
10159
- { label: path37.relative(root, story.storyDir), absPath: story.storyDir, kind: "dir" }
11485
+ { label: path38.relative(root, story.storyDir), absPath: story.storyDir, kind: "dir" }
10160
11486
  ];
10161
11487
  if (story.planFile) {
10162
- const planAbs = path37.join(paths.plansDir, story.feature, story.planFile);
10163
- targets.push({ label: path37.relative(root, planAbs), absPath: planAbs, kind: "file" });
11488
+ const planAbs = path38.join(paths.plansDir, story.feature, story.planFile);
11489
+ targets.push({ label: path38.relative(root, planAbs), absPath: planAbs, kind: "file" });
10164
11490
  }
10165
11491
  renderPreview("story", targets, {
10166
11492
  dryRun,
@@ -10177,7 +11503,7 @@ async function runRmStory(storyPathOrId, opts = {}) {
10177
11503
  "Pass --yes to confirm deletion in a non-interactive environment, or run `squad rm story` in a TTY to confirm the prompt interactively."
10178
11504
  );
10179
11505
  }
10180
- const ok = await confirm10({ message: "Delete this story and related plan/overview row?", default: false });
11506
+ const ok = await confirm11({ message: "Delete this story and related plan/overview row?", default: false });
10181
11507
  if (!ok) {
10182
11508
  info("Cancelled.");
10183
11509
  return;
@@ -10185,18 +11511,18 @@ async function runRmStory(storyPathOrId, opts = {}) {
10185
11511
  }
10186
11512
  const result = deleteStoryRecord({ paths, story, trash: useTrash });
10187
11513
  if (result.trashed) {
10188
- success(`Removed (moved to ${path37.relative(root, result.trashed)}).`);
11514
+ success(`Removed (moved to ${path38.relative(root, result.trashed)}).`);
10189
11515
  } else {
10190
11516
  success("Removed story folder, plan file (if any), and overview row.");
10191
11517
  }
10192
- const featurePlanDir = path37.join(paths.plansDir, story.feature);
10193
- const overviewPath = path37.join(featurePlanDir, "00-overview.md");
10194
- if (fs44.existsSync(overviewPath)) {
10195
- const body = fs44.readFileSync(overviewPath, "utf8");
11518
+ const featurePlanDir = path38.join(paths.plansDir, story.feature);
11519
+ const overviewPath = path38.join(featurePlanDir, "00-overview.md");
11520
+ if (fs47.existsSync(overviewPath)) {
11521
+ const body = fs47.readFileSync(overviewPath, "utf8");
10196
11522
  const onlyShell = countOverviewDataRows(body) === 0;
10197
11523
  if (onlyShell && featureStoriesDirEmpty(paths, story.feature)) {
10198
11524
  if (canPrompt) {
10199
- const also = await confirm10({
11525
+ const also = await confirm11({
10200
11526
  message: `Feature "${story.feature}" has no stories left and an empty overview table. Remove the whole feature with \`squad rm feature ${story.feature}\` now?`,
10201
11527
  default: false
10202
11528
  });
@@ -10212,8 +11538,8 @@ async function runRmStory(storyPathOrId, opts = {}) {
10212
11538
  }
10213
11539
 
10214
11540
  // src/commands/rm/plan.ts
10215
- import path38 from "path";
10216
- import { confirm as confirm11 } from "@inquirer/prompts";
11541
+ import path39 from "path";
11542
+ import { confirm as confirm12 } from "@inquirer/prompts";
10217
11543
  async function runRmPlan(planPathOrSequence, opts = {}) {
10218
11544
  const root = requireSquadRoot();
10219
11545
  const paths = buildPaths(root);
@@ -10248,7 +11574,7 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
10248
11574
  }
10249
11575
  const storyRows = listStories(paths, { feature: plan.feature });
10250
11576
  const linked = storyRows.find((s) => s.planFile === plan.file);
10251
- const targets = [{ label: path38.relative(root, plan.abs), absPath: plan.abs, kind: "file" }];
11577
+ const targets = [{ label: path39.relative(root, plan.abs), absPath: plan.abs, kind: "file" }];
10252
11578
  renderPreview("plan", targets, {
10253
11579
  dryRun,
10254
11580
  trash: useTrash,
@@ -10264,18 +11590,54 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
10264
11590
  "Pass --yes to confirm deletion in a non-interactive environment, or run `squad rm plan` in a TTY."
10265
11591
  );
10266
11592
  }
10267
- const ok = await confirm11({ message: "Delete this plan file only? (intake and config are kept.)", default: false });
11593
+ const ok = await confirm12({ message: "Delete this plan file only? (intake and config are kept.)", default: false });
10268
11594
  if (!ok) {
10269
11595
  info("Cancelled.");
10270
11596
  return;
10271
11597
  }
10272
11598
  }
10273
11599
  trashOrDelete([plan.abs], paths.trashDir, useTrash);
10274
- removeOverviewRowForPlanFile(path38.join(paths.plansDir, plan.feature), plan.file);
10275
- const intakeHint = linked ? path38.relative(root, linked.intakePath) : `under .squad/stories/${plan.feature}/`;
11600
+ removeOverviewRowForPlanFile(path39.join(paths.plansDir, plan.feature), plan.file);
11601
+ const intakeHint = linked ? path39.relative(root, linked.intakePath) : `under .squad/stories/${plan.feature}/`;
10276
11602
  success(`Plan removed. Run \`squad new-plan ${intakeHint}\` to regenerate.`);
10277
11603
  }
10278
11604
 
11605
+ // src/commands/auth/logout.ts
11606
+ import fs48 from "fs";
11607
+ import { confirm as confirm13 } from "@inquirer/prompts";
11608
+ async function runAuthLogout(opts = {}) {
11609
+ const paths = buildPaths(requireSquadRoot());
11610
+ const secrets = fs48.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
11611
+ if (!secrets.planner?.anthropicOauthToken) {
11612
+ info("No Claude OAuth token is stored in .squad/secrets.yaml \u2014 nothing to remove.");
11613
+ info("Your Claude Code login (OS credential store) is separate and was not touched.");
11614
+ return;
11615
+ }
11616
+ if (!opts.yes) {
11617
+ if (!isInteractive()) {
11618
+ warning("Pass -y in non-interactive mode to confirm, or run `squad auth logout` in a TTY.");
11619
+ return;
11620
+ }
11621
+ const ok = await confirm13({
11622
+ message: "Remove the Claude OAuth token squad-kit stored in .squad/secrets.yaml?",
11623
+ default: false
11624
+ });
11625
+ if (!ok) return;
11626
+ }
11627
+ delete secrets.planner.anthropicOauthToken;
11628
+ saveSecrets(paths.secretsFile, secrets);
11629
+ success("Removed the OAuth token squad-kit stored.");
11630
+ info(
11631
+ "Your Claude Code login is untouched \u2014 run `claude` and `/logout` if you also want to sign out of Claude Code."
11632
+ );
11633
+ const mode = loadConfig(paths.configFile).planner?.auth?.anthropic;
11634
+ if (mode === "subscription") {
11635
+ info(
11636
+ "`planner.auth.anthropic` is still `subscription`. If no Claude login remains on this machine, the next run fails with a message pointing back at `squad auth login`."
11637
+ );
11638
+ }
11639
+ }
11640
+
10279
11641
  // src/cli.ts
10280
11642
  var program = new Command();
10281
11643
  program.name("squad").description("Plan once, execute cheap. A 3-step SDD workflow CLI.").version(readInstalledPackage().version);
@@ -10344,6 +11706,14 @@ rm.command("plan [plan-path-or-sequence]").description("Delete a single plan fil
10344
11706
  rm.command("feature [feature-slug]").description("Delete an entire feature: every story, every plan, the overview file").option("--dry-run", "Show what would be deleted without touching the filesystem", false).option("--trash", "Move into .squad/.trash/<ts>/ instead of permanent delete", false).option("-y, --yes", "Skip the confirmation prompt", false).action(wrapArgs(runRmFeature));
10345
11707
  var tracker = program.command("tracker").description("Tracker id helpers");
10346
11708
  tracker.command("link [story-path] [tracker-id]").description("Attach/update a tracker id on an existing story intake").option("-y, --yes", "Fail fast instead of prompting for missing values", false).action(wrapArgs(runTrackerLink));
11709
+ var auth = program.command("auth").description(
11710
+ "Sign in to Anthropic with your Claude subscription, inspect it, or clear the stored token. Precedence, CI setup, usage limits, and troubleshooting: https://squad-kit.com/docs/auth"
11711
+ );
11712
+ auth.command("login").description(
11713
+ "Run the Anthropic browser authorization flow and store the resulting token in .squad/secrets.yaml. Prefer not to store a token? Run `claude` and use `/login` instead \u2014 squad-kit picks up the OS credential store automatically under `auth: subscription`."
11714
+ ).option("--token <value>", "Use a `claude setup-token` value you already have (CI / remote machines; no browser)").option("--print-only", "Run the flow, print the token, store nothing and change no config", false).option("-y, --yes", "Overwrite an already-stored token without confirming", false).action(wrap(runAuthLogin));
11715
+ auth.command("status").description("Show the resolved auth mode, the credential behind it, and the signed-in account").option("--json", "Emit a stable JSON object for scripting (never contains the token)", false).option("--offline", "Skip the live account check; report only what resolves locally", false).action(wrap(runAuthStatus));
11716
+ auth.command("logout").description("Remove only the OAuth token squad-kit stored; your Claude Code login is untouched").option("-y, --yes", "Skip the confirmation prompt", false).action(wrap(runAuthLogout));
10347
11717
  var config = program.command("config").description("Inspect and change .squad/ settings");
10348
11718
  config.command("show").description("Show the current .squad/config.yaml and .squad/secrets.yaml state (secrets masked)").option("--json", "Emit as JSON to stdout (secrets still masked)", false).action(wrap(runConfigShow));
10349
11719
  var set = config.command("set").description("Interactively change a section");