squad-kit 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,345 @@ 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
+ return {
1460
+ account: Object.keys(account).length > 0 ? account : void 0,
1461
+ apiKeySource
1462
+ };
1463
+ }
1464
+ var EXPIRED_HINT = /expire|refresh|revoked|no longer valid/i;
1465
+ var NO_BINARY_HINT = /native cli binary|executable not found|claude code executable/i;
1466
+ function mapProbeFailure(err2, auth2) {
1467
+ const message = err2 instanceof Error ? err2.message : String(err2);
1468
+ const signal = detectAuthShapedSdkError(message);
1469
+ if (signal === "authentication_failed") {
1470
+ return {
1471
+ ok: false,
1472
+ kind: EXPIRED_HINT.test(message) ? "expired" : "not-logged-in",
1473
+ // Reuse story 15's mapper rather than writing a second one: the copy a failed probe prints
1474
+ // must match the copy a failed run prints, or the two disagree about the same credential.
1475
+ detail: authErrorMessage("authentication_failed", authErrorContextFrom(auth2))
1476
+ };
1477
+ }
1478
+ if (signal === "oauth_org_not_allowed") {
1479
+ return {
1480
+ ok: false,
1481
+ kind: "org-not-allowed",
1482
+ detail: authErrorMessage("oauth_org_not_allowed", authErrorContextFrom(auth2))
1483
+ };
1484
+ }
1485
+ if (signal) {
1486
+ return { ok: false, kind: "unknown", detail: authErrorMessage(signal, authErrorContextFrom(auth2)) };
1487
+ }
1488
+ if (NO_BINARY_HINT.test(message)) {
1489
+ return { ok: false, kind: "no-binary", detail: message.slice(0, 300) };
1490
+ }
1491
+ return { ok: false, kind: "unknown", detail: message.slice(0, 300) };
1492
+ }
1493
+ async function probeClaudeAuth(auth2, opts = {}) {
1494
+ if (!resolveClaudeBinary()) {
1495
+ return {
1496
+ ok: false,
1497
+ kind: "no-binary",
1498
+ 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."
1499
+ };
1500
+ }
1501
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_AUTH_PROBE_TIMEOUT_MS;
1502
+ let releaseInput = () => {
1503
+ };
1504
+ const inputClosed = new Promise((resolve2) => {
1505
+ releaseInput = resolve2;
1506
+ });
1507
+ async function* neverSends() {
1508
+ await inputClosed;
1509
+ }
1510
+ let q;
1511
+ let timer;
1512
+ try {
1513
+ const { query } = await import("@anthropic-ai/claude-agent-sdk");
1514
+ q = query({
1515
+ prompt: neverSends(),
1516
+ options: {
1517
+ tools: [],
1518
+ settingSources: [],
1519
+ persistSession: false,
1520
+ maxTurns: 1,
1521
+ env: buildSdkEnv(auth2)
1522
+ }
1523
+ });
1524
+ const timedOut = /* @__PURE__ */ Symbol("timeout");
1525
+ const info2 = await Promise.race([
1526
+ typeof q.accountInfo === "function" ? q.accountInfo() : Promise.resolve(void 0),
1527
+ new Promise((resolve2) => {
1528
+ timer = setTimeout(() => resolve2(timedOut), timeoutMs);
1529
+ })
1530
+ ]);
1531
+ if (info2 === timedOut) {
1532
+ return {
1533
+ ok: false,
1534
+ kind: "timeout",
1535
+ detail: `The Claude auth check did not answer within ${timeoutMs}ms.`
1536
+ };
1537
+ }
1538
+ const { account, apiKeySource } = readAccount(info2);
1539
+ if (auth2.mode === "subscription" && !account && !apiKeySource) {
1540
+ return {
1541
+ ok: false,
1542
+ kind: "unknown",
1543
+ detail: "The Claude login did not report an account, so it could not be verified. The credential may be invalid or expired \u2014 run `squad auth login` to sign in again."
1544
+ };
1545
+ }
1546
+ return { ok: true, ...apiKeySource ? { apiKeySource } : {}, ...account ? { account } : {} };
1547
+ } catch (err2) {
1548
+ return mapProbeFailure(err2, auth2);
1549
+ } finally {
1550
+ if (timer) clearTimeout(timer);
1551
+ releaseInput();
1552
+ try {
1553
+ await q?.return?.();
1554
+ } catch {
1555
+ }
1556
+ }
1557
+ }
1558
+
1029
1559
  // src/commands/config/shared.ts
1030
1560
  import { input, password } from "@inquirer/prompts";
1031
1561
  var notEmpty = (v) => v.trim().length > 0 || "required";
@@ -1106,9 +1636,12 @@ function mergePlannerKeyIntoSecrets(base, provider, key) {
1106
1636
  }
1107
1637
  return mergeSecrets(base, { planner: { google: key } });
1108
1638
  }
1639
+ function mergePlannerOauthTokenIntoSecrets(base, token) {
1640
+ return mergeSecrets(base, { planner: { anthropicOauthToken: token } });
1641
+ }
1109
1642
 
1110
1643
  // src/commands/config/unset-planner.ts
1111
- import fs8 from "fs";
1644
+ import fs10 from "fs";
1112
1645
  import { confirm } from "@inquirer/prompts";
1113
1646
  async function runConfigUnsetPlanner(opts = {}) {
1114
1647
  const root = requireSquadRoot();
@@ -1125,28 +1658,246 @@ async function runConfigUnsetPlanner(opts = {}) {
1125
1658
  return;
1126
1659
  }
1127
1660
  }
1128
- const next = { ...config2 };
1129
- delete next.planner;
1130
- saveConfig(paths.configFile, next);
1131
- if (opts.removeCredentials) {
1132
- const s = fs8.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
1133
- if (s.planner) {
1134
- delete s.planner;
1135
- saveSecrets(paths.secretsFile, s);
1661
+ const next = { ...config2 };
1662
+ delete next.planner;
1663
+ saveConfig(paths.configFile, next);
1664
+ if (opts.removeCredentials) {
1665
+ const s = fs10.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
1666
+ if (s.planner) {
1667
+ delete s.planner;
1668
+ saveSecrets(paths.secretsFile, s);
1669
+ }
1670
+ success("Planner disabled; planner credentials were removed from .squad/secrets.yaml.");
1671
+ } else {
1672
+ success("Planner disabled; existing planner keys in .squad/secrets.yaml (if any) were preserved.");
1673
+ }
1674
+ info("Re-enable any time with `squad config set planner`.");
1675
+ }
1676
+
1677
+ // src/commands/auth/login.ts
1678
+ import fs11 from "fs";
1679
+ import { confirm as confirm2, password as password2 } from "@inquirer/prompts";
1680
+
1681
+ // src/core/ci-env.ts
1682
+ function skipExternalProbesInAutomation() {
1683
+ const c = process.env.CI;
1684
+ if (c === "true" || c === "1") return true;
1685
+ if (process.env.GITHUB_ACTIONS) return true;
1686
+ return false;
1687
+ }
1688
+
1689
+ // src/commands/auth/setup-token.ts
1690
+ import { spawn } from "child_process";
1691
+ function spawnSetupToken(binaryPath, opts = {}) {
1692
+ const tee = opts.tee !== false;
1693
+ return new Promise((resolve2, reject) => {
1694
+ const child = spawn(binaryPath, ["setup-token"], {
1695
+ stdio: tee ? ["inherit", "pipe", "inherit"] : "inherit",
1696
+ windowsHide: true
1697
+ });
1698
+ let output = "";
1699
+ if (tee && child.stdout) {
1700
+ child.stdout.setEncoding("utf8");
1701
+ child.stdout.on("data", (chunk) => {
1702
+ output += chunk;
1703
+ process.stderr.write(chunk);
1704
+ });
1705
+ }
1706
+ child.once("error", reject);
1707
+ child.once("close", (code, signal) => {
1708
+ resolve2({ code, signal, output });
1709
+ });
1710
+ });
1711
+ }
1712
+
1713
+ // src/commands/auth/shared.ts
1714
+ var OAUTH_TOKEN_PREFIX = "sk-ant-oat";
1715
+ var OAUTH_TOKEN_PATTERN = /sk-ant-oat[0-9]{2}-[A-Za-z0-9_-]{16,}/;
1716
+ var MIN_TOKEN_LENGTH = 20;
1717
+ var MAX_TOKEN_LENGTH = 2048;
1718
+ var InvalidOauthTokenError = class extends Error {
1719
+ constructor(message) {
1720
+ super(message);
1721
+ this.name = "InvalidOauthTokenError";
1722
+ }
1723
+ };
1724
+ function validateOauthToken(raw) {
1725
+ const value = raw ?? "";
1726
+ if (value.trim().length === 0) {
1727
+ throw new InvalidOauthTokenError(
1728
+ "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`."
1729
+ );
1730
+ }
1731
+ if (/\s/.test(value.trim())) {
1732
+ throw new InvalidOauthTokenError(
1733
+ "That token contains whitespace. Paste only the single-line value printed by `claude setup-token` (quote it in your shell if needed)."
1734
+ );
1735
+ }
1736
+ const token = value.trim();
1737
+ if (token.startsWith("sk-ant-") && !token.startsWith(OAUTH_TOKEN_PREFIX)) {
1738
+ throw new InvalidOauthTokenError(
1739
+ "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`."
1740
+ );
1741
+ }
1742
+ if (token.length < MIN_TOKEN_LENGTH) {
1743
+ throw new InvalidOauthTokenError(
1744
+ `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.`
1745
+ );
1746
+ }
1747
+ if (token.length > MAX_TOKEN_LENGTH) {
1748
+ throw new InvalidOauthTokenError(
1749
+ "That token is implausibly long. Paste only the single line `claude setup-token` printed."
1750
+ );
1751
+ }
1752
+ return token;
1753
+ }
1754
+ function extractOauthToken(output) {
1755
+ const hit = OAUTH_TOKEN_PATTERN.exec(output);
1756
+ return hit ? hit[0] : void 0;
1757
+ }
1758
+ function authReasonText(reason, provider) {
1759
+ switch (reason) {
1760
+ case "explicit-config":
1761
+ return "explicit in .squad/config.yaml";
1762
+ case "auto-login-detected":
1763
+ return "auto \u2014 Claude login detected";
1764
+ case "auto-oauth-token":
1765
+ return "auto \u2014 stored OAuth token";
1766
+ case "auto-fallback-api-key":
1767
+ return "auto \u2014 no Claude login, fell back to an API key";
1768
+ case "provider-is-api-key-only":
1769
+ return `${provider} is API-key only`;
1770
+ }
1771
+ }
1772
+ function runtimeNameFor(provider, planner) {
1773
+ if (provider !== "anthropic") return "vercel";
1774
+ return planner?.runtime?.anthropic ?? "agent-sdk";
1775
+ }
1776
+ function credentialPresent(auth2) {
1777
+ return auth2.mode === "api-key" || auth2.loginHint !== "assumed";
1778
+ }
1779
+ 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`.";
1780
+
1781
+ // src/commands/auth/login.ts
1782
+ 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.";
1783
+ async function runAuthLogin(opts = {}) {
1784
+ if (opts.token && opts.printOnly) {
1785
+ throw new Error(
1786
+ "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."
1787
+ );
1788
+ }
1789
+ const providedToken = opts.token !== void 0 ? validateOauthToken(opts.token) : void 0;
1790
+ const storing = !opts.printOnly;
1791
+ const workspace = storing ? openWorkspace() : void 0;
1792
+ if (workspace && !await confirmOverwrite(workspace.secrets, Boolean(opts.yes))) return;
1793
+ if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
1794
+ warning(
1795
+ "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."
1796
+ );
1797
+ }
1798
+ const token = providedToken ?? await runBrowserFlow();
1799
+ if (!storing) {
1800
+ blank();
1801
+ success("Login complete. Nothing was stored.");
1802
+ info("Token (copy it somewhere safe \u2014 squad-kit did not save it):");
1803
+ process.stdout.write(`${token}
1804
+ `);
1805
+ blank();
1806
+ info(SUBSCRIPTION_ALTERNATIVE_HINT);
1807
+ info("To store it in this workspace later: `squad auth login --token <value>`.");
1808
+ return;
1809
+ }
1810
+ const ws = workspace;
1811
+ saveSecrets(ws.paths.secretsFile, mergePlannerOauthTokenIntoSecrets(ws.secrets, token));
1812
+ success("OAuth token saved to .squad/secrets.yaml (chmod 0600 on POSIX).");
1813
+ const planner = ws.config.planner;
1814
+ if (planner) {
1815
+ saveConfig(ws.paths.configFile, {
1816
+ ...ws.config,
1817
+ planner: { ...planner, auth: { ...planner.auth, anthropic: "subscription" } }
1818
+ });
1819
+ info("Set `planner.auth.anthropic: subscription` in .squad/config.yaml.");
1820
+ } else {
1821
+ warning(
1822
+ "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."
1823
+ );
1824
+ }
1825
+ await verifyAndSummarise(planner ? { ...planner, auth: { anthropic: "subscription" } } : void 0);
1826
+ blank();
1827
+ info(SUBSCRIPTION_ALTERNATIVE_HINT);
1828
+ info("Next: `squad auth status` to confirm, then `squad new-plan --api`.");
1829
+ }
1830
+ function openWorkspace() {
1831
+ const paths = buildPaths(requireSquadRoot());
1832
+ return {
1833
+ paths,
1834
+ config: loadConfig(paths.configFile),
1835
+ secrets: fs11.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {}
1836
+ };
1837
+ }
1838
+ async function confirmOverwrite(secrets, yes) {
1839
+ if (!secrets.planner?.anthropicOauthToken) return true;
1840
+ if (yes) return true;
1841
+ if (!isInteractive()) {
1842
+ throw new Error(
1843
+ "A Claude OAuth token is already stored in .squad/secrets.yaml. Pass -y to overwrite it non-interactively."
1844
+ );
1845
+ }
1846
+ const ok = await confirm2({
1847
+ message: "A Claude OAuth token is already stored. Replace it with a new one?",
1848
+ default: false
1849
+ });
1850
+ if (!ok) info("Keeping the existing token; nothing changed.");
1851
+ return ok;
1852
+ }
1853
+ async function runBrowserFlow() {
1854
+ if (!isInteractive()) throw new Error(NON_TTY_LOGIN_MESSAGE);
1855
+ const binary = resolveClaudeBinary();
1856
+ if (!binary) throw new Error(MISSING_CLAUDE_BINARY_MESSAGE);
1857
+ step("Starting the Anthropic browser authorization flow\u2026");
1858
+ info(`Using ${binary.source === "bundled" ? "the bundled Agent SDK binary" : "the `claude` on your PATH"}.`);
1859
+ blank();
1860
+ const result = await spawnSetupToken(binary.path);
1861
+ if (result.signal) {
1862
+ throw new SquadExit(130, "Login cancelled \u2014 nothing was stored.");
1863
+ }
1864
+ if (result.code !== 0) {
1865
+ throw new SquadExit(
1866
+ result.code ?? 1,
1867
+ `\`claude setup-token\` exited with code ${result.code}. Nothing was stored. Re-run \`squad auth login\` to try again.`
1868
+ );
1869
+ }
1870
+ const extracted = extractOauthToken(result.output);
1871
+ if (extracted) return validateOauthToken(extracted);
1872
+ blank();
1873
+ warning("Could not read the token from the login output. Paste it here:");
1874
+ const pasted = await password2({
1875
+ message: "Token (input hidden):",
1876
+ validate: (v) => {
1877
+ try {
1878
+ validateOauthToken(v);
1879
+ return true;
1880
+ } catch (e) {
1881
+ return e instanceof Error ? e.message : "invalid token";
1882
+ }
1136
1883
  }
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.");
1884
+ });
1885
+ return validateOauthToken(pasted);
1886
+ }
1887
+ async function verifyAndSummarise(planner) {
1888
+ if (!planner || skipExternalProbesInAutomation()) return;
1889
+ const auth2 = resolvePlannerAuthForCwd("anthropic", planner);
1890
+ const spin = spinner("verifying the Claude login\u2026");
1891
+ const probe = await probeClaudeAuth(auth2);
1892
+ if (!probe.ok) {
1893
+ spin.fail(`could not verify the login (${probe.kind})`);
1894
+ warning(probe.detail);
1895
+ return;
1140
1896
  }
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;
1897
+ spin.succeed("login verified");
1898
+ const account = [probe.account?.email, probe.account?.organization, probe.account?.subscriptionType].filter(Boolean).join(" \xB7 ");
1899
+ if (account) kv("account", account, 15);
1900
+ if (probe.apiKeySource) kv("api key source", probe.apiKeySource, 15);
1150
1901
  }
1151
1902
 
1152
1903
  // src/commands/config/set-planner.ts
@@ -1168,7 +1919,7 @@ async function runConfigSetPlanner(opts = {}) {
1168
1919
  const root = requireSquadRoot();
1169
1920
  const paths = buildPaths(root);
1170
1921
  const config2 = loadConfig(paths.configFile);
1171
- const baseSecrets = fs9.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
1922
+ const baseSecrets = fs12.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
1172
1923
  const useYes = Boolean(opts.yes);
1173
1924
  const interactive = !useYes && isInteractive();
1174
1925
  if (interactive && config2.planner?.enabled === true) {
@@ -1205,7 +1956,7 @@ async function runConfigSetPlanner(opts = {}) {
1205
1956
  }
1206
1957
  if (interactive) {
1207
1958
  if (!config2.planner || config2.planner.enabled !== true) {
1208
- const enable = await confirm2({ message: "Enable the direct planner?", default: true });
1959
+ const enable = await confirm3({ message: "Enable the direct planner?", default: true });
1209
1960
  if (!enable) {
1210
1961
  return;
1211
1962
  }
@@ -1218,42 +1969,45 @@ async function runConfigSetPlanner(opts = {}) {
1218
1969
  } else {
1219
1970
  nextPlanner = { ...prev, provider, enabled: true };
1220
1971
  }
1972
+ let authChoice;
1973
+ if (interactive && provider === "anthropic") {
1974
+ authChoice = await select({
1975
+ message: "How should squad-kit authenticate with Anthropic?",
1976
+ choices: [
1977
+ {
1978
+ name: "Use my Claude subscription (browser login) \u2014 recommended, no API key needed",
1979
+ value: "subscription"
1980
+ },
1981
+ { name: "Use an Anthropic API key", value: "api-key" },
1982
+ {
1983
+ name: "Decide automatically (subscription if signed in, otherwise API key)",
1984
+ value: "auto"
1985
+ }
1986
+ ],
1987
+ default: "subscription"
1988
+ });
1989
+ nextPlanner = { ...nextPlanner, auth: { ...nextPlanner.auth, anthropic: authChoice } };
1990
+ }
1221
1991
  if (useYes) {
1222
1992
  if (!resolveProviderKey(provider)) {
1223
1993
  throw credentialError(provider);
1224
1994
  }
1225
1995
  } 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.");
1996
+ if (authChoice === "subscription") {
1997
+ await offerBrowserLogin(baseSecrets);
1998
+ } else if (authChoice === "auto") {
1999
+ info(
2000
+ "`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)."
2001
+ );
2002
+ if (!detectClaudeLogin(baseSecrets).present && !resolveProviderKey(provider)) {
2003
+ warning(
2004
+ "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."
2005
+ );
1244
2006
  }
1245
2007
  } 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)");
2008
+ await promptForApiKey(provider, paths.secretsFile, baseSecrets);
1255
2009
  }
1256
- const cacheEnabled = await confirm2({
2010
+ const cacheEnabled = await confirm3({
1257
2011
  message: "Enable prompt caching? (Recommended \u2014 reduces billed tokens by ~70% on most providers.)",
1258
2012
  default: nextPlanner.cache?.enabled ?? true
1259
2013
  });
@@ -1261,6 +2015,12 @@ async function runConfigSetPlanner(opts = {}) {
1261
2015
  }
1262
2016
  const next = { ...config2, planner: nextPlanner };
1263
2017
  saveConfig(paths.configFile, next);
2018
+ const auth2 = interactive && provider === "anthropic" ? tryResolveAuth(provider, nextPlanner) : void 0;
2019
+ const configuredMode = nextPlanner.auth?.anthropic;
2020
+ if (auth2?.mode === "subscription" && (configuredMode === "subscription" || configuredMode === "auto")) {
2021
+ await reportSubscriptionSetup(provider, nextPlanner, auth2);
2022
+ return;
2023
+ }
1264
2024
  const cred = resolveProviderKey(provider);
1265
2025
  if (!cred) {
1266
2026
  warning(
@@ -1278,7 +2038,7 @@ async function runConfigSetPlanner(opts = {}) {
1278
2038
  kv("credential", `${cred.source} (${sourceText})`, 10);
1279
2039
  }
1280
2040
  if (skipExternalProbesInAutomation() || !cred) {
1281
- printPlannerNextSteps(Boolean(cred));
2041
+ printPlannerNextSteps(Boolean(cred), "api-key");
1282
2042
  return;
1283
2043
  }
1284
2044
  try {
@@ -1299,11 +2059,90 @@ async function runConfigSetPlanner(opts = {}) {
1299
2059
  `Could not reach provider model list (${msg.slice(0, 160)}). Key was saved; run \`squad doctor\` to verify.`
1300
2060
  );
1301
2061
  }
1302
- printPlannerNextSteps(true);
2062
+ printPlannerNextSteps(true, "api-key");
2063
+ }
2064
+ async function promptForApiKey(provider, secretsFile, baseSecrets) {
2065
+ const envVar = providerEnvVar(provider);
2066
+ const existing = resolveProviderKey(provider);
2067
+ if (existing) {
2068
+ const shouldUpdate = await confirm3({
2069
+ message: "A planner credential is already available. Enter a new key and save it to .squad/secrets.yaml?",
2070
+ default: false
2071
+ });
2072
+ if (!shouldUpdate) {
2073
+ info("Keeping the existing planner credential; no change to .squad/secrets.yaml for the key.");
2074
+ return;
2075
+ }
2076
+ const key2 = await password3({
2077
+ message: `${envVar} value (input hidden):`,
2078
+ validate: (v) => v.length >= 20 ? true : "key looks too short"
2079
+ });
2080
+ saveSecrets(secretsFile, mergePlannerKeyIntoSecrets(baseSecrets, provider, key2));
2081
+ success("Planner key saved to .squad/secrets.yaml");
2082
+ info(".squad/secrets.yaml updated (chmod 0600 on POSIX)");
2083
+ return;
2084
+ }
2085
+ const key = await password3({
2086
+ message: `${envVar} value (input hidden) \u2014 required:`,
2087
+ validate: (v) => v.length >= 20 ? true : "key looks too short"
2088
+ });
2089
+ saveSecrets(secretsFile, mergePlannerKeyIntoSecrets(baseSecrets, provider, key));
2090
+ success("Planner key saved to .squad/secrets.yaml");
2091
+ info(".squad/secrets.yaml updated (chmod 0600 on POSIX)");
2092
+ }
2093
+ async function offerBrowserLogin(baseSecrets) {
2094
+ if (detectClaudeLogin(baseSecrets).present) {
2095
+ info("A Claude login is already available on this machine \u2014 no API key needed.");
2096
+ return;
2097
+ }
2098
+ const now = await confirm3({
2099
+ message: "No Claude login found. Run the browser login now?",
2100
+ default: true
2101
+ });
2102
+ if (!now) {
2103
+ info("Skipped. Run `squad auth login` when you are ready \u2014 planning needs it in subscription mode.");
2104
+ return;
2105
+ }
2106
+ await runAuthLogin({});
2107
+ }
2108
+ function tryResolveAuth(provider, planner) {
2109
+ try {
2110
+ return resolvePlannerAuthForCwd(provider, planner);
2111
+ } catch {
2112
+ return void 0;
2113
+ }
2114
+ }
2115
+ async function reportSubscriptionSetup(provider, planner, auth2) {
2116
+ blank();
2117
+ success("Planner configuration updated.");
2118
+ kv("provider", provider, 10);
2119
+ kv("model (plan)", modelFor(provider, "plan", planner.modelOverride), 10);
2120
+ kv("model (execute)", modelFor(provider, "execute", planner.modelOverride), 10);
2121
+ kv("auth", `subscription (${describeAuth(auth2).credentialHint})`, 10);
2122
+ if (!skipExternalProbesInAutomation()) {
2123
+ const probe = await probeClaudeAuth(auth2);
2124
+ if (probe.ok) {
2125
+ const account = [probe.account?.email, probe.account?.organization, probe.account?.subscriptionType].filter(Boolean).join(" \xB7 ");
2126
+ info(account ? `Credential check: signed in as ${account}.` : "Credential check: the Claude login responded OK.");
2127
+ } else {
2128
+ warning(`Credential check failed (${probe.kind}). ${probe.detail}`);
2129
+ }
2130
+ }
2131
+ printPlannerNextSteps(true, "subscription");
1303
2132
  }
1304
- function printPlannerNextSteps(credentialReady) {
2133
+ function printPlannerNextSteps(credentialReady, mode) {
1305
2134
  blank();
1306
2135
  step("Next:");
2136
+ if (mode === "subscription") {
2137
+ info("1) Confirm the login with `squad auth status` \u2014 it shows the account and how it resolved.");
2138
+ info("2) Verify with `squad doctor` \u2014 every planner check should be green.");
2139
+ info("3) Create a story: squad new-story <feature-slug> (or --no-tracker for a manual story).");
2140
+ info("4) Fill the generated intake.md, then run `squad new-plan --api` to generate the plan.");
2141
+ info(
2142
+ " 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`."
2143
+ );
2144
+ return;
2145
+ }
1307
2146
  if (!credentialReady) {
1308
2147
  info("1) Save a planner key: re-run `squad config set planner` and paste the key when prompted.");
1309
2148
  info("2) Verify with `squad doctor` \u2014 all planner checks should turn green.");
@@ -1319,7 +2158,7 @@ function printPlannerNextSteps(credentialReady) {
1319
2158
  }
1320
2159
 
1321
2160
  // src/commands/config/set-tracker.ts
1322
- import fs11 from "fs";
2161
+ import fs14 from "fs";
1323
2162
  import { select as select2 } from "@inquirer/prompts";
1324
2163
 
1325
2164
  // src/tracker/types.ts
@@ -1335,13 +2174,13 @@ var TrackerError = class extends Error {
1335
2174
  };
1336
2175
 
1337
2176
  // src/tracker/attachments.ts
1338
- import fs10 from "fs";
1339
- import path5 from "path";
2177
+ import fs13 from "fs";
2178
+ import path6 from "path";
1340
2179
  var DEFAULT_MAX_MB = 10;
1341
2180
  async function downloadAttachmentsWith(refs, targetDir, authHeaders, opts = {}) {
1342
2181
  const maxMb = opts.maxMegabytes ?? DEFAULT_MAX_MB;
1343
2182
  const maxBytes = maxMb * 1024 * 1024;
1344
- fs10.mkdirSync(targetDir, { recursive: true });
2183
+ fs13.mkdirSync(targetDir, { recursive: true });
1345
2184
  const out = [];
1346
2185
  for (const ref of refs) {
1347
2186
  out.push(await one(ref, targetDir, authHeaders, maxBytes, maxMb));
@@ -1387,7 +2226,7 @@ async function one(ref, targetDir, headers, maxBytes, maxMb) {
1387
2226
  skipReason: `${formatMb(buf.byteLength)} after download exceeds cap (${maxMb} MB)`
1388
2227
  };
1389
2228
  }
1390
- fs10.writeFileSync(dest, buf);
2229
+ fs13.writeFileSync(dest, buf);
1391
2230
  return {
1392
2231
  source: ref,
1393
2232
  outcome: "written",
@@ -1397,19 +2236,19 @@ async function one(ref, targetDir, headers, maxBytes, maxMb) {
1397
2236
  }
1398
2237
  function sanitizeFilename(raw) {
1399
2238
  const normalized = raw.replace(/\\/g, "/");
1400
- const base = path5.basename(normalized).replace(/[\\/]/g, "_");
2239
+ const base = path6.basename(normalized).replace(/[\\/]/g, "_");
1401
2240
  const cleaned = base.replace(/[\x00-\x1f]/g, "").replace(/^\.+/, "").replace(/\s+/g, " ").trim();
1402
2241
  if (cleaned.length === 0) return "attachment";
1403
2242
  return cleaned.slice(0, 180);
1404
2243
  }
1405
2244
  function uniqueDestination(dir, name) {
1406
- let candidate = path5.join(dir, name);
1407
- if (!fs10.existsSync(candidate)) return candidate;
1408
- const ext = path5.extname(name);
2245
+ let candidate = path6.join(dir, name);
2246
+ if (!fs13.existsSync(candidate)) return candidate;
2247
+ const ext = path6.extname(name);
1409
2248
  const stem = name.slice(0, name.length - ext.length);
1410
2249
  let n = 1;
1411
- while (fs10.existsSync(candidate) && n < 1e3) {
1412
- candidate = path5.join(dir, `${stem} (${n})${ext}`);
2250
+ while (fs13.existsSync(candidate) && n < 1e3) {
2251
+ candidate = path6.join(dir, `${stem} (${n})${ext}`);
1413
2252
  n += 1;
1414
2253
  }
1415
2254
  return candidate;
@@ -2023,7 +2862,7 @@ async function runConfigSetTracker(opts = {}) {
2023
2862
  const root = requireSquadRoot();
2024
2863
  const paths = buildPaths(root);
2025
2864
  const config2 = loadConfig(paths.configFile);
2026
- const baseSecrets = fs11.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
2865
+ const baseSecrets = fs14.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
2027
2866
  const useYes = Boolean(opts.yes);
2028
2867
  const interactive = !useYes && isInteractive();
2029
2868
  let type;
@@ -2146,7 +2985,7 @@ async function runConfigSetTracker(opts = {}) {
2146
2985
  return;
2147
2986
  }
2148
2987
  const reloaded = loadConfig(paths.configFile);
2149
- const s = fs11.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
2988
+ const s = fs14.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
2150
2989
  if (reloaded.tracker.type === "jira") {
2151
2990
  const r = await probeJiraConnectivity(s, reloaded);
2152
2991
  if (r.ok) {
@@ -2193,8 +3032,8 @@ function printTrackerNextSteps(type) {
2193
3032
  }
2194
3033
 
2195
3034
  // src/commands/config/unset-tracker.ts
2196
- import fs12 from "fs";
2197
- import { confirm as confirm3 } from "@inquirer/prompts";
3035
+ import fs15 from "fs";
3036
+ import { confirm as confirm4 } from "@inquirer/prompts";
2198
3037
  async function runConfigUnsetTracker(opts = {}) {
2199
3038
  const root = requireSquadRoot();
2200
3039
  const paths = buildPaths(root);
@@ -2205,7 +3044,7 @@ async function runConfigUnsetTracker(opts = {}) {
2205
3044
  warning("Pass -y in non-interactive mode to confirm setting the tracker to none.");
2206
3045
  return;
2207
3046
  }
2208
- const ok = await confirm3({ message: "Set tracker to none? (keeps tracker tokens in .squad/secrets.yaml.)", default: false });
3047
+ const ok = await confirm4({ message: "Set tracker to none? (keeps tracker tokens in .squad/secrets.yaml.)", default: false });
2209
3048
  if (!ok) {
2210
3049
  return;
2211
3050
  }
@@ -2213,7 +3052,7 @@ async function runConfigUnsetTracker(opts = {}) {
2213
3052
  const next = { ...config2, tracker: { type: "none" } };
2214
3053
  saveConfig(paths.configFile, next);
2215
3054
  if (opts.removeCredentials) {
2216
- const s = fs12.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
3055
+ const s = fs15.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
2217
3056
  if (s.tracker) {
2218
3057
  delete s.tracker;
2219
3058
  saveSecrets(paths.secretsFile, s);
@@ -2226,8 +3065,8 @@ async function runConfigUnsetTracker(opts = {}) {
2226
3065
  }
2227
3066
 
2228
3067
  // src/commands/config/remove-credential.ts
2229
- import fs13 from "fs";
2230
- import { confirm as confirm4 } from "@inquirer/prompts";
3068
+ import fs16 from "fs";
3069
+ import { confirm as confirm5 } from "@inquirer/prompts";
2231
3070
  async function runConfigRemoveCredential(section, opts = {}) {
2232
3071
  if (section !== "planner" && section !== "tracker") {
2233
3072
  throw new Error(
@@ -2242,7 +3081,7 @@ async function runConfigRemoveCredential(section, opts = {}) {
2242
3081
  warning("Pass -y in non-interactive mode to confirm, or run `squad config remove-credential` in a TTY.");
2243
3082
  return;
2244
3083
  }
2245
- const ok = await confirm4({
3084
+ const ok = await confirm5({
2246
3085
  message: `Delete all ${section} credentials from .squad/secrets.yaml? (config is unchanged)`,
2247
3086
  default: false
2248
3087
  });
@@ -2250,7 +3089,7 @@ async function runConfigRemoveCredential(section, opts = {}) {
2250
3089
  return;
2251
3090
  }
2252
3091
  }
2253
- const s = fs13.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
3092
+ const s = fs16.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
2254
3093
  if (section === "planner") {
2255
3094
  if (s.planner) {
2256
3095
  delete s.planner;
@@ -2268,10 +3107,10 @@ async function runConfigRemoveCredential(section, opts = {}) {
2268
3107
  // src/commands/init.ts
2269
3108
  var SUPPORTED_AGENTS = ["claude-code", "cursor", "copilot", "gemini"];
2270
3109
  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" }
3110
+ "claude-code": { subdir: path7.join(".claude", "commands"), srcDir: "claude-code" },
3111
+ cursor: { subdir: path7.join(".cursor", "commands"), srcDir: "cursor" },
3112
+ copilot: { subdir: path7.join(".github", "prompts"), srcDir: "copilot" },
3113
+ gemini: { subdir: path7.join(".gemini", "commands"), srcDir: "gemini" }
2275
3114
  };
2276
3115
  async function runInit(opts) {
2277
3116
  const root = process.cwd();
@@ -2279,7 +3118,7 @@ async function runInit(opts) {
2279
3118
  if (!opts.yes) {
2280
3119
  banner();
2281
3120
  }
2282
- if (fs14.existsSync(paths.configFile) && !opts.force) {
3121
+ if (fs17.existsSync(paths.configFile) && !opts.force) {
2283
3122
  if (!isInteractive() || opts.yes) {
2284
3123
  warning(
2285
3124
  `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 +3145,7 @@ async function runInit(opts) {
2306
3145
  }
2307
3146
  }
2308
3147
  const defaults = {
2309
- name: opts.name ?? path6.basename(root),
3148
+ name: opts.name ?? path7.basename(root),
2310
3149
  language: opts.language ?? "typescript",
2311
3150
  tracker: opts.tracker ?? "none",
2312
3151
  agents: parseAgentsFlag(opts.agents)
@@ -2382,11 +3221,11 @@ async function runInit(opts) {
2382
3221
  };
2383
3222
  }
2384
3223
  } else {
2385
- const plannerEnabled = await confirm5({
3224
+ const plannerEnabled2 = await confirm6({
2386
3225
  message: "Enable automatic plan generation? (press Enter to skip \u2014 you can always edit .squad/config.yaml later)",
2387
3226
  default: false
2388
3227
  });
2389
- if (plannerEnabled) {
3228
+ if (plannerEnabled2) {
2390
3229
  const provider = await select3({
2391
3230
  message: "Planner provider:",
2392
3231
  choices: [
@@ -2411,29 +3250,60 @@ async function runInit(opts) {
2411
3250
  }
2412
3251
  }
2413
3252
  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?`,
3253
+ let needsApiKey = true;
3254
+ if (plannerBlock.provider === "anthropic") {
3255
+ const authMode = await select3({
3256
+ message: "How should squad-kit authenticate with Anthropic?",
2421
3257
  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
- ]
3258
+ {
3259
+ name: "Use my Claude subscription (browser login) \u2014 recommended, no API key needed",
3260
+ value: "subscription"
3261
+ },
3262
+ { name: "Use an Anthropic API key", value: "api-key" },
3263
+ {
3264
+ name: "Decide automatically (subscription if signed in, otherwise API key)",
3265
+ value: "auto"
3266
+ }
3267
+ ],
3268
+ default: "subscription"
2425
3269
  });
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");
3270
+ plannerBlock = { ...plannerBlock, auth: { anthropic: authMode } };
3271
+ needsApiKey = authMode === "api-key";
3272
+ if (authMode === "subscription") {
3273
+ info("No API key needed. Run `squad auth login` after init to sign in with your Claude account.");
3274
+ info("Already signed in through Claude Code (`claude` then `/login`)? squad-kit picks that up automatically.");
3275
+ } else if (authMode === "auto") {
3276
+ info(
3277
+ "`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)."
3278
+ );
3279
+ info("Run `squad auth login` for the subscription, or `squad config set planner` to save a key.");
3280
+ }
3281
+ }
3282
+ if (needsApiKey) {
3283
+ const envVar = providerEnvVar(plannerBlock.provider);
3284
+ const envPresent = Boolean(process.env[envVar]);
3285
+ if (envPresent) {
3286
+ info(`Planner key detected in ${envVar}; keeping env-var resolution as the primary source.`);
2435
3287
  } else {
2436
- info(`Remember to export ${envVar} before running \`squad new-plan\`.`);
3288
+ const storeKey = await select3({
3289
+ message: `No ${envVar} in your environment. How do you want to store your planner API key?`,
3290
+ choices: [
3291
+ { name: `Enter it now and save to .squad/secrets.yaml (git-ignored)`, value: "secrets" },
3292
+ { name: `I'll export ${envVar} in my shell profile myself`, value: "env" }
3293
+ ]
3294
+ });
3295
+ if (storeKey === "secrets") {
3296
+ const key = await password4({
3297
+ message: `${envVar} value (input hidden):`,
3298
+ validate: (v) => v.length >= 20 ? true : "key looks too short"
3299
+ });
3300
+ const base = loadSecrets(paths.secretsFile);
3301
+ const merged = mergePlannerKeyIntoSecrets(base, plannerBlock.provider, key);
3302
+ saveSecrets(paths.secretsFile, merged);
3303
+ success("Planner key saved to .squad/secrets.yaml");
3304
+ } else {
3305
+ info(`Remember to export ${envVar} before running \`squad new-plan\`.`);
3306
+ }
2437
3307
  }
2438
3308
  }
2439
3309
  }
@@ -2450,12 +3320,12 @@ async function runInit(opts) {
2450
3320
  agents: answers.agents,
2451
3321
  planner: plannerBlock
2452
3322
  };
2453
- fs14.mkdirSync(paths.squadDir, { recursive: true });
2454
- fs14.mkdirSync(paths.storiesDir, { recursive: true });
2455
- fs14.mkdirSync(paths.plansDir, { recursive: true });
3323
+ fs17.mkdirSync(paths.squadDir, { recursive: true });
3324
+ fs17.mkdirSync(paths.storiesDir, { recursive: true });
3325
+ fs17.mkdirSync(paths.plansDir, { recursive: true });
2456
3326
  writeFileSafe(
2457
3327
  paths.indexFile,
2458
- readFile(path6.join(templatesDir(), "index.md")),
3328
+ readFile(path7.join(templatesDir(), "index.md")),
2459
3329
  !!opts.force
2460
3330
  );
2461
3331
  saveConfig(paths.configFile, config2);
@@ -2463,7 +3333,7 @@ async function runInit(opts) {
2463
3333
  info("Updated .gitignore with squad-managed patterns (e.g. .squad/secrets.yaml).");
2464
3334
  }
2465
3335
  writeFileSafe(
2466
- path6.join(paths.squadDir, "README.md"),
3336
+ path7.join(paths.squadDir, "README.md"),
2467
3337
  renderSquadReadme(config2),
2468
3338
  !!opts.force
2469
3339
  );
@@ -2480,8 +3350,13 @@ async function runInit(opts) {
2480
3350
  const model = modelFor(plannerBlock.provider, "plan", plannerBlock.modelOverride);
2481
3351
  const overrideNote = plannerBlock.modelOverride?.[plannerBlock.provider] ? " (override)" : "";
2482
3352
  kv("planner", `${plannerBlock.provider}/${model}${overrideNote}`);
2483
- kv("key", hasKey ? `\u2713 (${envVar} or .squad/secrets.yaml)` : `missing (set ${envVar})`);
2484
- if (!hasKey) {
3353
+ const subscriptionMode = plannerBlock.auth?.anthropic === "subscription" && plannerBlock.provider === "anthropic";
3354
+ if (subscriptionMode) {
3355
+ kv("auth", "subscription (Claude login) \u2014 run `squad auth login` if you have not signed in");
3356
+ } else {
3357
+ kv("key", hasKey ? `\u2713 (${envVar} or .squad/secrets.yaml)` : `missing (set ${envVar})`);
3358
+ }
3359
+ if (!hasKey && !subscriptionMode) {
2485
3360
  blank();
2486
3361
  warning("Planner key not found in environment or .squad/secrets.yaml.");
2487
3362
  info("Set one of these before running `squad new-plan`:");
@@ -2503,13 +3378,13 @@ function parseAgentsFlag(flag) {
2503
3378
  }
2504
3379
  async function confirmSafe(message, defaultValue, nonInteractive) {
2505
3380
  if (nonInteractive) return defaultValue;
2506
- return confirm5({ message, default: defaultValue });
3381
+ return confirm6({ message, default: defaultValue });
2507
3382
  }
2508
3383
  function installAgent(root, agent, overwrite) {
2509
3384
  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;
3385
+ const src = path7.join(templatesDir(), "agents", config2.srcDir);
3386
+ const dest = path7.join(root, config2.subdir);
3387
+ if (!fs17.existsSync(src)) return;
2513
3388
  copyTree(src, dest, overwrite);
2514
3389
  }
2515
3390
  function renderSquadReadme(config2) {
@@ -2535,22 +3410,22 @@ Plan meta-prompts (\`generate-plan.md\`, \`story-skeleton.md\`) ship inside the
2535
3410
  }
2536
3411
 
2537
3412
  // src/commands/new-story.ts
2538
- import fs20 from "fs";
2539
- import path12 from "path";
3413
+ import fs23 from "fs";
3414
+ import path13 from "path";
2540
3415
  import { input as input3, select as select5 } from "@inquirer/prompts";
2541
3416
 
2542
3417
  // src/core/stories.ts
2543
- import fs15 from "fs";
2544
- import path7 from "path";
3418
+ import fs18 from "fs";
3419
+ import path8 from "path";
2545
3420
  var STORY_FILE = /^(\d{2,})-story-.+\.md$/;
2546
3421
  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();
3422
+ if (!fs18.existsSync(paths.storiesDir)) return [];
3423
+ return fs18.readdirSync(paths.storiesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
2549
3424
  }
2550
3425
  function listPlanFeatureNames(paths) {
2551
3426
  const names = new Set(listFeatures(paths));
2552
- if (fs15.existsSync(paths.plansDir)) {
2553
- for (const e of fs15.readdirSync(paths.plansDir, { withFileTypes: true })) {
3427
+ if (fs18.existsSync(paths.plansDir)) {
3428
+ for (const e of fs18.readdirSync(paths.plansDir, { withFileTypes: true })) {
2554
3429
  if (e.isDirectory()) names.add(e.name);
2555
3430
  }
2556
3431
  }
@@ -2561,16 +3436,16 @@ function listAllStories(paths) {
2561
3436
  }
2562
3437
  function listStories(paths, opts = {}) {
2563
3438
  const rows = [];
2564
- if (!fs15.existsSync(paths.storiesDir)) return rows;
2565
- for (const feature of fs15.readdirSync(paths.storiesDir, { withFileTypes: true })) {
3439
+ if (!fs18.existsSync(paths.storiesDir)) return rows;
3440
+ for (const feature of fs18.readdirSync(paths.storiesDir, { withFileTypes: true })) {
2566
3441
  if (!feature.isDirectory()) continue;
2567
3442
  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 })) {
3443
+ const featureDir = path8.join(paths.storiesDir, feature.name);
3444
+ for (const story of fs18.readdirSync(featureDir, { withFileTypes: true })) {
2570
3445
  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;
3446
+ const storyDir = path8.join(featureDir, story.name);
3447
+ const intakePath = path8.join(storyDir, "intake.md");
3448
+ if (!fs18.existsSync(intakePath)) continue;
2574
3449
  rows.push({
2575
3450
  feature: feature.name,
2576
3451
  id: story.name,
@@ -2584,10 +3459,10 @@ function listStories(paths, opts = {}) {
2584
3459
  return rows;
2585
3460
  }
2586
3461
  function findPlanFor(plansDir, feature, storyId) {
2587
- const featureDir = path7.join(plansDir, feature);
2588
- if (!fs15.existsSync(featureDir)) return void 0;
3462
+ const featureDir = path8.join(plansDir, feature);
3463
+ if (!fs18.existsSync(featureDir)) return void 0;
2589
3464
  const candidates = [];
2590
- for (const entry of fs15.readdirSync(featureDir)) {
3465
+ for (const entry of fs18.readdirSync(featureDir)) {
2591
3466
  if (!STORY_FILE.test(entry)) continue;
2592
3467
  if (entry.includes(storyId)) candidates.push(entry);
2593
3468
  }
@@ -2600,16 +3475,16 @@ function findStoryByIntake(records, intakePath) {
2600
3475
  return records.find((r) => normalizeIntakePath(r.intakePath) === target);
2601
3476
  }
2602
3477
  function normalizeIntakePath(p) {
2603
- const resolved = path7.resolve(p);
3478
+ const resolved = path8.resolve(p);
2604
3479
  try {
2605
- return fs15.realpathSync(resolved);
3480
+ return fs18.realpathSync(resolved);
2606
3481
  } catch {
2607
3482
  return resolved;
2608
3483
  }
2609
3484
  }
2610
3485
  function readTitleHint(intakePath) {
2611
3486
  try {
2612
- const raw = fs15.readFileSync(intakePath, "utf8");
3487
+ const raw = fs18.readFileSync(intakePath, "utf8");
2613
3488
  const head = raw.split("\n").slice(0, 30).join("\n");
2614
3489
  const hintLine = head.match(/>\s*\*\*Title hint \(from CLI\):\*\*\s*(.+?)\s*$/m);
2615
3490
  const fromHint = hintLine?.[1]?.trim();
@@ -2646,7 +3521,7 @@ function parsePlanMetadataLine(line2) {
2646
3521
  }
2647
3522
  function readPlanMetadata(planFileAbsPath) {
2648
3523
  try {
2649
- const raw = fs15.readFileSync(planFileAbsPath, "utf8").replace(/^\uFEFF/, "");
3524
+ const raw = fs18.readFileSync(planFileAbsPath, "utf8").replace(/^\uFEFF/, "");
2650
3525
  const lines = raw.split(/\n/);
2651
3526
  const scan = Math.min(lines.length, 40);
2652
3527
  for (let i = 0; i < scan; i++) {
@@ -2678,8 +3553,8 @@ function trackerIdForFilename(type, id) {
2678
3553
  }
2679
3554
 
2680
3555
  // src/core/story-intake-helpers.ts
2681
- import fs16 from "fs";
2682
- import path8 from "path";
3556
+ import fs19 from "fs";
3557
+ import path9 from "path";
2683
3558
  function buildIntakeTemplateVars(args) {
2684
3559
  const e = escapeTemplateValue;
2685
3560
  const { config: config2, featureSlug, storyFolderName, trackerId, title, fetchedIssue } = args;
@@ -2735,7 +3610,7 @@ function renderAttachmentsTable(downloads) {
2735
3610
  const size = `${sizeNum | 0} KB`;
2736
3611
  switch (d.outcome) {
2737
3612
  case "written":
2738
- return `| \`attachments/${path8.basename(d.absolutePath)}\` | ${size} | downloaded |`;
3613
+ return `| \`attachments/${path9.basename(d.absolutePath)}\` | ${size} | downloaded |`;
2739
3614
  case "skipped-oversize":
2740
3615
  return `| ${d.source.filename} | ${size} | **skipped** \u2014 ${d.skipReason} |`;
2741
3616
  case "skipped-error":
@@ -2764,34 +3639,34 @@ function buildSkippedTrackerPreamble(trackerType) {
2764
3639
  ].join("\n");
2765
3640
  }
2766
3641
  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"));
3642
+ fs19.mkdirSync(featurePlanDir, { recursive: true });
3643
+ const overviewFile = path9.join(featurePlanDir, "00-overview.md");
3644
+ if (fs19.existsSync(overviewFile)) return;
3645
+ const template = readFile(path9.join(templatesDir(), "overview.md"));
2771
3646
  writeFileSafe(overviewFile, render(template, { featureSlug: slug }), false);
2772
3647
  }
2773
3648
 
2774
3649
  // src/core/story-mutations.ts
2775
- import fs19 from "fs";
2776
- import path11 from "path";
3650
+ import fs22 from "fs";
3651
+ import path12 from "path";
2777
3652
 
2778
3653
  // src/commands/rm/shared.ts
2779
- import fs18 from "fs";
2780
- import path10 from "path";
3654
+ import fs21 from "fs";
3655
+ import path11 from "path";
2781
3656
  import { select as select4 } from "@inquirer/prompts";
2782
3657
 
2783
3658
  // src/core/sequence.ts
2784
- import fs17 from "fs";
2785
- import path9 from "path";
3659
+ import fs20 from "fs";
3660
+ import path10 from "path";
2786
3661
  var STORY_FILE2 = /^(\d{2,})-story-.+\.md$/;
2787
3662
  function scanPlans(plansDir) {
2788
3663
  const used = [];
2789
3664
  const seen = /* @__PURE__ */ new Map();
2790
- if (fs17.existsSync(plansDir)) {
2791
- for (const feature of fs17.readdirSync(plansDir, { withFileTypes: true })) {
3665
+ if (fs20.existsSync(plansDir)) {
3666
+ for (const feature of fs20.readdirSync(plansDir, { withFileTypes: true })) {
2792
3667
  if (!feature.isDirectory()) continue;
2793
- const featureDir = path9.join(plansDir, feature.name);
2794
- for (const entry of fs17.readdirSync(featureDir)) {
3668
+ const featureDir = path10.join(plansDir, feature.name);
3669
+ for (const entry of fs20.readdirSync(featureDir)) {
2795
3670
  const match = entry.match(STORY_FILE2);
2796
3671
  if (!match) continue;
2797
3672
  const n = parseInt(match[1], 10);
@@ -2815,9 +3690,9 @@ function canPromptForRm(opts) {
2815
3690
  return isInteractive() && Boolean(process.stdin.isTTY);
2816
3691
  }
2817
3692
  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");
3693
+ const overview = path11.join(featurePlanDir, "00-overview.md");
3694
+ if (!fs21.existsSync(overview)) return;
3695
+ const src = fs21.readFileSync(overview, "utf8");
2821
3696
  const lines = src.split("\n");
2822
3697
  const filtered = lines.filter((line2) => {
2823
3698
  if (!line2.startsWith("|")) return true;
@@ -2825,12 +3700,12 @@ function removeOverviewRow(featurePlanDir, storyId) {
2825
3700
  if (/^\|\s*-+/.test(line2)) return true;
2826
3701
  return !line2.includes(storyId);
2827
3702
  });
2828
- fs18.writeFileSync(overview, filtered.join("\n"), "utf8");
3703
+ fs21.writeFileSync(overview, filtered.join("\n"), "utf8");
2829
3704
  }
2830
3705
  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");
3706
+ const overview = path11.join(featurePlanDir, "00-overview.md");
3707
+ if (!fs21.existsSync(overview)) return;
3708
+ const src = fs21.readFileSync(overview, "utf8");
2834
3709
  const token = `\`${planBasename}\``;
2835
3710
  const lines = src.split("\n");
2836
3711
  const filtered = lines.filter((line2) => {
@@ -2839,25 +3714,25 @@ function removeOverviewRowForPlanFile(featurePlanDir, planBasename) {
2839
3714
  if (/^\|\s*-+/.test(line2)) return true;
2840
3715
  return !line2.includes(token);
2841
3716
  });
2842
- fs18.writeFileSync(overview, filtered.join("\n"), "utf8");
3717
+ fs21.writeFileSync(overview, filtered.join("\n"), "utf8");
2843
3718
  }
2844
3719
  function trashOrDelete(absPaths, trashRoot, useTrash) {
2845
3720
  if (!useTrash) {
2846
- for (const p of absPaths) fs18.rmSync(p, { recursive: true, force: true });
3721
+ for (const p of absPaths) fs21.rmSync(p, { recursive: true, force: true });
2847
3722
  return { removed: absPaths };
2848
3723
  }
2849
3724
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2850
- const bucket = path10.join(trashRoot, ts);
2851
- fs18.mkdirSync(bucket, { recursive: true });
3725
+ const bucket = path11.join(trashRoot, ts);
3726
+ fs21.mkdirSync(bucket, { recursive: true });
2852
3727
  for (const src of absPaths) {
2853
- const rel = path10.basename(src);
2854
- let dst = path10.join(bucket, rel);
3728
+ const rel = path11.basename(src);
3729
+ let dst = path11.join(bucket, rel);
2855
3730
  let n = 0;
2856
- while (fs18.existsSync(dst)) {
3731
+ while (fs21.existsSync(dst)) {
2857
3732
  n += 1;
2858
- dst = path10.join(bucket, `${n}_${rel}`);
3733
+ dst = path11.join(bucket, `${n}_${rel}`);
2859
3734
  }
2860
- fs18.renameSync(src, dst);
3735
+ fs21.renameSync(src, dst);
2861
3736
  }
2862
3737
  return { removed: absPaths, trashed: bucket };
2863
3738
  }
@@ -2868,21 +3743,21 @@ function renderPreview(kind, targets, opts) {
2868
3743
  kv(t.kind, t.label, 6);
2869
3744
  }
2870
3745
  if (opts.trash) {
2871
- info(`Items will move to ${path10.relative(process.cwd(), opts.trashRoot)}/<timestamp>/`);
3746
+ info(`Items will move to ${path11.relative(process.cwd(), opts.trashRoot)}/<timestamp>/`);
2872
3747
  } else if (!opts.dryRun) {
2873
3748
  warning("This is a permanent delete. Pass --trash to move to .squad/.trash/ instead.");
2874
3749
  }
2875
3750
  }
2876
3751
  function listAllPlanListEntries(paths, featureFilter) {
2877
- if (!fs18.existsSync(paths.plansDir)) return [];
3752
+ if (!fs21.existsSync(paths.plansDir)) return [];
2878
3753
  const out = [];
2879
- for (const ent of fs18.readdirSync(paths.plansDir, { withFileTypes: true })) {
3754
+ for (const ent of fs21.readdirSync(paths.plansDir, { withFileTypes: true })) {
2880
3755
  if (!ent.isDirectory()) continue;
2881
3756
  if (featureFilter && ent.name !== featureFilter) continue;
2882
- const featureDir = path10.join(paths.plansDir, ent.name);
2883
- for (const name of fs18.readdirSync(featureDir)) {
3757
+ const featureDir = path11.join(paths.plansDir, ent.name);
3758
+ for (const name of fs21.readdirSync(featureDir)) {
2884
3759
  if (!STORY_PLAN.test(name)) continue;
2885
- out.push({ feature: ent.name, file: name, abs: path10.join(featureDir, name) });
3760
+ out.push({ feature: ent.name, file: name, abs: path11.join(featureDir, name) });
2886
3761
  }
2887
3762
  }
2888
3763
  return out.sort((a, b) => (a.feature + a.file).localeCompare(b.feature + b.file));
@@ -2933,34 +3808,34 @@ function resolveStoryFromArg(arg, stories, root) {
2933
3808
  if (s.id === t) return s;
2934
3809
  if (`${s.feature}/${s.id}` === t) return s;
2935
3810
  }
2936
- const abs = path10.isAbsolute(t) ? t : path10.resolve(root, t);
2937
- const withIntake = t.endsWith("intake.md") ? abs : path10.join(abs, "intake.md");
3811
+ const abs = path11.isAbsolute(t) ? t : path11.resolve(root, t);
3812
+ const withIntake = t.endsWith("intake.md") ? abs : path11.join(abs, "intake.md");
2938
3813
  return findStoryByIntake(stories, withIntake) ?? findStoryByIntake(stories, abs);
2939
3814
  }
2940
3815
  function isSequenceToken(s) {
2941
3816
  return /^\d{1,4}$/.test(s) && s.length > 0;
2942
3817
  }
2943
3818
  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);
3819
+ if (!fs21.existsSync(absPath) || !STORY_PLAN.test(path11.basename(absPath))) return void 0;
3820
+ const planRoot = fs21.realpathSync(path11.resolve(paths.plansDir));
3821
+ const fileReal = fs21.realpathSync(path11.resolve(absPath));
3822
+ const rel = path11.relative(planRoot, fileReal);
3823
+ if (rel.startsWith("..") || path11.isAbsolute(rel)) return void 0;
3824
+ const segs = rel.split(path11.sep);
2950
3825
  if (segs.length < 2) return void 0;
2951
3826
  const feature = segs[0];
2952
- const file = segs.slice(1).join(path10.sep);
3827
+ const file = segs.slice(1).join(path11.sep);
2953
3828
  if (featureFilter && feature !== featureFilter) return void 0;
2954
- return { feature, file, abs: path10.resolve(absPath) };
3829
+ return { feature, file, abs: path11.resolve(absPath) };
2955
3830
  }
2956
3831
  function resolvePlanFromArg(arg, paths, featureFilter) {
2957
3832
  const t = arg.trim();
2958
3833
  if (!t) return void 0;
2959
- const asPath = path10.isAbsolute(t) ? t : path10.resolve(process.cwd(), t);
3834
+ const asPath = path11.isAbsolute(t) ? t : path11.resolve(process.cwd(), t);
2960
3835
  const a = tryParsePlanFilePath(asPath, paths, featureFilter);
2961
3836
  if (a) return a;
2962
- if (!path10.isAbsolute(t)) {
2963
- const under = tryParsePlanFilePath(path10.join(paths.plansDir, t), paths, featureFilter);
3837
+ if (!path11.isAbsolute(t)) {
3838
+ const under = tryParsePlanFilePath(path11.join(paths.plansDir, t), paths, featureFilter);
2964
3839
  if (under) return under;
2965
3840
  }
2966
3841
  if (isSequenceToken(t)) {
@@ -2990,24 +3865,24 @@ function countOverviewDataRows(overviewContent) {
2990
3865
  return n;
2991
3866
  }
2992
3867
  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;
3868
+ const d = path11.join(paths.storiesDir, feature);
3869
+ if (!fs21.existsSync(d)) return true;
3870
+ return fs21.readdirSync(d).length === 0;
2996
3871
  }
2997
3872
 
2998
3873
  // src/core/story-mutations.ts
2999
3874
  function writeNewStoryToDisk(input5) {
3000
3875
  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);
3876
+ const storyDir = path12.join(paths.storiesDir, featureSlug, storyFolderName);
3877
+ const intakeFile = path12.join(storyDir, "intake.md");
3878
+ const attachmentsDir = path12.join(storyDir, "attachments");
3879
+ if (fs22.existsSync(intakeFile)) {
3880
+ const rel = path12.relative(paths.root, intakeFile);
3006
3881
  throw new Error(
3007
3882
  `Intake already exists: ${intakeFile}. Run \`squad new-plan ${rel}\` to plan it, or \`squad rm story\` to remove the story first.`
3008
3883
  );
3009
3884
  }
3010
- fs19.mkdirSync(attachmentsDir, { recursive: true });
3885
+ fs22.mkdirSync(attachmentsDir, { recursive: true });
3011
3886
  const template = readBundledPrompt("intake.md");
3012
3887
  const rendered = render(
3013
3888
  template,
@@ -3022,7 +3897,7 @@ function writeNewStoryToDisk(input5) {
3022
3897
  );
3023
3898
  const skippedPreamble = noTracker && config2.tracker.type !== "none" && !sourceBlock ? buildSkippedTrackerPreamble(config2.tracker.type) : "";
3024
3899
  writeFileSafe(intakeFile, skippedPreamble + sourceBlock + rendered, false);
3025
- ensureFeatureOverview(path11.join(paths.plansDir, featureSlug), featureSlug);
3900
+ ensureFeatureOverview(path12.join(paths.plansDir, featureSlug), featureSlug);
3026
3901
  return { storyDir, intakePath: intakeFile, feature: featureSlug, id: storyFolderName };
3027
3902
  }
3028
3903
  function createStoryRecord(input5) {
@@ -3071,16 +3946,16 @@ async function createStoryRecordFromIssue(input5) {
3071
3946
  }
3072
3947
  const title = input5.issue.title;
3073
3948
  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);
3949
+ const storyDir = path12.join(input5.paths.storiesDir, feature, storyFolderName);
3950
+ const intakeFile = path12.join(storyDir, "intake.md");
3951
+ const attachmentsDir = path12.join(storyDir, "attachments");
3952
+ if (fs22.existsSync(intakeFile)) {
3953
+ const rel = path12.relative(input5.paths.root, intakeFile);
3079
3954
  throw new Error(
3080
3955
  `Intake already exists: ${intakeFile}. Run \`squad new-plan ${rel}\` to plan it, or \`squad rm story\` to remove the story first.`
3081
3956
  );
3082
3957
  }
3083
- fs19.mkdirSync(attachmentsDir, { recursive: true });
3958
+ fs22.mkdirSync(attachmentsDir, { recursive: true });
3084
3959
  let downloads = [];
3085
3960
  if (input5.withAttachments && input5.issue.attachments.length > 0) {
3086
3961
  downloads = await input5.client.downloadAttachments(input5.issue.attachments, attachmentsDir);
@@ -3108,10 +3983,10 @@ async function createStoryRecordFromIssue(input5) {
3108
3983
  function deleteStoryRecord(input5) {
3109
3984
  const targets = [input5.story.storyDir];
3110
3985
  if (input5.story.planFile) {
3111
- targets.push(path11.join(input5.paths.plansDir, input5.story.feature, input5.story.planFile));
3986
+ targets.push(path12.join(input5.paths.plansDir, input5.story.feature, input5.story.planFile));
3112
3987
  }
3113
3988
  const result = trashOrDelete(targets, input5.paths.trashDir, input5.trash);
3114
- removeOverviewRow(path11.join(input5.paths.plansDir, input5.story.feature), input5.story.id);
3989
+ removeOverviewRow(path12.join(input5.paths.plansDir, input5.story.feature), input5.story.id);
3115
3990
  return result;
3116
3991
  }
3117
3992
 
@@ -3221,15 +4096,15 @@ async function runNewStory(featureSlug, opts) {
3221
4096
  '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
4097
  );
3223
4098
  }
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)) {
4099
+ const storyDir = path13.join(paths.storiesDir, slug, storyFolderName);
4100
+ const intakeFile = path13.join(storyDir, "intake.md");
4101
+ const attachmentsDir = path13.join(storyDir, "attachments");
4102
+ if (fs23.existsSync(intakeFile)) {
3228
4103
  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.`
4104
+ `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
4105
  );
3231
4106
  }
3232
- fs20.mkdirSync(attachmentsDir, { recursive: true });
4107
+ fs23.mkdirSync(attachmentsDir, { recursive: true });
3233
4108
  let sourceBlock = "";
3234
4109
  let fetchedSummary;
3235
4110
  let fetchedIssue;
@@ -3259,14 +4134,14 @@ async function runNewStory(featureSlug, opts) {
3259
4134
  sourceBlock,
3260
4135
  fetchedIssue
3261
4136
  });
3262
- const relativePath = path12.relative(root, intakeFile);
3263
- const relAttachDir = path12.relative(root, attachmentsDir);
4137
+ const relativePath = path13.relative(root, intakeFile);
4138
+ const relAttachDir = path13.relative(root, attachmentsDir);
3264
4139
  success("Created intake");
3265
4140
  if (noTracker && config2.tracker.type !== "none") {
3266
4141
  warning("Created without a tracker link. Use `squad tracker link` later to attach one.");
3267
4142
  }
3268
4143
  kv("intake", relativePath, 11);
3269
- kv("attachments", relAttachDir + path12.sep, 11);
4144
+ kv("attachments", relAttachDir + path13.sep, 11);
3270
4145
  if (fetchedSummary && fetchedSummary.written > 0) {
3271
4146
  kv("fetched", `${fetchedSummary.id} \xB7 ${fetchedSummary.written} attachments saved`, 11);
3272
4147
  }
@@ -3275,7 +4150,7 @@ async function runNewStory(featureSlug, opts) {
3275
4150
  info(`squad new-plan ${relativePath}`);
3276
4151
  }
3277
4152
  async function tryFetchIssue(config2, id, opts, paths, attachmentsDir) {
3278
- const secretsFromFile = fs20.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
4153
+ const secretsFromFile = fs23.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
3279
4154
  const secrets = overlayTrackerEnv(secretsFromFile);
3280
4155
  const resolution = clientFor(config2, secrets);
3281
4156
  if (resolution.error) {
@@ -3332,12 +4207,12 @@ function truncate(s, n) {
3332
4207
  }
3333
4208
 
3334
4209
  // 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";
4210
+ import fs35 from "fs";
4211
+ import path25 from "path";
4212
+ import { confirm as confirm7, select as select6 } from "@inquirer/prompts";
3338
4213
 
3339
4214
  // src/utils/clipboard.ts
3340
- import { spawn, spawnSync } from "child_process";
4215
+ import { spawn as spawn2, spawnSync } from "child_process";
3341
4216
  async function copyToClipboard(text) {
3342
4217
  const candidates = clipboardCandidates();
3343
4218
  if (candidates.length === 0) {
@@ -3360,7 +4235,7 @@ function tryCopy(cmd, text) {
3360
4235
  return new Promise((resolve2) => {
3361
4236
  let child;
3362
4237
  try {
3363
- child = spawn(cmd.command, cmd.args, { stdio: ["pipe", "ignore", "pipe"] });
4238
+ child = spawn2(cmd.command, cmd.args, { stdio: ["pipe", "ignore", "pipe"] });
3364
4239
  } catch (err2) {
3365
4240
  resolve2({ ok: false, reason: `${cmd.command} failed to spawn: ${err2.message}` });
3366
4241
  return;
@@ -3425,16 +4300,16 @@ function noCandidateReason() {
3425
4300
  }
3426
4301
 
3427
4302
  // src/planner/loop.ts
3428
- import path20 from "path";
3429
- import fs28 from "fs";
4303
+ import path21 from "path";
4304
+ import fs31 from "fs";
3430
4305
  import { APICallError as APICallError3 } from "ai";
3431
4306
 
3432
4307
  // src/planner/tools/index.ts
3433
4308
  import { randomUUID } from "crypto";
3434
4309
 
3435
4310
  // src/planner/tools/read-file.ts
3436
- import fs21 from "fs";
3437
- import path13 from "path";
4311
+ import fs24 from "fs";
4312
+ import path14 from "path";
3438
4313
  import { tool } from "ai";
3439
4314
  import { z } from "zod";
3440
4315
  var MAX_BYTES_PER_FULL_READ = 32e3;
@@ -3469,15 +4344,15 @@ function readFileTool(root, budget, input5) {
3469
4344
  if (offset === void 0 !== (limit === void 0)) {
3470
4345
  return err("read_file: pass both offset and limit for a ranged read, or neither for a full read.");
3471
4346
  }
3472
- const resolved = path13.resolve(root, raw);
3473
- const relCheck = path13.relative(root, resolved);
3474
- if (relCheck.startsWith("..") || path13.isAbsolute(relCheck)) {
4347
+ const resolved = path14.resolve(root, raw);
4348
+ const relCheck = path14.relative(root, resolved);
4349
+ if (relCheck.startsWith("..") || path14.isAbsolute(relCheck)) {
3475
4350
  return err(`read_file: path "${raw}" escapes the project root and was refused.`);
3476
4351
  }
3477
- if (!fs21.existsSync(resolved)) {
4352
+ if (!fs24.existsSync(resolved)) {
3478
4353
  return err(`read_file: not found "${raw}".`);
3479
4354
  }
3480
- const stat = fs21.statSync(resolved);
4355
+ const stat = fs24.statSync(resolved);
3481
4356
  if (stat.isDirectory()) {
3482
4357
  return err(`read_file: "${raw}" is a directory. Ask for a specific file path.`);
3483
4358
  }
@@ -3495,7 +4370,7 @@ function readFileTool(root, budget, input5) {
3495
4370
  }
3496
4371
  let buf;
3497
4372
  try {
3498
- buf = fs21.readFileSync(resolved);
4373
+ buf = fs24.readFileSync(resolved);
3499
4374
  } catch (e) {
3500
4375
  return err(`read_file: failed to read "${raw}" (${e.message}).`);
3501
4376
  }
@@ -3518,7 +4393,7 @@ function readFileRanged(resolved, rawRel, budget, fileSize, offset, limit) {
3518
4393
  }
3519
4394
  let data;
3520
4395
  try {
3521
- data = fs21.readFileSync(resolved);
4396
+ data = fs24.readFileSync(resolved);
3522
4397
  } catch (e) {
3523
4398
  return err(`read_file: failed to read "${rawRel}" (${e.message}).`);
3524
4399
  }
@@ -3574,14 +4449,14 @@ function looksBinary(buf) {
3574
4449
  }
3575
4450
 
3576
4451
  // src/planner/tools/grep.ts
3577
- import fs23 from "fs";
3578
- import path15 from "path";
4452
+ import fs26 from "fs";
4453
+ import path16 from "path";
3579
4454
  import { tool as tool2 } from "ai";
3580
4455
  import { z as z2 } from "zod";
3581
4456
 
3582
4457
  // src/core/repo-map.ts
3583
- import fs22 from "fs";
3584
- import path14 from "path";
4458
+ import fs25 from "fs";
4459
+ import path15 from "path";
3585
4460
  import ignore from "ignore";
3586
4461
  var DEFAULT_IGNORES = [
3587
4462
  "node_modules",
@@ -3597,10 +4472,10 @@ var DEFAULT_IGNORES = [
3597
4472
  ];
3598
4473
  function buildRepoIgnore(root) {
3599
4474
  const ig = ignore().add(DEFAULT_IGNORES);
3600
- const gitignore = path14.join(root, ".gitignore");
3601
- if (fs22.existsSync(gitignore)) {
4475
+ const gitignore = path15.join(root, ".gitignore");
4476
+ if (fs25.existsSync(gitignore)) {
3602
4477
  try {
3603
- ig.add(fs22.readFileSync(gitignore, "utf8"));
4478
+ ig.add(fs25.readFileSync(gitignore, "utf8"));
3604
4479
  } catch {
3605
4480
  }
3606
4481
  }
@@ -3656,7 +4531,7 @@ function renderDirTree(name, tree, depth) {
3656
4531
  function renderRepoTree(entries) {
3657
4532
  const root = { files: [], dirs: /* @__PURE__ */ new Map() };
3658
4533
  for (const e of entries) {
3659
- const parts = e.rel.split(path14.sep).filter(Boolean);
4534
+ const parts = e.rel.split(path15.sep).filter(Boolean);
3660
4535
  if (parts.length === 0) continue;
3661
4536
  insertTree(root, parts, e.size);
3662
4537
  }
@@ -3666,26 +4541,26 @@ function collectFileEntries(root, dir, ig, out, max, includeHidden) {
3666
4541
  if (out.length >= max) return out;
3667
4542
  let dirents;
3668
4543
  try {
3669
- dirents = fs22.readdirSync(dir, { withFileTypes: true });
4544
+ dirents = fs25.readdirSync(dir, { withFileTypes: true });
3670
4545
  } catch {
3671
4546
  return out;
3672
4547
  }
3673
4548
  for (const entry of dirents) {
3674
4549
  if (out.length >= max) break;
3675
4550
  if (!includeHidden && entry.name.startsWith(".") && entry.name !== ".squad") continue;
3676
- const abs = path14.join(dir, entry.name);
3677
- const rel = path14.relative(root, abs);
4551
+ const abs = path15.join(dir, entry.name);
4552
+ const rel = path15.relative(root, abs);
3678
4553
  if (!rel || ig.ignores(rel) || entry.isDirectory() && ig.ignores(rel + "/")) continue;
3679
4554
  if (entry.isDirectory()) {
3680
4555
  collectFileEntries(root, abs, ig, out, max, includeHidden);
3681
4556
  } else if (entry.isFile()) {
3682
4557
  let size = 0;
3683
4558
  try {
3684
- size = fs22.statSync(abs).size;
4559
+ size = fs25.statSync(abs).size;
3685
4560
  } catch {
3686
4561
  size = 0;
3687
4562
  }
3688
- out.push({ rel: rel.split(path14.sep).join("/"), size });
4563
+ out.push({ rel: rel.split(path15.sep).join("/"), size });
3689
4564
  }
3690
4565
  }
3691
4566
  return out;
@@ -3694,20 +4569,20 @@ function walkFlat(root, dir, ig, out, max, includeHidden) {
3694
4569
  if (out.length >= max) return;
3695
4570
  let entries;
3696
4571
  try {
3697
- entries = fs22.readdirSync(dir, { withFileTypes: true });
4572
+ entries = fs25.readdirSync(dir, { withFileTypes: true });
3698
4573
  } catch {
3699
4574
  return;
3700
4575
  }
3701
4576
  for (const entry of entries) {
3702
4577
  if (out.length >= max) return;
3703
4578
  if (!includeHidden && entry.name.startsWith(".") && entry.name !== ".squad") continue;
3704
- const abs = path14.join(dir, entry.name);
3705
- const rel = path14.relative(root, abs);
4579
+ const abs = path15.join(dir, entry.name);
4580
+ const rel = path15.relative(root, abs);
3706
4581
  if (!rel || ig.ignores(rel) || entry.isDirectory() && ig.ignores(rel + "/")) continue;
3707
4582
  if (entry.isDirectory()) {
3708
4583
  walkFlat(root, abs, ig, out, max, includeHidden);
3709
4584
  } else if (entry.isFile()) {
3710
- out.push(rel.split(path14.sep).join("/"));
4585
+ out.push(rel.split(path15.sep).join("/"));
3711
4586
  }
3712
4587
  }
3713
4588
  }
@@ -3743,12 +4618,12 @@ function runGrep(root, budget, args) {
3743
4618
  return { content: `grep: invalid pattern (${e.message}).`, isError: true };
3744
4619
  }
3745
4620
  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)) {
4621
+ const scopeRoot = args.path?.trim() ? path16.resolve(root, args.path) : root;
4622
+ const relScope = path16.relative(root, scopeRoot);
4623
+ if (relScope.startsWith("..") || path16.isAbsolute(relScope)) {
3749
4624
  return { content: `grep: scope path escapes the project root and was refused.`, isError: true };
3750
4625
  }
3751
- if (!fs23.existsSync(scopeRoot)) {
4626
+ if (!fs26.existsSync(scopeRoot)) {
3752
4627
  return { content: `grep: scope path not found "${args.path}".`, isError: true };
3753
4628
  }
3754
4629
  const matches = [];
@@ -3756,28 +4631,28 @@ function runGrep(root, budget, args) {
3756
4631
  if (matches.length >= MAX_GREP_MATCHES) return;
3757
4632
  let entries;
3758
4633
  try {
3759
- entries = fs23.readdirSync(dir, { withFileTypes: true });
4634
+ entries = fs26.readdirSync(dir, { withFileTypes: true });
3760
4635
  } catch {
3761
4636
  return;
3762
4637
  }
3763
4638
  for (const entry of entries) {
3764
4639
  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("/");
4640
+ const abs = path16.join(dir, entry.name);
4641
+ const rel = path16.relative(root, abs).split(path16.sep).join("/");
3767
4642
  if (!rel || ig.ignores(rel) || entry.isDirectory() && ig.ignores(rel + "/")) continue;
3768
4643
  if (entry.isDirectory()) {
3769
4644
  walk2(abs);
3770
4645
  } else if (entry.isFile()) {
3771
4646
  let st;
3772
4647
  try {
3773
- st = fs23.statSync(abs);
4648
+ st = fs26.statSync(abs);
3774
4649
  } catch {
3775
4650
  continue;
3776
4651
  }
3777
4652
  if (st.size > MAX_GREP_FILE_SIZE_BYTES) continue;
3778
4653
  let buf;
3779
4654
  try {
3780
- buf = fs23.readFileSync(abs);
4655
+ buf = fs26.readFileSync(abs);
3781
4656
  } catch {
3782
4657
  continue;
3783
4658
  }
@@ -3796,14 +4671,14 @@ function runGrep(root, budget, args) {
3796
4671
  }
3797
4672
  }
3798
4673
  };
3799
- if (fs23.statSync(scopeRoot).isFile()) {
3800
- const rel = path15.relative(root, scopeRoot).split(path15.sep).join("/");
4674
+ if (fs26.statSync(scopeRoot).isFile()) {
4675
+ const rel = path16.relative(root, scopeRoot).split(path16.sep).join("/");
3801
4676
  if (ig.ignores(rel)) {
3802
4677
  return { content: `grep: file is ignored by repository filters.`, isError: true };
3803
4678
  }
3804
4679
  let st;
3805
4680
  try {
3806
- st = fs23.statSync(scopeRoot);
4681
+ st = fs26.statSync(scopeRoot);
3807
4682
  } catch {
3808
4683
  return { content: `grep: not found.`, isError: true };
3809
4684
  }
@@ -3812,7 +4687,7 @@ function runGrep(root, budget, args) {
3812
4687
  }
3813
4688
  let buf;
3814
4689
  try {
3815
- buf = fs23.readFileSync(scopeRoot);
4690
+ buf = fs26.readFileSync(scopeRoot);
3816
4691
  } catch (e) {
3817
4692
  return { content: `grep: failed to read (${e.message}).`, isError: true };
3818
4693
  }
@@ -3846,8 +4721,8 @@ ${matches.join("\n")}` : `No matches found for "${args.pattern}".`;
3846
4721
  }
3847
4722
 
3848
4723
  // src/planner/tools/list-dir.ts
3849
- import fs24 from "fs";
3850
- import path16 from "path";
4724
+ import fs27 from "fs";
4725
+ import path17 from "path";
3851
4726
  import { tool as tool3 } from "ai";
3852
4727
  import { z as z3 } from "zod";
3853
4728
  var MAX_ENTRIES_PER_LIST = 200;
@@ -3865,44 +4740,44 @@ function runListDir(root, budget, rawPath) {
3865
4740
  };
3866
4741
  }
3867
4742
  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)) {
4743
+ const resolved = path17.resolve(root, normalized);
4744
+ const relCheck = path17.relative(root, resolved);
4745
+ if (relCheck.startsWith("..") || path17.isAbsolute(relCheck)) {
3871
4746
  return {
3872
4747
  content: `list_dir: path "${rawPath}" escapes the project root and was refused.`,
3873
4748
  isError: true
3874
4749
  };
3875
4750
  }
3876
- const relNorm = relCheck.split(path16.sep).join("/");
4751
+ const relNorm = relCheck.split(path17.sep).join("/");
3877
4752
  const ig = buildRepoIgnore(root);
3878
4753
  if (relNorm && (ig.ignores(relNorm) || ig.ignores(`${relNorm}/`))) {
3879
4754
  return { content: "list_dir: (directory ignored by repo-map filter)", isError: true };
3880
4755
  }
3881
- if (!fs24.existsSync(resolved)) {
4756
+ if (!fs27.existsSync(resolved)) {
3882
4757
  return { content: `list_dir: not found "${rawPath}".`, isError: true };
3883
4758
  }
3884
- const st = fs24.statSync(resolved);
4759
+ const st = fs27.statSync(resolved);
3885
4760
  if (!st.isDirectory()) {
3886
4761
  return { content: `list_dir: "${rawPath}" is not a directory.`, isError: true };
3887
4762
  }
3888
4763
  let dirents;
3889
4764
  try {
3890
- dirents = fs24.readdirSync(resolved, { withFileTypes: true });
4765
+ dirents = fs27.readdirSync(resolved, { withFileTypes: true });
3891
4766
  } catch (e) {
3892
4767
  return { content: `list_dir: failed to read (${e.message}).`, isError: true };
3893
4768
  }
3894
4769
  const rows = [];
3895
4770
  for (const d of dirents) {
3896
4771
  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("/");
4772
+ const childAbs = path17.join(resolved, d.name);
4773
+ const childRel = path17.relative(root, childAbs).split(path17.sep).join("/");
3899
4774
  if (childRel && (ig.ignores(childRel) || d.isDirectory() && ig.ignores(`${childRel}/`))) continue;
3900
4775
  if (d.isDirectory()) {
3901
4776
  rows.push({ type: "d", name: d.name + "/", size: null });
3902
4777
  } else if (d.isFile()) {
3903
4778
  let size = 0;
3904
4779
  try {
3905
- size = fs24.statSync(childAbs).size;
4780
+ size = fs27.statSync(childAbs).size;
3906
4781
  } catch {
3907
4782
  size = 0;
3908
4783
  }
@@ -4181,96 +5056,9 @@ function vercelToolsFromDefinitions(defs) {
4181
5056
  return out;
4182
5057
  }
4183
5058
 
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
5059
  // src/core/runs.ts
4272
- import fs26 from "fs/promises";
4273
- import path18 from "path";
5060
+ import fs29 from "fs/promises";
5061
+ import path19 from "path";
4274
5062
  import { randomBytes } from "crypto";
4275
5063
 
4276
5064
  // src/core/run-retention.ts
@@ -4278,9 +5066,9 @@ var RUN_HISTORY_RING_SIZE = 20;
4278
5066
  var RUN_EVENT_JSONL_UNCOMPRESSED_HEAD = 5;
4279
5067
 
4280
5068
  // src/core/run-events-store.ts
4281
- import fs25, { createWriteStream } from "fs";
5069
+ import fs28, { createWriteStream } from "fs";
4282
5070
  import fsPromises from "fs/promises";
4283
- import path17 from "path";
5071
+ import path18 from "path";
4284
5072
  import readline from "readline";
4285
5073
  import { pipeline } from "stream/promises";
4286
5074
  import zlib from "zlib";
@@ -4288,10 +5076,14 @@ var SUFFIX_JSONL = ".events.jsonl";
4288
5076
  var SUFFIX_GZ = ".events.jsonl.gz";
4289
5077
  function redactPlannerEventForDisk(e) {
4290
5078
  if (e.kind === "thinking_delta") return { ...e, delta: "" };
5079
+ if (e.kind === "auth_info") {
5080
+ const { account: _account, ...rest } = e;
5081
+ return rest;
5082
+ }
4291
5083
  return e;
4292
5084
  }
4293
5085
  function runsDir(paths) {
4294
- return path17.join(paths.squadDir, "runs");
5086
+ return path18.join(paths.squadDir, "runs");
4295
5087
  }
4296
5088
  function logAppendErrorOnce(err2) {
4297
5089
  console.error("[run-events-store] append failed:", err2);
@@ -4348,8 +5140,8 @@ var RunEventsStoreImpl = class {
4348
5140
  };
4349
5141
  function createRunEventsStore(paths, runId) {
4350
5142
  const dir = runsDir(paths);
4351
- fs25.mkdirSync(dir, { recursive: true });
4352
- const file = path17.join(dir, `${runId}${SUFFIX_JSONL}`);
5143
+ fs28.mkdirSync(dir, { recursive: true });
5144
+ const file = path18.join(dir, `${runId}${SUFFIX_JSONL}`);
4353
5145
  return new RunEventsStoreImpl(file);
4354
5146
  }
4355
5147
  var RunEventsReaderImpl = class {
@@ -4374,7 +5166,7 @@ var RunEventsReaderImpl = class {
4374
5166
  const limit = opts?.limit ?? Number.POSITIVE_INFINITY;
4375
5167
  let logical = 0;
4376
5168
  let yielded = 0;
4377
- const stream = fs25.createReadStream(this.filePath);
5169
+ const stream = fs28.createReadStream(this.filePath);
4378
5170
  const input5 = this.useGunzip ? stream.pipe(zlib.createGunzip()) : stream;
4379
5171
  const rl = readline.createInterface({ input: input5, crlfDelay: Infinity });
4380
5172
  try {
@@ -4402,8 +5194,8 @@ var RunEventsReaderImpl = class {
4402
5194
  };
4403
5195
  async function openRunEventsReader(paths, runId) {
4404
5196
  const dir = runsDir(paths);
4405
- const jsonl = path17.join(dir, `${runId}${SUFFIX_JSONL}`);
4406
- const gz = path17.join(dir, `${runId}${SUFFIX_GZ}`);
5197
+ const jsonl = path18.join(dir, `${runId}${SUFFIX_JSONL}`);
5198
+ const gz = path18.join(dir, `${runId}${SUFFIX_GZ}`);
4407
5199
  let jsonlStat = null;
4408
5200
  let gzStat = null;
4409
5201
  try {
@@ -4435,10 +5227,10 @@ function collectEventLogs(dir, names) {
4435
5227
  kind = "jsonl";
4436
5228
  }
4437
5229
  if (!runId || !kind) continue;
4438
- const full = path17.join(dir, name);
5230
+ const full = path18.join(dir, name);
4439
5231
  let st;
4440
5232
  try {
4441
- st = { mtimeMs: fs25.statSync(full).mtimeMs };
5233
+ st = { mtimeMs: fs28.statSync(full).mtimeMs };
4442
5234
  } catch {
4443
5235
  continue;
4444
5236
  }
@@ -4466,7 +5258,7 @@ async function fsyncFile(filePath) {
4466
5258
  async function gzipAtomic(jsonlPath, gzPath) {
4467
5259
  const gzTmp = `${gzPath}.tmp`;
4468
5260
  await fsPromises.rm(gzTmp, { force: true });
4469
- await pipeline(fs25.createReadStream(jsonlPath), zlib.createGzip(), fs25.createWriteStream(gzTmp));
5261
+ await pipeline(fs28.createReadStream(jsonlPath), zlib.createGzip(), fs28.createWriteStream(gzTmp));
4470
5262
  await fsyncFile(gzTmp);
4471
5263
  await fsPromises.rename(gzTmp, gzPath);
4472
5264
  await fsPromises.unlink(jsonlPath);
@@ -4481,14 +5273,14 @@ async function rotateRunEventsUnlocked(paths) {
4481
5273
  }
4482
5274
  for (const name of names) {
4483
5275
  if (name.endsWith(`${SUFFIX_GZ}.tmp`)) {
4484
- await fsPromises.rm(path17.join(dir, name), { force: true });
5276
+ await fsPromises.rm(path18.join(dir, name), { force: true });
4485
5277
  }
4486
5278
  }
4487
5279
  names = await fsPromises.readdir(dir);
4488
5280
  const ordered = collectEventLogs(dir, names);
4489
5281
  for (let i = RUN_HISTORY_RING_SIZE; i < ordered.length; i++) {
4490
5282
  const e = ordered[i];
4491
- const summary = path17.join(dir, `${e.runId}.json`);
5283
+ const summary = path18.join(dir, `${e.runId}.json`);
4492
5284
  await fsPromises.rm(summary, { force: true });
4493
5285
  if (e.jsonlPath) await fsPromises.rm(e.jsonlPath, { force: true });
4494
5286
  if (e.gzPath) await fsPromises.rm(e.gzPath, { force: true });
@@ -4498,7 +5290,7 @@ async function rotateRunEventsUnlocked(paths) {
4498
5290
  const e = keep[i];
4499
5291
  const jsonl = e.jsonlPath;
4500
5292
  if (!jsonl) continue;
4501
- const gz = path17.join(dir, `${e.runId}${SUFFIX_GZ}`);
5293
+ const gz = path18.join(dir, `${e.runId}${SUFFIX_GZ}`);
4502
5294
  await gzipAtomic(jsonl, gz);
4503
5295
  }
4504
5296
  }
@@ -4515,34 +5307,34 @@ function newRunId() {
4515
5307
  return `${ts}-${rnd}`;
4516
5308
  }
4517
5309
  async function pruneOldRuns(paths) {
4518
- const dir = path18.join(paths.squadDir, "runs");
4519
- const entries = (await fs26.readdir(dir)).filter((n) => n.endsWith(".json"));
5310
+ const dir = path19.join(paths.squadDir, "runs");
5311
+ const entries = (await fs29.readdir(dir)).filter((n) => n.endsWith(".json"));
4520
5312
  if (entries.length <= RUN_HISTORY_RING_SIZE) return;
4521
5313
  entries.sort();
4522
5314
  const drop = entries.slice(0, entries.length - RUN_HISTORY_RING_SIZE);
4523
5315
  await Promise.all(
4524
5316
  drop.map(async (n) => {
4525
- const runPath = path18.join(dir, n);
5317
+ const runPath = path19.join(dir, n);
4526
5318
  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 });
5319
+ await fs29.rm(runPath, { force: true });
5320
+ await fs29.rm(path19.join(dir, `${runId}.events.jsonl`), { force: true });
5321
+ await fs29.rm(path19.join(dir, `${runId}.events.jsonl.gz`), { force: true });
4530
5322
  })
4531
5323
  );
4532
5324
  }
4533
5325
  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");
5326
+ const dir = path19.join(paths.squadDir, "runs");
5327
+ await fs29.mkdir(dir, { recursive: true });
5328
+ const file = path19.join(dir, `${rec.runId}.json`);
5329
+ await fs29.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
4538
5330
  await pruneOldRuns(paths);
4539
5331
  await rotateRunEvents(paths);
4540
5332
  }
4541
5333
  async function listRuns(paths) {
4542
- const dir = path18.join(paths.squadDir, "runs");
5334
+ const dir = path19.join(paths.squadDir, "runs");
4543
5335
  let entries;
4544
5336
  try {
4545
- entries = await fs26.readdir(dir);
5337
+ entries = await fs29.readdir(dir);
4546
5338
  } catch {
4547
5339
  return [];
4548
5340
  }
@@ -4550,7 +5342,7 @@ async function listRuns(paths) {
4550
5342
  const out = [];
4551
5343
  for (const f of files) {
4552
5344
  try {
4553
- const raw = await fs26.readFile(path18.join(dir, f), "utf8");
5345
+ const raw = await fs29.readFile(path19.join(dir, f), "utf8");
4554
5346
  const parsed = JSON.parse(raw);
4555
5347
  if (parsed.version === 1) out.push(parsed);
4556
5348
  } catch {
@@ -4887,6 +5679,11 @@ var VercelRuntime = class {
4887
5679
  if (typeof apiKeyOrModel !== "string") {
4888
5680
  throw new Error("VercelRuntime: apiKey must be a string unless injectModel is true");
4889
5681
  }
5682
+ if (!apiKeyOrModel) {
5683
+ throw new Error(
5684
+ `VercelRuntime: a non-empty ${provider} API key is required. Run \`squad config set planner\` to save one.`
5685
+ );
5686
+ }
4890
5687
  this.languageModel = resolveModel(provider, modelId, apiKeyOrModel).model;
4891
5688
  }
4892
5689
  async runScout(input5) {
@@ -5339,8 +6136,8 @@ async function runScout(input5) {
5339
6136
  }
5340
6137
 
5341
6138
  // src/planner/validation.ts
5342
- import fs27 from "fs";
5343
- import path19 from "path";
6139
+ import fs30 from "fs";
6140
+ import path20 from "path";
5344
6141
  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
6142
  function summariseIssuesByKind(issues) {
5346
6143
  const out = {};
@@ -5410,21 +6207,21 @@ function trigramSimilarity(a, b) {
5410
6207
  return inter / Math.sqrt(A.size * B.size);
5411
6208
  }
5412
6209
  function suggestPaths(root, missingRel, max = 3) {
5413
- const targetBase = path19.basename(missingRel).toLowerCase();
6210
+ const targetBase = path20.basename(missingRel).toLowerCase();
5414
6211
  const ig = buildRepoIgnore(root);
5415
6212
  const candidates = [];
5416
6213
  const walk2 = (dir) => {
5417
6214
  if (candidates.length >= 2e3) return;
5418
6215
  let ents;
5419
6216
  try {
5420
- ents = fs27.readdirSync(dir, { withFileTypes: true });
6217
+ ents = fs30.readdirSync(dir, { withFileTypes: true });
5421
6218
  } catch {
5422
6219
  return;
5423
6220
  }
5424
6221
  for (const ent of ents) {
5425
6222
  if (candidates.length >= 2e3) return;
5426
- const abs = path19.join(dir, ent.name);
5427
- const rel = path19.relative(root, abs).split(path19.sep).join("/");
6223
+ const abs = path20.join(dir, ent.name);
6224
+ const rel = path20.relative(root, abs).split(path20.sep).join("/");
5428
6225
  if (!rel || ig.ignores(rel) || ent.isDirectory() && ig.ignores(`${rel}/`)) continue;
5429
6226
  if (ent.isDirectory()) walk2(abs);
5430
6227
  else if (ent.isFile()) candidates.push(rel);
@@ -5433,7 +6230,7 @@ function suggestPaths(root, missingRel, max = 3) {
5433
6230
  walk2(root);
5434
6231
  const rows = [];
5435
6232
  for (const rel of candidates) {
5436
- const base = path19.basename(rel).toLowerCase();
6233
+ const base = path20.basename(rel).toLowerCase();
5437
6234
  let score = 0;
5438
6235
  if (base === targetBase) score = 1e3;
5439
6236
  else if (base.startsWith(targetBase) || targetBase.startsWith(base)) score = 500;
@@ -5511,12 +6308,12 @@ function validatePlan(input5) {
5511
6308
  const cache = /* @__PURE__ */ new Map();
5512
6309
  const readCached = (rel) => {
5513
6310
  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;
6311
+ const resolved = path20.resolve(input5.root, rel);
6312
+ const rc = path20.relative(input5.root, resolved);
6313
+ if (rc.startsWith("..") || path20.isAbsolute(rc)) return void 0;
6314
+ if (!fs30.existsSync(resolved) || !fs30.statSync(resolved).isFile()) return void 0;
5518
6315
  try {
5519
- const buf = fs27.readFileSync(resolved);
6316
+ const buf = fs30.readFileSync(resolved);
5520
6317
  const isBinary = buf.includes(0);
5521
6318
  const text = isBinary ? "" : buf.toString("utf8");
5522
6319
  const lines = isBinary ? 0 : text.split(/\r?\n/).length;
@@ -5530,10 +6327,10 @@ function validatePlan(input5) {
5530
6327
  const pathClaims = mergePathClaims(input5.planText);
5531
6328
  for (const c of pathClaims) {
5532
6329
  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)) {
6330
+ const resolved = path20.resolve(input5.root, c.path);
6331
+ const rel = path20.relative(input5.root, resolved);
6332
+ if (rel.startsWith("..") || path20.isAbsolute(rel)) continue;
6333
+ if (!fs30.existsSync(resolved)) {
5537
6334
  const suggestions = suggestPaths(input5.root, c.path);
5538
6335
  issues.push({
5539
6336
  severity: "warning",
@@ -5553,8 +6350,8 @@ function validatePlan(input5) {
5553
6350
  if (!Number.isFinite(lo) || !Number.isFinite(hi)) continue;
5554
6351
  const filePath = pathNear(rangeText, rm2.index);
5555
6352
  if (!filePath) continue;
5556
- const cached = readCached(filePath);
5557
- const lines = cached?.isBinary ? void 0 : cached?.lines;
6353
+ const cached2 = readCached(filePath);
6354
+ const lines = cached2?.isBinary ? void 0 : cached2?.lines;
5558
6355
  if (lines !== void 0 && hi > lines) {
5559
6356
  issues.push({
5560
6357
  severity: "warning",
@@ -5571,8 +6368,8 @@ function validatePlan(input5) {
5571
6368
  if (!Number.isFinite(n)) continue;
5572
6369
  const filePath = pathNear(rangeText, rm2.index);
5573
6370
  if (!filePath) continue;
5574
- const cached = readCached(filePath);
5575
- const lines = cached?.isBinary ? void 0 : cached?.lines;
6371
+ const cached2 = readCached(filePath);
6372
+ const lines = cached2?.isBinary ? void 0 : cached2?.lines;
5576
6373
  if (lines !== void 0 && n > lines) {
5577
6374
  issues.push({
5578
6375
  severity: "warning",
@@ -5588,8 +6385,8 @@ function validatePlan(input5) {
5588
6385
  const fp = rm2[1].trim().replace(/^\.\//, "");
5589
6386
  const n = parseInt(rm2[2], 10);
5590
6387
  if (!Number.isFinite(n)) continue;
5591
- const cached = readCached(fp);
5592
- const lines = cached?.isBinary ? void 0 : cached?.lines;
6388
+ const cached2 = readCached(fp);
6389
+ const lines = cached2?.isBinary ? void 0 : cached2?.lines;
5593
6390
  if (lines !== void 0 && n > lines) {
5594
6391
  issues.push({
5595
6392
  severity: "warning",
@@ -5607,8 +6404,8 @@ function validatePlan(input5) {
5607
6404
  const pathNearCell = near.match(/`([^`\n/]+\/[^`\n]+?\.[a-z0-9]{1,6})`/i);
5608
6405
  if (!pathNearCell) continue;
5609
6406
  const fp = pathNearCell[1].replace(/^\.\//, "");
5610
- const cached = readCached(fp);
5611
- if (!cached || cached.isBinary) continue;
6407
+ const cached2 = readCached(fp);
6408
+ if (!cached2 || cached2.isBinary) continue;
5612
6409
  const symRe = /`([A-Za-z_][A-Za-z0-9_]{2,50})`/g;
5613
6410
  let sm;
5614
6411
  while ((sm = symRe.exec(line2)) !== null) {
@@ -5618,8 +6415,8 @@ function validatePlan(input5) {
5618
6415
  )) {
5619
6416
  continue;
5620
6417
  }
5621
- if (!cached.text.includes(sym)) {
5622
- const suggestions = suggestSymbols(cached.text, sym);
6418
+ if (!cached2.text.includes(sym)) {
6419
+ const suggestions = suggestSymbols(cached2.text, sym);
5623
6420
  issues.push({
5624
6421
  severity: "warning",
5625
6422
  kind: "symbol_not_found",
@@ -5700,9 +6497,9 @@ ${result.content}
5700
6497
  }
5701
6498
  continue;
5702
6499
  }
5703
- const resolved = path20.resolve(root, cleaned);
5704
- const rc = path20.relative(root, resolved);
5705
- if (rc.startsWith("..") || path20.isAbsolute(rc)) {
6500
+ const resolved = path21.resolve(root, cleaned);
6501
+ const rc = path21.relative(root, resolved);
6502
+ if (rc.startsWith("..") || path21.isAbsolute(rc)) {
5706
6503
  bus.emit({
5707
6504
  kind: "validation_issue",
5708
6505
  runId,
@@ -5713,7 +6510,7 @@ ${result.content}
5713
6510
  });
5714
6511
  continue;
5715
6512
  }
5716
- if (!fs28.existsSync(resolved)) {
6513
+ if (!fs31.existsSync(resolved)) {
5717
6514
  bus.emit({
5718
6515
  kind: "validation_issue",
5719
6516
  runId,
@@ -5724,7 +6521,7 @@ ${result.content}
5724
6521
  });
5725
6522
  continue;
5726
6523
  }
5727
- const st = fs28.statSync(resolved);
6524
+ const st = fs31.statSync(resolved);
5728
6525
  if (!st.isFile()) continue;
5729
6526
  const n = Math.min(st.size, MAX_BYTES_PER_FULL_READ);
5730
6527
  const cap = budget.canRead(n);
@@ -5738,7 +6535,7 @@ _(skipped: ${cap.reason})_
5738
6535
  }
5739
6536
  let buf;
5740
6537
  try {
5741
- buf = fs28.readFileSync(resolved);
6538
+ buf = fs31.readFileSync(resolved);
5742
6539
  } catch {
5743
6540
  continue;
5744
6541
  }
@@ -5931,6 +6728,9 @@ async function runPlanner(input5) {
5931
6728
  }
5932
6729
  } : void 0
5933
6730
  });
6731
+ if (input5.auth) {
6732
+ bus.emit({ kind: "auth_info", runId, ...describeAuth(input5.auth) });
6733
+ }
5934
6734
  const merged = { ...input5, events: bus, runId };
5935
6735
  const systemPrompt = input5.systemPrompt;
5936
6736
  let scoutSummary;
@@ -6071,8 +6871,16 @@ async function runPlanner(input5) {
6071
6871
  }
6072
6872
 
6073
6873
  // src/planner/planner-limit-messages.ts
6074
- function printPlannerApiCostNotice() {
6874
+ function printPlannerApiCostNotice(authMode = "api-key") {
6075
6875
  blank();
6876
+ if (authMode === "subscription") {
6877
+ step("Usage");
6878
+ info(
6879
+ "`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."
6880
+ );
6881
+ blank();
6882
+ return;
6883
+ }
6076
6884
  step("Billing");
6077
6885
  info(
6078
6886
  "`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 +6903,12 @@ function tokenSummary(ctx) {
6095
6903
  const snap = ctx.budgetSnapshot;
6096
6904
  return `Tokens so far: ${snap.usage.inputTokens} in / ${snap.usage.outputTokens} out for this session.`;
6097
6905
  }
6098
- function kindDetail(ctx) {
6906
+ function kindDetail(ctx, authMode) {
6099
6907
  switch (ctx.kind) {
6100
6908
  case "max_output_tokens":
6101
6909
  return [
6102
6910
  `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).",
6911
+ "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
6912
  tokenSummary(ctx)
6105
6913
  ];
6106
6914
  case "max_iterations":
@@ -6123,9 +6931,9 @@ function kindDetail(ctx) {
6123
6931
  ];
6124
6932
  }
6125
6933
  }
6126
- function printPlannerLimitExplanation(ctx) {
6934
+ function printPlannerLimitExplanation(ctx, authMode = "api-key") {
6127
6935
  warning(kindTitle(ctx.kind));
6128
- for (const line2 of kindDetail(ctx)) {
6936
+ for (const line2 of kindDetail(ctx, authMode)) {
6129
6937
  info(line2);
6130
6938
  }
6131
6939
  kv("session so far", `${snapSummary(ctx)}`, 18);
@@ -6176,6 +6984,23 @@ function thinkingConfigFromProviderSpecific(ps) {
6176
6984
  }
6177
6985
 
6178
6986
  // src/planner/runtimes/agent-sdk-runtime.ts
6987
+ var ACCOUNT_INFO_TIMEOUT_MS = 2e3;
6988
+ var MAX_RATE_LIMIT_RETRY_SEC2 = 90;
6989
+ var enrichedAuthInfoRuns = /* @__PURE__ */ new WeakMap();
6990
+ function markAuthInfoEmitted(bus, runId) {
6991
+ let seen = enrichedAuthInfoRuns.get(bus);
6992
+ if (!seen) {
6993
+ seen = /* @__PURE__ */ new Set();
6994
+ enrichedAuthInfoRuns.set(bus, seen);
6995
+ }
6996
+ if (seen.has(runId)) return false;
6997
+ seen.add(runId);
6998
+ return true;
6999
+ }
7000
+ var API_KEY_SOURCES = ["user", "project", "org", "temporary", "oauth"];
7001
+ function asApiKeySource(v) {
7002
+ return API_KEY_SOURCES.find((s) => s === v);
7003
+ }
6179
7004
  var BUILTIN_TOOL_DENY = [
6180
7005
  "Read",
6181
7006
  "Write",
@@ -6347,15 +7172,96 @@ function handleDecodedStreamEvent(decoded, input5) {
6347
7172
  }
6348
7173
  }
6349
7174
  }
7175
+ async function readSdkAccountInfo(q) {
7176
+ const fn = q?.accountInfo;
7177
+ if (typeof fn !== "function") return void 0;
7178
+ let timer;
7179
+ try {
7180
+ const info2 = await Promise.race([
7181
+ fn.call(q),
7182
+ new Promise((resolve2) => {
7183
+ timer = setTimeout(() => resolve2(void 0), ACCOUNT_INFO_TIMEOUT_MS);
7184
+ })
7185
+ ]);
7186
+ if (!info2 || typeof info2 !== "object") return void 0;
7187
+ const o = info2;
7188
+ const out = {};
7189
+ if (typeof o.email === "string") out.email = o.email;
7190
+ if (typeof o.organization === "string") out.organization = o.organization;
7191
+ if (typeof o.subscriptionType === "string") out.subscriptionType = o.subscriptionType;
7192
+ return Object.keys(out).length > 0 ? out : void 0;
7193
+ } catch {
7194
+ return void 0;
7195
+ } finally {
7196
+ if (timer) clearTimeout(timer);
7197
+ }
7198
+ }
6350
7199
  var AgentSdkRuntime = class {
6351
- constructor(modelId, apiKey) {
7200
+ constructor(modelId, auth2) {
6352
7201
  this.modelId = modelId;
6353
- this.apiKey = apiKey;
7202
+ this.auth = auth2;
6354
7203
  }
6355
7204
  modelId;
6356
- apiKey;
7205
+ auth;
6357
7206
  kind = "agent-sdk";
6358
7207
  providerName = "anthropic";
7208
+ /** Safe-to-log auth projection — never carries the key or the token. */
7209
+ authDescriptor() {
7210
+ return describeAuth(this.auth);
7211
+ }
7212
+ /**
7213
+ * Handle a `system` message. On `init` this emits the enriched `auth_info` (once per run, even
7214
+ * though scout and draft are separate instances); on `api_retry` it forwards a rate limit through
7215
+ * the existing 0.8.0 rate-limit channel so the console countdown and CLI spinner keep working.
7216
+ */
7217
+ async handleSystemMessage(m, input5, ctx) {
7218
+ if (m.subtype === "init") {
7219
+ const apiKeySource = asApiKeySource(m.apiKeySource);
7220
+ if (!apiKeySource || !markAuthInfoEmitted(input5.bus, input5.runId)) return;
7221
+ const account = ctx.withAccountInfo && this.auth.mode === "subscription" ? await readSdkAccountInfo(ctx.query) : void 0;
7222
+ input5.bus.emit({
7223
+ kind: "auth_info",
7224
+ runId: input5.runId,
7225
+ ...this.authDescriptor(),
7226
+ apiKeySource,
7227
+ ...account ? { account } : {}
7228
+ });
7229
+ return;
7230
+ }
7231
+ if (m.subtype === "api_retry" && detectAuthShapedSdkError(m.error) === "rate_limit") {
7232
+ const delayMs = typeof m.retry_delay_ms === "number" ? m.retry_delay_ms : 0;
7233
+ const waitSec = Math.max(1, Math.ceil(delayMs / 1e3));
7234
+ input5.bus.emit({
7235
+ kind: "rate_limit",
7236
+ runId: input5.runId,
7237
+ turn: ctx.turn,
7238
+ retryAfterSec: waitSec,
7239
+ waitSec,
7240
+ capSec: MAX_RATE_LIMIT_RETRY_SEC2,
7241
+ phase: "retrying",
7242
+ provider: "anthropic"
7243
+ });
7244
+ }
7245
+ }
7246
+ /**
7247
+ * Turn an auth-shaped SDK failure into an actionable error. Rate limits also emit the terminal
7248
+ * `rate_limit` event so the UI closes its countdown before the throw.
7249
+ */
7250
+ authError(signal, input5, turn) {
7251
+ if (signal === "rate_limit") {
7252
+ input5.bus.emit({
7253
+ kind: "rate_limit",
7254
+ runId: input5.runId,
7255
+ turn,
7256
+ retryAfterSec: void 0,
7257
+ waitSec: 0,
7258
+ capSec: MAX_RATE_LIMIT_RETRY_SEC2,
7259
+ phase: "aborted",
7260
+ provider: "anthropic"
7261
+ });
7262
+ }
7263
+ return new Error(authErrorMessage(signal, authErrorContextFrom(this.auth)));
7264
+ }
6359
7265
  async runDraft(input5) {
6360
7266
  const { query, createSdkMcpServer, tool: tool5 } = await import("@anthropic-ai/claude-agent-sdk");
6361
7267
  const sdkTools = input5.tools.map(
@@ -6390,7 +7296,7 @@ var AgentSdkRuntime = class {
6390
7296
  persistSession: false,
6391
7297
  settingSources: [],
6392
7298
  abortController: ac,
6393
- env: { ...process.env, ANTHROPIC_API_KEY: this.apiKey },
7299
+ env: buildSdkEnv(this.auth),
6394
7300
  thinking,
6395
7301
  effort
6396
7302
  };
@@ -6400,6 +7306,7 @@ var AgentSdkRuntime = class {
6400
7306
  let finishedNormally = false;
6401
7307
  let incompleteKind;
6402
7308
  let userCancelled = false;
7309
+ let authSignal;
6403
7310
  const think = initialThinkingState();
6404
7311
  try {
6405
7312
  const q = query({
@@ -6421,6 +7328,7 @@ var AgentSdkRuntime = class {
6421
7328
  break;
6422
7329
  }
6423
7330
  case "assistant": {
7331
+ authSignal = detectAuthShapedSdkError(m.error) ?? authSignal;
6424
7332
  think.blockIndexCounter = 0;
6425
7333
  think.inThinkingBlock = false;
6426
7334
  think.thinkingBlockIndex = -1;
@@ -6447,10 +7355,17 @@ var AgentSdkRuntime = class {
6447
7355
  finishedNormally = false;
6448
7356
  } else if (typeof sub === "string" && sub.startsWith("error")) {
6449
7357
  finishedNormally = false;
7358
+ const signal = authSignal ?? detectAuthShapedSdkError(m.errors);
7359
+ if (signal) throw this.authError(signal, input5, assistantTurns);
6450
7360
  }
6451
7361
  break;
6452
7362
  }
6453
7363
  case "system":
7364
+ await this.handleSystemMessage(m, input5, {
7365
+ query: q,
7366
+ withAccountInfo: true,
7367
+ turn: assistantTurns
7368
+ });
6454
7369
  break;
6455
7370
  default:
6456
7371
  break;
@@ -6509,12 +7424,13 @@ var AgentSdkRuntime = class {
6509
7424
  includePartialMessages: true,
6510
7425
  persistSession: false,
6511
7426
  settingSources: [],
6512
- env: { ...process.env, ANTHROPIC_API_KEY: this.apiKey },
7427
+ env: buildSdkEnv(this.auth),
6513
7428
  thinking,
6514
7429
  effort
6515
7430
  };
6516
7431
  let usage = { inputTokens: 0, outputTokens: 0 };
6517
7432
  let assistantTurns = 0;
7433
+ let authSignal;
6518
7434
  const think = initialThinkingState();
6519
7435
  const ac = new AbortController();
6520
7436
  const onAbort = () => ac.abort();
@@ -6539,6 +7455,7 @@ var AgentSdkRuntime = class {
6539
7455
  });
6540
7456
  break;
6541
7457
  case "assistant":
7458
+ authSignal = detectAuthShapedSdkError(m.error) ?? authSignal;
6542
7459
  think.blockIndexCounter = 0;
6543
7460
  think.inThinkingBlock = false;
6544
7461
  think.thinkingBlockIndex = -1;
@@ -6549,8 +7466,17 @@ var AgentSdkRuntime = class {
6549
7466
  usage = usageFromAgentSdkResult(
6550
7467
  m.usage
6551
7468
  );
7469
+ if (typeof m.subtype === "string" && m.subtype.startsWith("error")) {
7470
+ const signal = authSignal ?? detectAuthShapedSdkError(m.errors);
7471
+ if (signal) throw this.authError(signal, input5, assistantTurns);
7472
+ }
6552
7473
  break;
6553
7474
  case "system":
7475
+ await this.handleSystemMessage(m, input5, {
7476
+ query: q,
7477
+ withAccountInfo: false,
7478
+ turn: assistantTurns
7479
+ });
6554
7480
  break;
6555
7481
  default:
6556
7482
  break;
@@ -6572,14 +7498,40 @@ var AgentSdkRuntime = class {
6572
7498
  }
6573
7499
  };
6574
7500
 
7501
+ // src/planner/runtimes/auth-runtime-mismatch.ts
7502
+ var SUBSCRIPTION_NEEDS_AGENT_SDK_LEAD = "Subscription auth needs the Claude Agent SDK runtime.";
7503
+ 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.";
7504
+ function subscriptionProviderMismatchHint(provider) {
7505
+ 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.`;
7506
+ }
7507
+ function plannerAuthRuntimeMismatchMessage(provider) {
7508
+ return `${SUBSCRIPTION_NEEDS_AGENT_SDK_LEAD} ${provider === "anthropic" ? SUBSCRIPTION_VERCEL_RUNTIME_HINT : subscriptionProviderMismatchHint(provider)}`;
7509
+ }
7510
+
6575
7511
  // src/planner/runtimes/index.ts
7512
+ var PlannerAuthRuntimeMismatchError = class extends Error {
7513
+ constructor(provider, runtimeChoice) {
7514
+ super(plannerAuthRuntimeMismatchMessage(provider));
7515
+ this.provider = provider;
7516
+ this.runtimeChoice = runtimeChoice;
7517
+ this.name = "PlannerAuthRuntimeMismatchError";
7518
+ }
7519
+ provider;
7520
+ runtimeChoice;
7521
+ };
6576
7522
  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);
7523
+ const choice = input5.provider === "anthropic" ? input5.anthropicRuntime ?? "agent-sdk" : "vercel";
7524
+ const auth2 = input5.auth;
7525
+ if (auth2.mode === "subscription") {
7526
+ if (input5.provider !== "anthropic" || choice !== "agent-sdk") {
7527
+ throw new PlannerAuthRuntimeMismatchError(input5.provider, choice);
7528
+ }
7529
+ return new AgentSdkRuntime(input5.modelId, auth2);
7530
+ }
7531
+ if (input5.provider === "anthropic" && choice === "agent-sdk") {
7532
+ return new AgentSdkRuntime(input5.modelId, auth2);
6581
7533
  }
6582
- return new VercelRuntime(input5.provider, input5.modelId, input5.apiKey);
7534
+ return new VercelRuntime(input5.provider, input5.modelId, auth2.key);
6583
7535
  }
6584
7536
 
6585
7537
  // src/planner/budget.ts
@@ -6651,8 +7603,8 @@ var Budget = class {
6651
7603
  };
6652
7604
 
6653
7605
  // src/planner/writer.ts
6654
- import fs29 from "fs";
6655
- import path21 from "path";
7606
+ import fs32 from "fs";
7607
+ import path22 from "path";
6656
7608
  function planFileSlugParts(config2, story) {
6657
7609
  const baseSlug = slugify(story.id);
6658
7610
  const trackerId = config2.naming.includeTrackerId && config2.tracker.type !== "none" ? trackerIdForFilename(config2.tracker.type, story.id) : "";
@@ -6661,8 +7613,8 @@ function planFileSlugParts(config2, story) {
6661
7613
  }
6662
7614
  function writePlanFile(input5) {
6663
7615
  const { paths, config: config2, story } = input5;
6664
- const featurePlanDir = path21.join(paths.plansDir, story.feature);
6665
- fs29.mkdirSync(featurePlanDir, { recursive: true });
7616
+ const featurePlanDir = path22.join(paths.plansDir, story.feature);
7617
+ fs32.mkdirSync(featurePlanDir, { recursive: true });
6666
7618
  if (input5.partial) {
6667
7619
  return writePartialPlanFile(input5, featurePlanDir, paths, config2, story);
6668
7620
  }
@@ -6677,14 +7629,14 @@ function writePlanFile(input5) {
6677
7629
  } else {
6678
7630
  nn = scanPlans(paths.plansDir).nextGlobal;
6679
7631
  const { baseSlug, trackerPart } = planFileSlugParts(config2, story);
6680
- targetPath = path21.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.md`);
7632
+ targetPath = path22.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.md`);
6681
7633
  }
6682
7634
  const body = `${input5.metadataHeader.trimEnd()}
6683
7635
 
6684
7636
  ${input5.planBodyMarkdown.trimStart()}
6685
7637
  `;
6686
- fs29.writeFileSync(targetPath, body, "utf8");
6687
- upsertOverviewRow(featurePlanDir, story.feature, nn, path21.basename(targetPath), story, false);
7638
+ fs32.writeFileSync(targetPath, body, "utf8");
7639
+ upsertOverviewRow(featurePlanDir, story.feature, nn, path22.basename(targetPath), story, false);
6688
7640
  return { planFile: targetPath, sequenceNumber: nn, overwrote };
6689
7641
  }
6690
7642
  function writePartialPlanFile(input5, featurePlanDir, paths, config2, story) {
@@ -6699,42 +7651,42 @@ function writePartialPlanFile(input5, featurePlanDir, paths, config2, story) {
6699
7651
  } else {
6700
7652
  nn = scanPlans(paths.plansDir).nextGlobal;
6701
7653
  const { baseSlug, trackerPart } = planFileSlugParts(config2, story);
6702
- targetPath = path21.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.partial.md`);
7654
+ targetPath = path22.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.partial.md`);
6703
7655
  }
6704
7656
  const body = `${input5.metadataHeader.trimEnd()}
6705
7657
 
6706
7658
  ${input5.planBodyMarkdown.trimStart()}
6707
7659
  `;
6708
- fs29.writeFileSync(targetPath, body, "utf8");
6709
- upsertOverviewRow(featurePlanDir, story.feature, nn, path21.basename(targetPath), story, true);
7660
+ fs32.writeFileSync(targetPath, body, "utf8");
7661
+ upsertOverviewRow(featurePlanDir, story.feature, nn, path22.basename(targetPath), story, true);
6710
7662
  return { planFile: targetPath, sequenceNumber: nn, overwrote };
6711
7663
  }
6712
7664
  function findExistingPlan(featureDir, storyId) {
6713
- if (!fs29.existsSync(featureDir)) return void 0;
7665
+ if (!fs32.existsSync(featureDir)) return void 0;
6714
7666
  const pattern = /^(\d{2,})-story-.+\.md$/;
6715
- for (const entry of fs29.readdirSync(featureDir)) {
7667
+ for (const entry of fs32.readdirSync(featureDir)) {
6716
7668
  if (entry.endsWith(".partial.md")) continue;
6717
7669
  const m = entry.match(pattern);
6718
7670
  if (!m) continue;
6719
- if (entry.includes(storyId)) return { absPath: path21.join(featureDir, entry), nn: parseInt(m[1], 10) };
7671
+ if (entry.includes(storyId)) return { absPath: path22.join(featureDir, entry), nn: parseInt(m[1], 10) };
6720
7672
  }
6721
7673
  return void 0;
6722
7674
  }
6723
7675
  function findExistingPartialPlan(featureDir, storyId) {
6724
- if (!fs29.existsSync(featureDir)) return void 0;
7676
+ if (!fs32.existsSync(featureDir)) return void 0;
6725
7677
  const pattern = /^(\d{2,})-story-.+\.partial\.md$/;
6726
- for (const entry of fs29.readdirSync(featureDir)) {
7678
+ for (const entry of fs32.readdirSync(featureDir)) {
6727
7679
  const m = entry.match(pattern);
6728
7680
  if (!m) continue;
6729
- if (entry.includes(storyId)) return { absPath: path21.join(featureDir, entry), nn: parseInt(m[1], 10) };
7681
+ if (entry.includes(storyId)) return { absPath: path22.join(featureDir, entry), nn: parseInt(m[1], 10) };
6730
7682
  }
6731
7683
  return void 0;
6732
7684
  }
6733
7685
  function upsertOverviewRow(featureDir, feature, nn, filename, story, isPartial) {
6734
- const overviewPath = path21.join(featureDir, "00-overview.md");
7686
+ const overviewPath = path22.join(featureDir, "00-overview.md");
6735
7687
  const titleCell = isPartial ? `${story.titleHint ?? story.id} (partial)` : story.titleHint ?? story.id;
6736
7688
  const row = `| ${formatSequence(nn)} | \`${filename}\` | ${titleCell} | ${story.id} | \u2014 |`;
6737
- if (!fs29.existsSync(overviewPath)) {
7689
+ if (!fs32.existsSync(overviewPath)) {
6738
7690
  const content = `# ${feature} \u2014 plan overview
6739
7691
 
6740
7692
  Entry point for the **${feature}** feature. Stories execute in order by their \`NN\` prefix.
@@ -6745,10 +7697,10 @@ Entry point for the **${feature}** feature. Stories execute in order by their \`
6745
7697
  |----|------|-------|------------|------------|
6746
7698
  ${row}
6747
7699
  `;
6748
- fs29.writeFileSync(overviewPath, content, "utf8");
7700
+ fs32.writeFileSync(overviewPath, content, "utf8");
6749
7701
  return;
6750
7702
  }
6751
- const current = fs29.readFileSync(overviewPath, "utf8");
7703
+ const current = fs32.readFileSync(overviewPath, "utf8");
6752
7704
  const lines = current.split("\n");
6753
7705
  const existingIdx = lines.findIndex((l) => l.includes(`\`${filename}\``) || l.includes(`| ${formatSequence(nn)} |`));
6754
7706
  if (existingIdx >= 0) {
@@ -6757,7 +7709,7 @@ ${row}
6757
7709
  const tableEnd = findTableEnd(lines);
6758
7710
  lines.splice(tableEnd, 0, row);
6759
7711
  }
6760
- fs29.writeFileSync(overviewPath, lines.join("\n"), "utf8");
7712
+ fs32.writeFileSync(overviewPath, lines.join("\n"), "utf8");
6761
7713
  }
6762
7714
  function findTableEnd(lines) {
6763
7715
  let inTable = false;
@@ -6810,16 +7762,16 @@ function buildCopyPlanPromptMarkdown(config2, intakeContent) {
6810
7762
  }
6811
7763
 
6812
7764
  // src/core/last-run.ts
6813
- import fs30 from "fs/promises";
6814
- import path22 from "path";
7765
+ import fs33 from "fs/promises";
7766
+ import path23 from "path";
6815
7767
  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");
7768
+ const file = path23.join(paths.squadDir, ".last-run.json");
7769
+ await fs33.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
6818
7770
  }
6819
7771
  async function readLastRun(paths) {
6820
- const file = path22.join(paths.squadDir, ".last-run.json");
7772
+ const file = path23.join(paths.squadDir, ".last-run.json");
6821
7773
  try {
6822
- const raw = await fs30.readFile(file, "utf8");
7774
+ const raw = await fs33.readFile(file, "utf8");
6823
7775
  const parsed = JSON.parse(raw);
6824
7776
  if (parsed.version !== 1) return void 0;
6825
7777
  return parsed;
@@ -6852,8 +7804,8 @@ function formatPlannerCacheLine(opts) {
6852
7804
  }
6853
7805
 
6854
7806
  // src/core/plans-index.ts
6855
- import fs31 from "fs";
6856
- import path23 from "path";
7807
+ import fs34 from "fs";
7808
+ import path24 from "path";
6857
7809
  var MAX_ENTRIES = 30;
6858
7810
  function firstMarkdownH1(src) {
6859
7811
  const lines = src.split(/\r?\n/);
@@ -6868,12 +7820,12 @@ function collectPlanMarkdownFiles(plansDir) {
6868
7820
  const walk2 = (dir) => {
6869
7821
  let ents;
6870
7822
  try {
6871
- ents = fs31.readdirSync(dir, { withFileTypes: true });
7823
+ ents = fs34.readdirSync(dir, { withFileTypes: true });
6872
7824
  } catch {
6873
7825
  return;
6874
7826
  }
6875
7827
  for (const ent of ents) {
6876
- const abs = path23.join(dir, ent.name);
7828
+ const abs = path24.join(dir, ent.name);
6877
7829
  if (ent.isDirectory()) {
6878
7830
  walk2(abs);
6879
7831
  } else if (ent.isFile() && ent.name.endsWith(".md")) {
@@ -6882,7 +7834,7 @@ function collectPlanMarkdownFiles(plansDir) {
6882
7834
  }
6883
7835
  }
6884
7836
  };
6885
- if (fs31.existsSync(plansDir)) walk2(plansDir);
7837
+ if (fs34.existsSync(plansDir)) walk2(plansDir);
6886
7838
  out.sort((a, b) => a.localeCompare(b));
6887
7839
  return out.slice(0, MAX_ENTRIES);
6888
7840
  }
@@ -6891,13 +7843,13 @@ function buildPlansIndex(paths) {
6891
7843
  if (files.length === 0) return "(no prior plans yet)";
6892
7844
  const bullets = [];
6893
7845
  for (const abs of files) {
6894
- const rel = path23.relative(paths.root, abs).split(path23.sep).join("/");
7846
+ const rel = path24.relative(paths.root, abs).split(path24.sep).join("/");
6895
7847
  let title = "";
6896
7848
  try {
6897
- const raw = fs31.readFileSync(abs, "utf8");
6898
- title = firstMarkdownH1(raw) ?? path23.basename(abs);
7849
+ const raw = fs34.readFileSync(abs, "utf8");
7850
+ title = firstMarkdownH1(raw) ?? path24.basename(abs);
6899
7851
  } catch {
6900
- title = path23.basename(abs);
7852
+ title = path24.basename(abs);
6901
7853
  }
6902
7854
  bullets.push(`- ${rel} \u2014 "${title}"`);
6903
7855
  }
@@ -7157,9 +8109,10 @@ function decideMode(opts, config2) {
7157
8109
  }
7158
8110
  if (opts.api) return "api";
7159
8111
  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";
8112
+ const planner = config2.planner;
8113
+ if (!planner?.enabled || !planner.provider) return "copy";
8114
+ if (planner.provider === "anthropic" && planner.auth?.anthropic === "subscription") return "api";
8115
+ return readProviderKey(planner.provider) ? "api" : "copy";
7163
8116
  }
7164
8117
  async function runNewPlan(intakePath, opts) {
7165
8118
  const root = requireSquadRoot();
@@ -7170,7 +8123,7 @@ async function runNewPlan(intakePath, opts) {
7170
8123
  const story = intakePath ? resolveFromPath(intakePath, stories, root) : await pickStory(stories, { all: !!opts.all, interactive, feature: opts.feature });
7171
8124
  if (!story) return;
7172
8125
  if (story.planFile) {
7173
- const proceed = await confirmOverwrite(story, interactive, !!opts.yes);
8126
+ const proceed = await confirmOverwrite2(story, interactive, !!opts.yes);
7174
8127
  if (!proceed) return;
7175
8128
  }
7176
8129
  const mode = decideMode(opts, config2);
@@ -7181,8 +8134,8 @@ async function runNewPlan(intakePath, opts) {
7181
8134
  }
7182
8135
  }
7183
8136
  function resolveFromPath(intakePath, stories, root) {
7184
- const resolved = path24.resolve(intakePath.trim());
7185
- if (!fs32.existsSync(resolved)) {
8137
+ const resolved = path25.resolve(intakePath.trim());
8138
+ if (!fs35.existsSync(resolved)) {
7186
8139
  throw new Error(
7187
8140
  `Intake not found: ${resolved}. Run \`squad list\` to see intakes, or \`squad new-story\` to create one first.`
7188
8141
  );
@@ -7194,7 +8147,7 @@ function resolveFromPath(intakePath, stories, root) {
7194
8147
  );
7195
8148
  }
7196
8149
  function relativeStoriesDir(root) {
7197
- return path24.relative(root, path24.join(root, ".squad", "stories"));
8150
+ return path25.relative(root, path25.join(root, ".squad", "stories"));
7198
8151
  }
7199
8152
  async function pickStory(stories, opts) {
7200
8153
  const candidates = opts.all ? stories : stories.filter((s) => !s.planFile);
@@ -7225,7 +8178,7 @@ async function pickStory(stories, opts) {
7225
8178
  value: s.intakePath
7226
8179
  }))
7227
8180
  });
7228
- return candidates.find((s) => path24.resolve(s.intakePath) === path24.resolve(pick));
8181
+ return candidates.find((s) => path25.resolve(s.intakePath) === path25.resolve(pick));
7229
8182
  }
7230
8183
  function formatLabel(s) {
7231
8184
  const head = `${s.feature} / ${s.id}`;
@@ -7233,7 +8186,7 @@ function formatLabel(s) {
7233
8186
  const plannedTag = s.planFile ? ` ${theme_exports.warn("(planned)")}` : "";
7234
8187
  return head + tail + plannedTag;
7235
8188
  }
7236
- async function confirmOverwrite(story, interactive, yes) {
8189
+ async function confirmOverwrite2(story, interactive, yes) {
7237
8190
  warning(`A plan already exists for this intake:`);
7238
8191
  kv("plan", `.squad/plans/${story.feature}/${story.planFile}`);
7239
8192
  info("Regenerating will replace it when your planner writes the new version.");
@@ -7243,7 +8196,7 @@ async function confirmOverwrite(story, interactive, yes) {
7243
8196
  `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
8197
  );
7245
8198
  }
7246
- const go = await confirm6({ message: "Proceed and regenerate?", default: false });
8199
+ const go = await confirm7({ message: "Proceed and regenerate?", default: false });
7247
8200
  return go;
7248
8201
  }
7249
8202
  function printPlannerRunIncompleteSummary(result, relPlanFile) {
@@ -7258,6 +8211,10 @@ function printPlannerRunIncompleteSummary(result, relPlanFile) {
7258
8211
  info("The file uses the `.partial.md` suffix and YAML `squad-kit-plan-status: partial`.");
7259
8212
  info("Raise limits in `.squad/config.yaml` (`planner.budget`, `planner.maxOutputTokens`) or re-run when ready.");
7260
8213
  }
8214
+ function formatAuthRow(auth2) {
8215
+ if (auth2.mode === "subscription") return "subscription (Claude login)";
8216
+ return `api-key (${describeAuth(auth2).credentialHint})`;
8217
+ }
7261
8218
  async function emitViaApi(story, paths, config2, opts) {
7262
8219
  const planner = config2.planner;
7263
8220
  if (!planner?.enabled) {
@@ -7265,18 +8222,13 @@ async function emitViaApi(story, paths, config2, opts) {
7265
8222
  "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
8223
  );
7267
8224
  }
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
- }
8225
+ const auth2 = resolvePlannerAuthForCwd(planner.provider, planner);
7274
8226
  const modelId = modelFor(planner.provider, "plan", planner.modelOverride);
7275
8227
  const anthropicRuntimeChoice = opts.anthropicRuntime ?? planner.runtime?.anthropic ?? "agent-sdk";
7276
8228
  const draftRuntime = resolveRuntime({
7277
8229
  provider: planner.provider,
7278
8230
  modelId,
7279
- apiKey,
8231
+ auth: auth2,
7280
8232
  anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
7281
8233
  });
7282
8234
  const thinkingCli = opts.noThinking ? "off" : void 0;
@@ -7295,10 +8247,11 @@ async function emitViaApi(story, paths, config2, opts) {
7295
8247
  step(`planning ${story.feature} / ${story.id}`);
7296
8248
  kv("provider", planner.provider);
7297
8249
  kv("model", modelId);
8250
+ kv("auth", formatAuthRow(auth2));
7298
8251
  blank();
7299
8252
  const interactive = !opts.yes && Boolean(process.stdin.isTTY);
7300
8253
  if (interactive) {
7301
- printPlannerApiCostNotice();
8254
+ printPlannerApiCostNotice(auth2.mode);
7302
8255
  }
7303
8256
  const mapSpinner = spinner("building repo map\u2026");
7304
8257
  const repoMap = buildRepoMap(paths.root, { format: "tree" });
@@ -7330,7 +8283,7 @@ async function emitViaApi(story, paths, config2, opts) {
7330
8283
  scoutRuntime = resolveRuntime({
7331
8284
  provider: planner.provider,
7332
8285
  modelId: scoutModelId,
7333
- apiKey,
8286
+ auth: auth2,
7334
8287
  anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
7335
8288
  });
7336
8289
  scoutSystemPrompt = composeScoutSystemPrompt({
@@ -7369,9 +8322,9 @@ async function emitViaApi(story, paths, config2, opts) {
7369
8322
  dispatchNewPlanApiPlannerEvent(apiUiCtx, e);
7370
8323
  });
7371
8324
  const decideOnLimit = interactive ? async (ctx) => {
7372
- printPlannerLimitExplanation(ctx);
8325
+ printPlannerLimitExplanation(ctx, auth2.mode);
7373
8326
  const ans = await select6({
7374
- message: "Continue this planning session? (Continuing sends more API requests and is billed.)",
8327
+ 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
8328
  choices: [
7376
8329
  { name: "Continue \u2014 extend limits for this run", value: "continue" },
7377
8330
  { name: "Stop \u2014 save partial plan and exit", value: "cancel" }
@@ -7387,6 +8340,7 @@ async function emitViaApi(story, paths, config2, opts) {
7387
8340
  runtime: draftRuntime,
7388
8341
  provider: planner.provider,
7389
8342
  modelId,
8343
+ auth: auth2,
7390
8344
  anthropicProviderSpecific,
7391
8345
  systemPrompt,
7392
8346
  userPrompt,
@@ -7452,7 +8406,7 @@ async function emitViaApi(story, paths, config2, opts) {
7452
8406
  metadataHeader: header,
7453
8407
  partial: !success2
7454
8408
  });
7455
- const relPlan = path24.relative(paths.root, planFile);
8409
+ const relPlan = path25.relative(paths.root, planFile);
7456
8410
  try {
7457
8411
  await writeLastRun(paths, {
7458
8412
  stats: result.stats,
@@ -7487,6 +8441,7 @@ async function emitViaApi(story, paths, config2, opts) {
7487
8441
  durationMs: result.validation?.durationMs
7488
8442
  },
7489
8443
  plannerRuntime: { kind: draftRuntime.kind, provider: planner.provider },
8444
+ authMode: describeAuth(auth2).mode,
7490
8445
  providerOptionsSnapshot: anthropicProviderSpecific ? { anthropic: anthropicProviderSpecific } : void 0
7491
8446
  });
7492
8447
  } catch {
@@ -7588,14 +8543,14 @@ async function emitViaApi(story, paths, config2, opts) {
7588
8543
  info("Note: file-read budget was reached; the model finalised without further reads.");
7589
8544
  }
7590
8545
  blank();
7591
- info(`next \u2192 open a new agent chat and attach only ${path24.basename(planFile)}`);
8546
+ info(`next \u2192 open a new agent chat and attach only ${path25.basename(planFile)}`);
7592
8547
  }
7593
8548
  async function emitCopyPrompt(story, paths, config2, clipboard) {
7594
8549
  const intakeContent = readFile(story.intakePath);
7595
8550
  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);
8551
+ const promptFile = path25.join(paths.squadDir, ".last-copy-prompt.md");
8552
+ fs35.writeFileSync(promptFile, composed, "utf8");
8553
+ const relPromptFile = path25.relative(paths.root, promptFile);
7599
8554
  const bytes = Buffer.byteLength(composed, "utf8");
7600
8555
  const estTokens = Math.round(bytes / 4);
7601
8556
  const clipResult = clipboard ? await copyToClipboard(composed) : { ok: false, reason: "clipboard disabled (--no-clipboard)" };
@@ -7653,7 +8608,7 @@ function printModelBanner(config2) {
7653
8608
  }
7654
8609
 
7655
8610
  // src/commands/status.ts
7656
- import fs33 from "fs";
8611
+ import fs36 from "fs";
7657
8612
  var kw = 28;
7658
8613
  function probeTrackerCredential(config2, overlay) {
7659
8614
  const { client, error } = clientFor(config2, overlay);
@@ -7679,7 +8634,7 @@ async function runStatus() {
7679
8634
  const scan = scanPlans(paths.plansDir);
7680
8635
  const storyCount = listStories(paths).length;
7681
8636
  const planCount = scan.usedNumbers.length;
7682
- const secrets = fs33.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
8637
+ const secrets = fs36.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
7683
8638
  const overlay = overlayTrackerEnv(secrets);
7684
8639
  banner();
7685
8640
  step("squad-kit status");
@@ -7695,12 +8650,7 @@ async function runStatus() {
7695
8650
  const model = modelFor(config2.planner.provider, "plan", config2.planner.modelOverride);
7696
8651
  const overrideNote = config2.planner.modelOverride?.[config2.planner.provider] ? " (override)" : "";
7697
8652
  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
- );
8653
+ kv("planner auth", buildPlannerAuthLine(config2.planner), kw);
7704
8654
  } else {
7705
8655
  kv("planner", "disabled (copy-paste flow)", kw);
7706
8656
  }
@@ -7711,6 +8661,15 @@ async function runStatus() {
7711
8661
  warning(`duplicate NN numbers: ${scan.duplicates.map(formatSequence).join(", ")}`);
7712
8662
  }
7713
8663
  }
8664
+ function buildPlannerAuthLine(planner) {
8665
+ try {
8666
+ const described = describeAuth(resolvePlannerAuthForCwd(planner.provider, planner));
8667
+ return `${described.mode} \xB7 ${described.credentialHint} (${authReasonText(described.reason, planner.provider)})`;
8668
+ } catch (err2) {
8669
+ if (!(err2 instanceof PlannerAuthUnavailableError)) throw err2;
8670
+ 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\``;
8671
+ }
8672
+ }
7714
8673
  function buildTrackerLine(config2, overlay) {
7715
8674
  if (config2.tracker.type === "none") return "none";
7716
8675
  let trackerLine = config2.tracker.type;
@@ -7733,13 +8692,13 @@ function buildTrackerLine(config2, overlay) {
7733
8692
  }
7734
8693
 
7735
8694
  // src/commands/list.ts
7736
- import path25 from "path";
8695
+ import path26 from "path";
7737
8696
  async function runList(opts) {
7738
8697
  const root = requireSquadRoot();
7739
8698
  const paths = buildPaths(root);
7740
8699
  const records = listStories(paths, { feature: opts.feature });
7741
8700
  const rows = records.map((r) => {
7742
- const planAbs = r.planFile ? path25.join(paths.plansDir, r.feature, r.planFile) : void 0;
8701
+ const planAbs = r.planFile ? path26.join(paths.plansDir, r.feature, r.planFile) : void 0;
7743
8702
  const meta = planAbs ? readPlanMetadata(planAbs) : void 0;
7744
8703
  return {
7745
8704
  feature: r.feature,
@@ -7778,8 +8737,8 @@ function stripAnsi(s) {
7778
8737
  }
7779
8738
 
7780
8739
  // src/commands/tracker-link.ts
7781
- import fs34 from "fs";
7782
- import path26 from "path";
8740
+ import fs37 from "fs";
8741
+ import path27 from "path";
7783
8742
  import { input as input4, select as select7 } from "@inquirer/prompts";
7784
8743
  async function runTrackerLink(storyPath, trackerId, opts = {}) {
7785
8744
  const root = requireSquadRoot();
@@ -7826,17 +8785,17 @@ async function runTrackerLink(storyPath, trackerId, opts = {}) {
7826
8785
  `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
8786
  );
7828
8787
  }
7829
- const resolved = path26.resolve(resolvedStoryPath);
7830
- const intakeFile = resolved.endsWith("intake.md") ? resolved : path26.join(resolved, "intake.md");
7831
- if (!fs34.existsSync(intakeFile)) {
8788
+ const resolved = path27.resolve(resolvedStoryPath);
8789
+ const intakeFile = resolved.endsWith("intake.md") ? resolved : path27.join(resolved, "intake.md");
8790
+ if (!fs37.existsSync(intakeFile)) {
7832
8791
  throw new Error(
7833
8792
  `intake.md not found at ${intakeFile}. Run \`squad list\` to see valid paths, or \`squad new-story\` to create a story first.`
7834
8793
  );
7835
8794
  }
7836
- const content = fs34.readFileSync(intakeFile, "utf8");
8795
+ const content = fs37.readFileSync(intakeFile, "utf8");
7837
8796
  const updated = upsertTrackerId(content, resolvedTrackerId);
7838
- fs34.writeFileSync(intakeFile, updated, "utf8");
7839
- success(`Linked tracker id ${resolvedTrackerId} \u2192 ${path26.relative(root, intakeFile)}`);
8797
+ fs37.writeFileSync(intakeFile, updated, "utf8");
8798
+ success(`Linked tracker id ${resolvedTrackerId} \u2192 ${path27.relative(root, intakeFile)}`);
7840
8799
  blank();
7841
8800
  step("Next:");
7842
8801
  info("1) Review the intake \u2014 the Source block at the top now carries the tracker id.");
@@ -7854,8 +8813,8 @@ function upsertTrackerId(content, id) {
7854
8813
  }
7855
8814
 
7856
8815
  // src/commands/doctor-engine.ts
7857
- import fs35 from "fs";
7858
- import path27 from "path";
8816
+ import fs38 from "fs";
8817
+ import path28 from "path";
7859
8818
  async function gatherContext(paths) {
7860
8819
  const ctx = { hasLegacyPromptsDir: false };
7861
8820
  try {
@@ -7864,29 +8823,30 @@ async function gatherContext(paths) {
7864
8823
  ctx.configError = err2;
7865
8824
  }
7866
8825
  try {
7867
- ctx.secrets = fs35.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
8826
+ ctx.secrets = fs38.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
7868
8827
  } catch (err2) {
7869
8828
  ctx.secretsError = err2;
7870
8829
  }
7871
- ctx.hasLegacyPromptsDir = fs35.existsSync(paths.promptsDir);
8830
+ ctx.hasLegacyPromptsDir = fs38.existsSync(paths.promptsDir);
7872
8831
  return ctx;
7873
8832
  }
7874
8833
  function gitignoreHasManagedBlock(repoRoot) {
7875
- const gitignore = path27.join(repoRoot, ".gitignore");
7876
- return fs35.existsSync(gitignore) && fs35.readFileSync(gitignore, "utf8").includes(".squad/secrets.yaml");
8834
+ const gitignore = path28.join(repoRoot, ".gitignore");
8835
+ return fs38.existsSync(gitignore) && fs38.readFileSync(gitignore, "utf8").includes(".squad/secrets.yaml");
7877
8836
  }
7878
8837
  function gitignoreHasTrashPattern(repoRoot) {
7879
- const gitignore = path27.join(repoRoot, ".gitignore");
7880
- return fs35.existsSync(gitignore) && fs35.readFileSync(gitignore, "utf8").includes(SQUAD_TRASH_PATTERN);
8838
+ const gitignore = path28.join(repoRoot, ".gitignore");
8839
+ return fs38.existsSync(gitignore) && fs38.readFileSync(gitignore, "utf8").includes(SQUAD_TRASH_PATTERN);
7881
8840
  }
8841
+ var DOCTOR_AUTH_PROBE_TIMEOUT_MS = 25e3;
7882
8842
  async function checkDirStructure(paths, _ctx, fix) {
7883
- const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !fs35.existsSync(p));
8843
+ const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !fs38.existsSync(p));
7884
8844
  if (need.length === 0) {
7885
8845
  return { id: "dirs", name: ".squad/ directory structure", status: "ok" };
7886
8846
  }
7887
8847
  if (fix) {
7888
8848
  for (const p of need) {
7889
- fs35.mkdirSync(p, { recursive: true });
8849
+ fs38.mkdirSync(p, { recursive: true });
7890
8850
  }
7891
8851
  return { id: "dirs", name: ".squad/ directory structure", status: "ok", detail: "repaired" };
7892
8852
  }
@@ -7894,7 +8854,7 @@ async function checkDirStructure(paths, _ctx, fix) {
7894
8854
  id: "dirs",
7895
8855
  name: ".squad/ directory structure",
7896
8856
  status: "warn",
7897
- detail: `missing: ${need.map((p) => path27.relative(paths.root, p)).join(", ")}`,
8857
+ detail: `missing: ${need.map((p) => path28.relative(paths.root, p)).join(", ")}`,
7898
8858
  fixable: true,
7899
8859
  fixHint: "squad doctor --fix"
7900
8860
  };
@@ -7949,15 +8909,15 @@ async function checkSecretsPermissions(paths, _ctx, fix) {
7949
8909
  if (process.platform === "win32") {
7950
8910
  return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "skip", detail: "Windows" };
7951
8911
  }
7952
- if (!fs35.existsSync(paths.secretsFile)) {
8912
+ if (!fs38.existsSync(paths.secretsFile)) {
7953
8913
  return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "not present" };
7954
8914
  }
7955
- const mode = fs35.statSync(paths.secretsFile).mode & 511;
8915
+ const mode = fs38.statSync(paths.secretsFile).mode & 511;
7956
8916
  if (mode === 384) {
7957
8917
  return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok" };
7958
8918
  }
7959
8919
  if (fix) {
7960
- fs35.chmodSync(paths.secretsFile, 384);
8920
+ fs38.chmodSync(paths.secretsFile, 384);
7961
8921
  return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "repaired" };
7962
8922
  }
7963
8923
  return {
@@ -7978,7 +8938,7 @@ async function checkSecretsParseable(_paths, ctx) {
7978
8938
  detail: ctx.secretsError.message
7979
8939
  };
7980
8940
  }
7981
- if (!fs35.existsSync(_paths.secretsFile)) {
8941
+ if (!fs38.existsSync(_paths.secretsFile)) {
7982
8942
  return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok", detail: "not present" };
7983
8943
  }
7984
8944
  return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok" };
@@ -8040,36 +9000,136 @@ async function checkPlannerConfig(_paths, ctx) {
8040
9000
  }
8041
9001
  return { id: "planner-config", name: "planner configuration", status: "ok" };
8042
9002
  }
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" };
9003
+ var LOGIN_FIX_HINT = "Run `squad auth login` to sign in with your Claude account.";
9004
+ 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.";
9005
+ function plannerEnabled(ctx) {
9006
+ return ctx.config?.planner?.enabled === true;
9007
+ }
9008
+ function authFor(paths, ctx, provider) {
9009
+ const memo = ctx.authMemo ??= {};
9010
+ if (!memo.resolution) {
9011
+ try {
9012
+ memo.resolution = { ok: true, auth: resolvePlannerAuthForPaths(paths, provider, ctx.config?.planner) };
9013
+ } catch (err2) {
9014
+ memo.resolution = { ok: false, error: err2 };
9015
+ }
9016
+ }
9017
+ return memo.resolution;
9018
+ }
9019
+ async function probeAuthOnce(ctx, auth2) {
9020
+ const memo = ctx.authMemo ??= {};
9021
+ if (!memo.probe) {
9022
+ memo.probe = skipExternalProbesInAutomation() ? { skipped: true } : await probeClaudeAuth(auth2, { timeoutMs: DOCTOR_AUTH_PROBE_TIMEOUT_MS });
9023
+ }
9024
+ return memo.probe;
9025
+ }
9026
+ function probeWasSkipped(outcome) {
9027
+ return "skipped" in outcome;
9028
+ }
9029
+ async function checkPlannerAuthMode(paths, ctx, _fix) {
9030
+ const id = "planner-auth-mode";
9031
+ const name = "planner auth mode";
9032
+ if (!plannerEnabled(ctx) || !ctx.config?.planner) {
9033
+ return { id, name, status: "skip", detail: "planner disabled" };
9034
+ }
9035
+ const planner = ctx.config.planner;
9036
+ const provider = planner.provider;
9037
+ if (provider !== "anthropic") {
9038
+ return { id, name, status: "skip", detail: `api key only for ${provider}` };
9039
+ }
9040
+ const resolution = authFor(paths, ctx, provider);
9041
+ if (!resolution.ok) {
9042
+ return { id, name, status: "fail", detail: resolution.error.message, fixHint: LOGIN_FIX_HINT };
9043
+ }
9044
+ const auth2 = resolution.auth;
9045
+ const described = describeAuth(auth2);
9046
+ let detail = `${described.mode} (${authReasonText(described.reason, provider)}) \u2192 ${described.credentialHint}`;
9047
+ if (auth2.mode === "subscription") {
9048
+ const ignoredKey = resolveProviderKeyForPaths(paths, provider);
9049
+ if (ignoredKey) detail += ` \xB7 ignoring the API key from ${ignoredKey.detail}`;
9050
+ }
9051
+ const implicitApiKey = (planner.auth?.anthropic ?? "auto") === "auto" && auth2.reason === "auto-fallback-api-key";
9052
+ if (implicitApiKey) {
9053
+ return {
9054
+ id,
9055
+ name,
9056
+ status: "ok",
9057
+ detail,
9058
+ 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."
9059
+ };
9060
+ }
9061
+ return { id, name, status: "ok", detail };
9062
+ }
9063
+ function describeProbeAccount(probe) {
9064
+ const bits = [];
9065
+ const email = probe.account?.email;
9066
+ const plan = probe.account?.subscriptionType;
9067
+ bits.push(email ? `signed in as ${email}${plan ? ` (${plan})` : ""}` : "Claude login verified");
9068
+ if (probe.account?.organization) bits.push(probe.account.organization);
9069
+ if (probe.apiKeySource) bits.push(`apiKeySource=${probe.apiKeySource}`);
9070
+ return bits.join(" \xB7 ");
9071
+ }
9072
+ async function checkPlannerCredential(paths, ctx) {
9073
+ const id = "planner-cred";
9074
+ const name = "planner credential resolves";
9075
+ if (!plannerEnabled(ctx) || !ctx.config?.planner) {
9076
+ return { id, name, status: "skip", detail: "planner disabled" };
8046
9077
  }
8047
9078
  const provider = ctx.config.planner.provider;
8048
- const resolved = resolveProviderKey(provider);
8049
- if (!resolved) {
9079
+ const resolution = authFor(paths, ctx, provider);
9080
+ if (!resolution.ok) {
8050
9081
  const envVar = providerEnvVar(provider);
8051
9082
  return {
8052
- id: "planner-cred",
8053
- name: "planner credential resolves",
9083
+ id,
9084
+ name,
8054
9085
  status: "fail",
8055
9086
  detail: `no API key found (${envVar} or .squad/secrets.yaml)`,
8056
- fixHint: `Set ${envVar} or save a key via 'squad init' interactive setup`
9087
+ fixHint: resolution.error.message
8057
9088
  };
8058
9089
  }
8059
- return {
8060
- id: "planner-cred",
8061
- name: "planner credential resolves",
8062
- status: "ok",
8063
- detail: `source=${resolved.source} (${resolved.detail})`
8064
- };
9090
+ const auth2 = resolution.auth;
9091
+ if (auth2.mode === "api-key") {
9092
+ return { id, name, status: "ok", detail: `source=${auth2.source} (${auth2.detail})` };
9093
+ }
9094
+ const credentialHint = describeAuth(auth2).credentialHint;
9095
+ const probe = await probeAuthOnce(ctx, auth2);
9096
+ if (probeWasSkipped(probe)) {
9097
+ return credentialPresent(auth2) ? { id, name, status: "ok", detail: `${credentialHint} \xB7 offline check only` } : {
9098
+ id,
9099
+ name,
9100
+ status: "warn",
9101
+ detail: "no Claude login detected \xB7 offline check only",
9102
+ fixHint: LOGIN_FIX_HINT
9103
+ };
9104
+ }
9105
+ if (probe.ok) {
9106
+ return { id, name, status: "ok", detail: describeProbeAccount(probe) };
9107
+ }
9108
+ switch (probe.kind) {
9109
+ case "not-logged-in":
9110
+ case "expired":
9111
+ return { id, name, status: "fail", detail: probe.detail, fixHint: LOGIN_FIX_HINT };
9112
+ case "org-not-allowed":
9113
+ return { id, name, status: "fail", detail: probe.detail, fixHint: ORG_NOT_ALLOWED_FIX_HINT };
9114
+ case "no-binary":
9115
+ return { id, name, status: "warn", detail: probe.detail, fixHint: MISSING_CLAUDE_BINARY_MESSAGE };
9116
+ default:
9117
+ return credentialPresent(auth2) ? { id, name, status: "warn", detail: `${probe.kind}: ${probe.detail}` } : {
9118
+ id,
9119
+ name,
9120
+ status: "fail",
9121
+ detail: `no Claude login detected (live check: ${probe.kind})`,
9122
+ fixHint: LOGIN_FIX_HINT
9123
+ };
9124
+ }
8065
9125
  }
8066
- async function checkPlannerModel(_paths, ctx) {
8067
- if (!ctx.config || ctx.config.planner?.enabled !== true) {
9126
+ async function checkPlannerModel(paths, ctx) {
9127
+ if (!plannerEnabled(ctx) || !ctx.config?.planner) {
8068
9128
  return { id: "planner-model", name: "planner model resolves at provider", status: "skip", detail: "planner disabled" };
8069
9129
  }
8070
9130
  const provider = ctx.config.planner.provider;
8071
- const cred = resolveProviderKey(provider);
8072
- if (!cred) {
9131
+ const resolution = authFor(paths, ctx, provider);
9132
+ if (!resolution.ok) {
8073
9133
  return {
8074
9134
  id: "planner-model",
8075
9135
  name: "planner model resolves at provider",
@@ -8077,9 +9137,18 @@ async function checkPlannerModel(_paths, ctx) {
8077
9137
  detail: "no credential"
8078
9138
  };
8079
9139
  }
9140
+ const auth2 = resolution.auth;
9141
+ if (auth2.mode === "subscription") {
9142
+ return {
9143
+ id: "planner-model",
9144
+ name: "planner model resolves at provider",
9145
+ status: "skip",
9146
+ detail: "model list needs an API key; skipped in subscription mode"
9147
+ };
9148
+ }
8080
9149
  const model = modelFor(provider, "plan", ctx.config.planner.modelOverride);
8081
9150
  try {
8082
- const listed = await fetchProviderModelIds(provider, cred.value);
9151
+ const listed = await fetchProviderModelIds(provider, auth2.key);
8083
9152
  if (!listed.ok) {
8084
9153
  const st = listed.status;
8085
9154
  if (st === 401 || st === 403) {
@@ -8121,7 +9190,7 @@ async function checkPlannerModel(_paths, ctx) {
8121
9190
  };
8122
9191
  }
8123
9192
  }
8124
- async function checkPlannerTierAwareness(_paths, ctx) {
9193
+ async function checkPlannerTierAwareness(paths, ctx) {
8125
9194
  const id = "planner-tier";
8126
9195
  const name = "planner tier vs. model";
8127
9196
  if (!ctx.config || ctx.config.planner?.enabled !== true) {
@@ -8131,6 +9200,10 @@ async function checkPlannerTierAwareness(_paths, ctx) {
8131
9200
  if (provider !== "anthropic") {
8132
9201
  return { id, name, status: "skip", detail: `not applicable for ${provider}` };
8133
9202
  }
9203
+ const tierAuth = authFor(paths, ctx, provider);
9204
+ if (tierAuth.ok && tierAuth.auth.mode === "subscription") {
9205
+ return { id, name, status: "skip", detail: "not applicable on subscription auth (no API rate tier)" };
9206
+ }
8134
9207
  const planModel = modelFor(provider, "plan", ctx.config.planner.modelOverride);
8135
9208
  if (!/opus/i.test(planModel)) {
8136
9209
  return { id, name, status: "ok", detail: `${planModel} is comfortably under Tier 1` };
@@ -8259,18 +9332,57 @@ async function checkPlannerAnthropicRuntimeModelFit(_paths, ctx) {
8259
9332
  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
9333
  };
8261
9334
  }
8262
- async function checkAgentSdkBinaryPresent(_paths, ctx) {
9335
+ async function checkPlannerAuthRuntimeFit(paths, ctx) {
9336
+ const id = "planner-auth-runtime-fit";
9337
+ const name = "planner auth vs. runtime";
9338
+ if (!plannerEnabled(ctx) || !ctx.config?.planner) {
9339
+ return { id, name, status: "skip", detail: "planner disabled" };
9340
+ }
9341
+ const planner = ctx.config.planner;
9342
+ const provider = planner.provider;
9343
+ const configuredMode = planner.auth?.anthropic ?? "auto";
9344
+ if (provider !== "anthropic") {
9345
+ if (configuredMode === "subscription") {
9346
+ return {
9347
+ id,
9348
+ name,
9349
+ status: "fail",
9350
+ detail: `planner.auth.anthropic: subscription with planner.provider: ${provider}`,
9351
+ fixHint: plannerAuthRuntimeMismatchMessage(provider)
9352
+ };
9353
+ }
9354
+ return { id, name, status: "skip", detail: `api key only for ${provider}` };
9355
+ }
9356
+ const runtime = runtimeNameFor(provider, planner);
9357
+ const resolution = authFor(paths, ctx, provider);
9358
+ if (!resolution.ok) {
9359
+ return { id, name, status: "skip", detail: "auth unresolved" };
9360
+ }
9361
+ const mode = resolution.auth.mode;
9362
+ if (mode === "subscription" && runtime !== "agent-sdk") {
9363
+ return {
9364
+ id,
9365
+ name,
9366
+ status: "fail",
9367
+ detail: `subscription auth with planner.runtime.anthropic: ${runtime}`,
9368
+ fixHint: plannerAuthRuntimeMismatchMessage(provider)
9369
+ };
9370
+ }
9371
+ return { id, name, status: "ok", detail: `${mode} + ${runtime} runtime` };
9372
+ }
9373
+ async function checkAgentSdkBinaryPresent(paths, ctx) {
8263
9374
  const id = "agent-sdk-binary-present";
8264
9375
  const name = "Anthropic Agent SDK install";
8265
9376
  if (!ctx.config?.planner?.enabled || ctx.config.planner.provider !== "anthropic") {
8266
9377
  return { id, name, status: "skip", detail: "not applicable" };
8267
9378
  }
8268
- if ((ctx.config.planner.runtime?.anthropic ?? "agent-sdk") === "vercel") {
9379
+ const resolution = authFor(paths, ctx, "anthropic");
9380
+ const subscription = resolution.ok && resolution.auth.mode === "subscription";
9381
+ if ((ctx.config.planner.runtime?.anthropic ?? "agent-sdk") === "vercel" && !subscription) {
8269
9382
  return { id, name, status: "skip", detail: "vercel runtime selected" };
8270
9383
  }
8271
9384
  try {
8272
9385
  await import("@anthropic-ai/claude-agent-sdk");
8273
- return { id, name, status: "ok", detail: "package resolves" };
8274
9386
  } catch (e) {
8275
9387
  return {
8276
9388
  id,
@@ -8280,6 +9392,22 @@ async function checkAgentSdkBinaryPresent(_paths, ctx) {
8280
9392
  fixHint: "Run pnpm install / npm install in the squad-kit package so the Agent SDK (and its platform binary) is present."
8281
9393
  };
8282
9394
  }
9395
+ const binary = resolveClaudeBinary();
9396
+ if (!binary) {
9397
+ return {
9398
+ id,
9399
+ name,
9400
+ status: subscription ? "fail" : "warn",
9401
+ detail: "package resolves; no `claude` executable found",
9402
+ fixHint: MISSING_CLAUDE_BINARY_MESSAGE
9403
+ };
9404
+ }
9405
+ return {
9406
+ id,
9407
+ name,
9408
+ status: "ok",
9409
+ detail: `package + ${binary.source === "bundled" ? "bundled" : "PATH"} binary`
9410
+ };
8283
9411
  }
8284
9412
  async function checkTrackerConfig(_paths, ctx) {
8285
9413
  if (!ctx.config) {
@@ -8418,11 +9546,13 @@ async function runAllChecks(paths, ctx, fix) {
8418
9546
  await add(".squad/secrets.yaml parseable", () => checkSecretsParseable(paths, ctx));
8419
9547
  await add("legacy .squad/prompts/ directory", () => checkLegacyPrompts(paths, ctx));
8420
9548
  await add("planner configuration", () => checkPlannerConfig(paths, ctx));
9549
+ await add("planner auth mode", () => checkPlannerAuthMode(paths, ctx, fix));
8421
9550
  await add("planner credential resolves", () => checkPlannerCredential(paths, ctx));
8422
9551
  await add("planner model resolves at provider", () => checkPlannerModel(paths, ctx));
8423
9552
  await add("planner tier vs. model", () => checkPlannerTierAwareness(paths, ctx));
8424
9553
  await add("planner cache effectiveness", () => checkPlannerCache(paths, ctx));
8425
9554
  await add("planner runtime (resolved)", () => checkPlannerRuntimeInfo(paths, ctx));
9555
+ await add("planner auth vs. runtime", () => checkPlannerAuthRuntimeFit(paths, ctx));
8426
9556
  await add("Anthropic Opus 4.7+ vs Vercel runtime", () => checkPlannerAnthropicRuntimeModelFit(paths, ctx));
8427
9557
  await add("Anthropic Agent SDK install", () => checkAgentSdkBinaryPresent(paths, ctx));
8428
9558
  await add("tracker configuration", () => checkTrackerConfig(paths, ctx));
@@ -8532,23 +9662,23 @@ async function runDoctor(opts) {
8532
9662
  }
8533
9663
 
8534
9664
  // src/commands/migrate.ts
8535
- import fs36 from "fs";
8536
- import path28 from "path";
8537
- import { confirm as confirm7 } from "@inquirer/prompts";
9665
+ import fs39 from "fs";
9666
+ import path29 from "path";
9667
+ import { confirm as confirm8 } from "@inquirer/prompts";
8538
9668
  var LEGACY_PROMPTS_MIGRATION = {
8539
9669
  id: "legacy-prompts",
8540
9670
  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)`,
9671
+ isApplied: (paths) => !fs39.existsSync(paths.promptsDir),
9672
+ describe: (paths) => `delete ${path29.relative(paths.root, paths.promptsDir)} (prompts are now bundled in squad-kit)`,
8543
9673
  apply: (paths) => {
8544
9674
  const target = paths.promptsDir;
8545
- if (!target.includes(path28.sep + ".squad" + path28.sep + "prompts")) {
9675
+ if (!target.includes(path29.sep + ".squad" + path29.sep + "prompts")) {
8546
9676
  throw new Error(
8547
9677
  `Refusing to delete suspicious path: ${target}. This is a safety check \u2014 run \`squad doctor\` if .squad/ paths look wrong.`
8548
9678
  );
8549
9679
  }
8550
- if (fs36.existsSync(target)) {
8551
- fs36.rmSync(target, { recursive: true, force: true });
9680
+ if (fs39.existsSync(target)) {
9681
+ fs39.rmSync(target, { recursive: true, force: true });
8552
9682
  }
8553
9683
  }
8554
9684
  };
@@ -8556,8 +9686,8 @@ var GITIGNORE_MIGRATION = {
8556
9686
  id: "gitignore",
8557
9687
  name: "Ensure .gitignore managed block",
8558
9688
  isApplied: (paths) => {
8559
- const f = path28.join(paths.root, ".gitignore");
8560
- return fs36.existsSync(f) && fs36.readFileSync(f, "utf8").includes(".squad/secrets.yaml");
9689
+ const f = path29.join(paths.root, ".gitignore");
9690
+ return fs39.existsSync(f) && fs39.readFileSync(f, "utf8").includes(".squad/secrets.yaml");
8561
9691
  },
8562
9692
  describe: () => "add squad-kit managed block to .gitignore",
8563
9693
  apply: (paths) => {
@@ -8569,23 +9699,23 @@ var SECRETS_PERMS_MIGRATION = {
8569
9699
  name: "Tighten .squad/secrets.yaml permissions",
8570
9700
  isApplied: (paths) => {
8571
9701
  if (process.platform === "win32") return true;
8572
- if (!fs36.existsSync(paths.secretsFile)) return true;
8573
- const st = fs36.statSync(paths.secretsFile);
9702
+ if (!fs39.existsSync(paths.secretsFile)) return true;
9703
+ const st = fs39.statSync(paths.secretsFile);
8574
9704
  return (st.mode & 511) === 384;
8575
9705
  },
8576
- describe: (paths) => `chmod 0600 ${path28.relative(paths.root, paths.secretsFile)}`,
9706
+ describe: (paths) => `chmod 0600 ${path29.relative(paths.root, paths.secretsFile)}`,
8577
9707
  apply: (paths) => {
8578
9708
  if (process.platform === "win32") return;
8579
- if (fs36.existsSync(paths.secretsFile)) fs36.chmodSync(paths.secretsFile, 384);
9709
+ if (fs39.existsSync(paths.secretsFile)) fs39.chmodSync(paths.secretsFile, 384);
8580
9710
  }
8581
9711
  };
8582
9712
  var PLANNER_BUDGET_DEFAULTS_MIGRATION = {
8583
9713
  id: "planner-budget-defaults",
8584
9714
  name: "Backfill planner.budget defaults in config.yaml",
8585
9715
  isApplied: (paths) => {
8586
- if (!fs36.existsSync(paths.configFile)) return true;
9716
+ if (!fs39.existsSync(paths.configFile)) return true;
8587
9717
  try {
8588
- const raw = fs36.readFileSync(paths.configFile, "utf8");
9718
+ const raw = fs39.readFileSync(paths.configFile, "utf8");
8589
9719
  if (!/^\s*planner:/m.test(raw)) return true;
8590
9720
  if (/^\s*budget:/m.test(raw)) return true;
8591
9721
  const cfg = loadConfig(paths.configFile);
@@ -8604,9 +9734,9 @@ var CONFIG_NORMALIZE_MIGRATION = {
8604
9734
  id: "config-normalize",
8605
9735
  name: "Normalise .squad/config.yaml formatting",
8606
9736
  isApplied: (paths) => {
8607
- if (!fs36.existsSync(paths.configFile)) return true;
9737
+ if (!fs39.existsSync(paths.configFile)) return true;
8608
9738
  try {
8609
- const raw = fs36.readFileSync(paths.configFile, "utf8");
9739
+ const raw = fs39.readFileSync(paths.configFile, "utf8");
8610
9740
  const cfg = loadConfig(paths.configFile);
8611
9741
  return raw.trim() === serializeConfig(cfg).trim();
8612
9742
  } catch {
@@ -8615,7 +9745,7 @@ var CONFIG_NORMALIZE_MIGRATION = {
8615
9745
  },
8616
9746
  describe: () => "rewrite config.yaml with canonical YAML formatting (drops comments)",
8617
9747
  apply: (paths) => {
8618
- if (!fs36.existsSync(paths.configFile)) return;
9748
+ if (!fs39.existsSync(paths.configFile)) return;
8619
9749
  const cfg = loadConfig(paths.configFile);
8620
9750
  saveConfig(paths.configFile, cfg);
8621
9751
  }
@@ -8663,7 +9793,7 @@ async function runMigrate(opts) {
8663
9793
  );
8664
9794
  process.exit(1);
8665
9795
  }
8666
- const go = await confirm7({ message: `Apply ${pending.length} migration(s)?`, default: true });
9796
+ const go = await confirm8({ message: `Apply ${pending.length} migration(s)?`, default: true });
8667
9797
  if (!go) {
8668
9798
  info("Cancelled. No changes applied.");
8669
9799
  return;
@@ -8691,13 +9821,13 @@ async function runMigrate(opts) {
8691
9821
  }
8692
9822
 
8693
9823
  // src/commands/upgrade.ts
8694
- import { spawn as spawn2 } from "child_process";
9824
+ import { spawn as spawn3 } from "child_process";
8695
9825
  import process3 from "process";
8696
- import { confirm as confirm8 } from "@inquirer/prompts";
9826
+ import { confirm as confirm9 } from "@inquirer/prompts";
8697
9827
 
8698
9828
  // src/core/package-info.ts
8699
- import fs37 from "fs";
8700
- import path29 from "path";
9829
+ import fs40 from "fs";
9830
+ import path30 from "path";
8701
9831
  function changelogBlobUrl(repositoryUrl, version) {
8702
9832
  let base = repositoryUrl;
8703
9833
  if (!base) base = "https://github.com/AzmSquad/squad-kit";
@@ -8708,9 +9838,9 @@ function changelogBlobUrl(repositoryUrl, version) {
8708
9838
  }
8709
9839
  function readInstalledPackage() {
8710
9840
  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"));
9841
+ const pkgFile = path30.join(root, "package.json");
9842
+ const pkg = JSON.parse(fs40.readFileSync(pkgFile, "utf8"));
9843
+ const isDevInstall = fs40.existsSync(path30.join(root, "src"));
8714
9844
  return {
8715
9845
  name: pkg.name,
8716
9846
  version: pkg.version,
@@ -8750,7 +9880,7 @@ function isNewer(a, b) {
8750
9880
  }
8751
9881
 
8752
9882
  // src/core/package-manager.ts
8753
- import path30 from "path";
9883
+ import path31 from "path";
8754
9884
  import process2 from "process";
8755
9885
  function detectPackageManager(installRoot) {
8756
9886
  const ua = process2.env.npm_config_user_agent;
@@ -8761,11 +9891,11 @@ function detectPackageManager(installRoot) {
8761
9891
  if (ua.startsWith("npm/")) return { pm: "npm", reason: "npm_config_user_agent" };
8762
9892
  }
8763
9893
  const root = installRoot.toLowerCase();
8764
- if (root.includes(`${path30.sep}pnpm${path30.sep}`) || root.includes(`${path30.sep}.pnpm${path30.sep}`))
9894
+ if (root.includes(`${path31.sep}pnpm${path31.sep}`) || root.includes(`${path31.sep}.pnpm${path31.sep}`))
8765
9895
  return { pm: "pnpm", reason: `install path contains pnpm` };
8766
- if (root.includes(`${path30.sep}.yarn${path30.sep}`) || root.includes(`${path30.sep}yarn${path30.sep}`))
9896
+ if (root.includes(`${path31.sep}.yarn${path31.sep}`) || root.includes(`${path31.sep}yarn${path31.sep}`))
8767
9897
  return { pm: "yarn", reason: `install path contains yarn` };
8768
- if (root.includes(`${path30.sep}.bun${path30.sep}`) || root.includes(`${path30.sep}bun${path30.sep}`))
9898
+ if (root.includes(`${path31.sep}.bun${path31.sep}`) || root.includes(`${path31.sep}bun${path31.sep}`))
8769
9899
  return { pm: "bun", reason: `install path contains bun` };
8770
9900
  return { pm: "npm", reason: "default fallback" };
8771
9901
  }
@@ -8838,7 +9968,7 @@ async function runUpgrade(opts) {
8838
9968
  );
8839
9969
  process3.exit(1);
8840
9970
  }
8841
- const go = await confirm8({ message: `Run "${cmd} ${args.join(" ")}"?`, default: true });
9971
+ const go = await confirm9({ message: `Run "${cmd} ${args.join(" ")}"?`, default: true });
8842
9972
  if (!go) {
8843
9973
  info("Cancelled. No changes applied.");
8844
9974
  return;
@@ -8859,7 +9989,7 @@ async function runUpgrade(opts) {
8859
9989
  }
8860
9990
  function spawnInherit(cmd, args) {
8861
9991
  return new Promise((resolve2, reject) => {
8862
- const child = spawn2(cmd, args, { stdio: "inherit", shell: process3.platform === "win32" });
9992
+ const child = spawn3(cmd, args, { stdio: "inherit", shell: process3.platform === "win32" });
8863
9993
  child.on("exit", (code) => resolve2(code ?? 0));
8864
9994
  child.on("error", (err2) => reject(err2));
8865
9995
  });
@@ -8878,14 +10008,14 @@ import { cors } from "hono/cors";
8878
10008
  import { secureHeaders } from "hono/secure-headers";
8879
10009
 
8880
10010
  // src/console/api/stories.ts
8881
- import fs38 from "fs";
10011
+ import fs41 from "fs";
8882
10012
  import { z as z6 } from "zod";
8883
10013
 
8884
10014
  // src/utils/path-safety.ts
8885
- import path31 from "path";
10015
+ import path32 from "path";
8886
10016
  function isInside(abs, parent) {
8887
- const a = path31.resolve(abs) + path31.sep;
8888
- const p = path31.resolve(parent) + path31.sep;
10017
+ const a = path32.resolve(abs) + path32.sep;
10018
+ const p = path32.resolve(parent) + path32.sep;
8889
10019
  return a.startsWith(p);
8890
10020
  }
8891
10021
 
@@ -8925,7 +10055,7 @@ function mountStoriesApi(app, opts) {
8925
10055
  if (!isInside(hit.intakePath, opts.paths.storiesDir)) return c.json({ error: "forbidden" }, 403);
8926
10056
  let intakeContent = "";
8927
10057
  try {
8928
- intakeContent = fs38.readFileSync(hit.intakePath, "utf8");
10058
+ intakeContent = fs41.readFileSync(hit.intakePath, "utf8");
8929
10059
  } catch {
8930
10060
  }
8931
10061
  return c.json({
@@ -8969,7 +10099,7 @@ function mountStoriesApi(app, opts) {
8969
10099
  const hit = stories.find((s) => s.id === id);
8970
10100
  if (!hit) return c.json({ error: "not_found" }, 404);
8971
10101
  if (!isInside(hit.intakePath, opts.paths.storiesDir)) return c.json({ error: "forbidden" }, 403);
8972
- fs38.writeFileSync(hit.intakePath, body.data.intakeContent, "utf8");
10102
+ fs41.writeFileSync(hit.intakePath, body.data.intakeContent, "utf8");
8973
10103
  return c.json({ ok: true });
8974
10104
  });
8975
10105
  app.delete("/api/stories/:feature/:id", (c) => {
@@ -8986,8 +10116,8 @@ function mountStoriesApi(app, opts) {
8986
10116
  }
8987
10117
 
8988
10118
  // src/console/api/plans.ts
8989
- import fs39 from "fs";
8990
- import path32 from "path";
10119
+ import fs42 from "fs";
10120
+ import path33 from "path";
8991
10121
  import { z as z7 } from "zod";
8992
10122
  import { diffLines } from "diff";
8993
10123
  var DiffQuery = z7.object({
@@ -9002,11 +10132,11 @@ function mountPlansApi(app, opts) {
9002
10132
  app.get("/api/plans", (c) => {
9003
10133
  const out = [];
9004
10134
  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)) {
10135
+ const featureDir = path33.join(opts.paths.plansDir, feature);
10136
+ if (!fs42.existsSync(featureDir)) continue;
10137
+ for (const entry of fs42.readdirSync(featureDir)) {
9008
10138
  if (!entry.endsWith(".md") || entry === "00-index.md") continue;
9009
- const abs = path32.join(featureDir, entry);
10139
+ const abs = path33.join(featureDir, entry);
9010
10140
  out.push({ feature, planFile: entry, metadata: readPlanMetadata(abs) });
9011
10141
  }
9012
10142
  }
@@ -9015,10 +10145,10 @@ function mountPlansApi(app, opts) {
9015
10145
  app.get("/api/plans/:feature/:planFile", (c) => {
9016
10146
  const { feature, planFile } = c.req.param();
9017
10147
  if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
9018
- const abs = path32.resolve(path32.join(opts.paths.plansDir, feature, planFile));
10148
+ const abs = path33.resolve(path33.join(opts.paths.plansDir, feature, planFile));
9019
10149
  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");
10150
+ if (!fs42.existsSync(abs)) return c.json({ error: "not_found" }, 404);
10151
+ const content = fs42.readFileSync(abs, "utf8");
9022
10152
  return c.json({
9023
10153
  feature,
9024
10154
  planFile,
@@ -9036,15 +10166,15 @@ function mountPlansApi(app, opts) {
9036
10166
  if (!parse.success) return c.json({ error: "invalid_query", issues: parse.error.issues }, 400);
9037
10167
  const { feature, a, b } = parse.data;
9038
10168
  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);
10169
+ const featureDir = path33.join(opts.paths.plansDir, feature);
10170
+ const aAbs = path33.join(featureDir, a);
10171
+ const bAbs = path33.join(featureDir, b);
9042
10172
  if (!isInside(aAbs, opts.paths.plansDir) || !isInside(bAbs, opts.paths.plansDir)) {
9043
10173
  return c.json({ error: "forbidden" }, 403);
9044
10174
  }
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");
10175
+ if (!fs42.existsSync(aAbs) || !fs42.existsSync(bAbs)) return c.json({ error: "not_found" }, 404);
10176
+ const aText = fs42.readFileSync(aAbs, "utf8");
10177
+ const bText = fs42.readFileSync(bAbs, "utf8");
9048
10178
  const changes = diffLines(aText, bText);
9049
10179
  return c.json({ feature, a, b, changes });
9050
10180
  });
@@ -9052,16 +10182,16 @@ function mountPlansApi(app, opts) {
9052
10182
  const trash = c.req.query("trash") === "1";
9053
10183
  const { feature, planFile } = c.req.param();
9054
10184
  if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
9055
- const abs = path32.join(opts.paths.plansDir, feature, planFile);
10185
+ const abs = path33.join(opts.paths.plansDir, feature, planFile);
9056
10186
  if (!isInside(abs, opts.paths.plansDir)) return c.json({ error: "forbidden" }, 403);
9057
- if (!fs39.existsSync(abs)) return c.json({ error: "not_found" }, 404);
10187
+ if (!fs42.existsSync(abs)) return c.json({ error: "not_found" }, 404);
9058
10188
  if (trash) {
9059
10189
  const result = trashOrDelete([abs], opts.paths.trashDir, true);
9060
- removeOverviewRowForPlanFile(path32.join(opts.paths.plansDir, feature), planFile);
10190
+ removeOverviewRowForPlanFile(path33.join(opts.paths.plansDir, feature), planFile);
9061
10191
  return c.json({ ok: true, trashed: result.trashed });
9062
10192
  }
9063
- fs39.rmSync(abs);
9064
- removeOverviewRowForPlanFile(path32.join(opts.paths.plansDir, feature), planFile);
10193
+ fs42.rmSync(abs);
10194
+ removeOverviewRowForPlanFile(path33.join(opts.paths.plansDir, feature), planFile);
9065
10195
  return c.json({ ok: true });
9066
10196
  });
9067
10197
  }
@@ -9109,8 +10239,8 @@ function mountMetaApi(app, opts) {
9109
10239
  // src/console/api/runs.ts
9110
10240
  import { streamSSE } from "hono/streaming";
9111
10241
  import { z as z9 } from "zod";
9112
- import path33 from "path";
9113
- import fs40 from "fs";
10242
+ import path34 from "path";
10243
+ import fs43 from "fs";
9114
10244
  var active = /* @__PURE__ */ new Map();
9115
10245
  var EVENT_BUF_CAP = 4096;
9116
10246
  var StartBody = z9.object({
@@ -9146,15 +10276,21 @@ function mountRunsApi(app, opts) {
9146
10276
  if (!cfg.planner?.enabled) {
9147
10277
  return c.json({ error: "planner_disabled", detail: "Run `squad config set planner` first." }, 400);
9148
10278
  }
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
- );
10279
+ let auth2;
10280
+ try {
10281
+ auth2 = resolvePlannerAuthForPaths(opts.paths, cfg.planner.provider, cfg.planner);
10282
+ } catch (err2) {
10283
+ if (err2 instanceof PlannerAuthUnavailableError) {
10284
+ return c.json(
10285
+ {
10286
+ error: "auth_unavailable",
10287
+ mode: cfg.planner.auth?.anthropic ?? "auto",
10288
+ detail: err2.message
10289
+ },
10290
+ 400
10291
+ );
10292
+ }
10293
+ throw err2;
9158
10294
  }
9159
10295
  const stories = listStories(opts.paths, { feature: body.data.feature });
9160
10296
  const story = stories.find((s) => s.id === body.data.storyId);
@@ -9176,7 +10312,7 @@ function mountRunsApi(app, opts) {
9176
10312
  const draftRuntime = resolveRuntime({
9177
10313
  provider: planner.provider,
9178
10314
  modelId,
9179
- apiKey,
10315
+ auth: auth2,
9180
10316
  anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
9181
10317
  });
9182
10318
  const anthropicProviderSpecific = planner.provider === "anthropic" ? {
@@ -9202,7 +10338,7 @@ function mountRunsApi(app, opts) {
9202
10338
  scoutRuntime = resolveRuntime({
9203
10339
  provider: planner.provider,
9204
10340
  modelId: scoutModelId,
9205
- apiKey,
10341
+ auth: auth2,
9206
10342
  anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
9207
10343
  });
9208
10344
  }
@@ -9223,7 +10359,7 @@ function mountRunsApi(app, opts) {
9223
10359
  repoMap
9224
10360
  });
9225
10361
  const userPrompt = composeUserPrompt({
9226
- intakeContent: fs40.readFileSync(story.intakePath, "utf8")
10362
+ intakeContent: fs43.readFileSync(story.intakePath, "utf8")
9227
10363
  });
9228
10364
  let result;
9229
10365
  try {
@@ -9233,6 +10369,7 @@ function mountRunsApi(app, opts) {
9233
10369
  runtime: draftRuntime,
9234
10370
  provider: planner.provider,
9235
10371
  modelId,
10372
+ auth: auth2,
9236
10373
  anthropicProviderSpecific,
9237
10374
  systemPrompt,
9238
10375
  userPrompt,
@@ -9302,7 +10439,7 @@ function mountRunsApi(app, opts) {
9302
10439
  metadataHeader: header,
9303
10440
  partial: !runSuccess
9304
10441
  });
9305
- planFile = path33.relative(opts.paths.root, out.planFile);
10442
+ planFile = path34.relative(opts.paths.root, out.planFile);
9306
10443
  }
9307
10444
  try {
9308
10445
  await writeLastRun(opts.paths, {
@@ -9338,6 +10475,7 @@ function mountRunsApi(app, opts) {
9338
10475
  durationMs: result.validation?.durationMs
9339
10476
  },
9340
10477
  plannerRuntime: { kind: draftRuntime.kind, provider: planner.provider },
10478
+ authMode: describeAuth(auth2).mode,
9341
10479
  providerOptionsSnapshot: anthropicProviderSpecific ? { anthropic: anthropicProviderSpecific } : void 0
9342
10480
  });
9343
10481
  } catch {
@@ -9469,7 +10607,13 @@ function mountSecretsApi(app, opts) {
9469
10607
  planner: {
9470
10608
  anthropic: maskToken(s.planner?.anthropic),
9471
10609
  openai: maskToken(s.planner?.openai),
9472
- google: maskToken(s.planner?.google)
10610
+ google: maskToken(s.planner?.google),
10611
+ /**
10612
+ * `claude setup-token` output. Masked exactly like the API keys — the Claude account
10613
+ * card shows it so a user can tell "a token is stored here" from "the OS login store
10614
+ * is being used". Never returned in full, and never by `/api/planner-auth`.
10615
+ */
10616
+ anthropicOauthToken: maskToken(s.planner?.anthropicOauthToken)
9473
10617
  },
9474
10618
  tracker: {
9475
10619
  jira: {
@@ -9493,7 +10637,8 @@ function mountSecretsApi(app, opts) {
9493
10637
  planner: z10.object({
9494
10638
  anthropic: z10.string().optional(),
9495
10639
  openai: z10.string().optional(),
9496
- google: z10.string().optional()
10640
+ google: z10.string().optional(),
10641
+ anthropicOauthToken: z10.string().optional()
9497
10642
  }).optional(),
9498
10643
  tracker: z10.object({
9499
10644
  jira: z10.object({
@@ -9517,6 +10662,9 @@ function mountSecretsApi(app, opts) {
9517
10662
  if (!parse.success) return c.json({ error: "invalid_body", issues: parse.error.issues }, 400);
9518
10663
  const current = loadSecrets(opts.paths.secretsFile);
9519
10664
  const merged = mergeSecrets(current, parse.data);
10665
+ if (parse.data.planner?.anthropicOauthToken === "" && merged.planner) {
10666
+ delete merged.planner.anthropicOauthToken;
10667
+ }
9520
10668
  saveSecrets(opts.paths.secretsFile, merged);
9521
10669
  return c.json({ ok: true });
9522
10670
  });
@@ -9547,13 +10695,166 @@ function mountSecretsApi(app, opts) {
9547
10695
  });
9548
10696
  }
9549
10697
 
9550
- // src/console/api/tracker.ts
10698
+ // src/console/api/planner-auth.ts
9551
10699
  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()
10700
+
10701
+ // src/commands/auth/status.ts
10702
+ async function runAuthStatus(opts = {}) {
10703
+ const paths = buildPaths(requireSquadRoot());
10704
+ const config2 = loadConfig(paths.configFile);
10705
+ const planner = config2.planner;
10706
+ const provider = planner?.provider ?? "anthropic";
10707
+ const auth2 = resolvePlannerAuthForCwd(provider, planner);
10708
+ const described = describeAuth(auth2);
10709
+ const probe = shouldProbe(opts, provider) ? await probeClaudeAuth(auth2) : void 0;
10710
+ const payload = buildPayload2({ auth: described, provider, planner, probe, fallbackLoggedIn: credentialPresent(auth2) });
10711
+ if (opts.json) {
10712
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
10713
+ `);
10714
+ return;
10715
+ }
10716
+ const kw2 = 15;
10717
+ step("squad-kit auth");
10718
+ blank();
10719
+ kv("auth mode", `${payload.mode} (${payload.reason})`, kw2);
10720
+ kv("credential", payload.credentialHint, kw2);
10721
+ if (payload.account) {
10722
+ const account = [payload.account.email, payload.account.organization, payload.account.subscriptionType].filter(Boolean).join(" \xB7 ");
10723
+ if (account) kv("account", account, kw2);
10724
+ }
10725
+ if (payload.apiKeySource) kv("api key source", payload.apiKeySource, kw2);
10726
+ kv("runtime", payload.runtime, kw2);
10727
+ if (probe && !probe.ok) {
10728
+ blank();
10729
+ warning(`Live check failed (${probe.kind}).`);
10730
+ info(probe.detail);
10731
+ if (probe.kind === "not-logged-in" || probe.kind === "expired") {
10732
+ info("Run `squad auth login` to sign in again.");
10733
+ }
10734
+ } else if (!probe) {
10735
+ blank();
10736
+ info("Account details skipped (offline). Re-run without --offline to check the live login.");
10737
+ }
10738
+ }
10739
+ function shouldProbe(opts, provider) {
10740
+ if (opts.offline) return false;
10741
+ if (skipExternalProbesInAutomation()) return false;
10742
+ return provider === "anthropic";
10743
+ }
10744
+ function buildPayload2(input5) {
10745
+ const { auth: auth2, provider, planner, probe } = input5;
10746
+ const payload = {
10747
+ mode: auth2.mode,
10748
+ reason: authReasonText(auth2.reason, provider),
10749
+ credentialHint: auth2.credentialHint,
10750
+ runtime: runtimeNameFor(provider, planner),
10751
+ // Without a probe the honest answer is "a credential resolved locally"; with one, the live
10752
+ // result wins. Either way the key is always present so scripts can rely on the shape.
10753
+ loggedIn: probe ? probe.ok : input5.fallbackLoggedIn
10754
+ };
10755
+ if (probe?.ok) {
10756
+ if (probe.account) payload.account = probe.account;
10757
+ if (probe.apiKeySource) payload.apiKeySource = probe.apiKeySource;
10758
+ }
10759
+ return payload;
10760
+ }
10761
+
10762
+ // src/console/api/planner-auth.ts
10763
+ var PLANNER_AUTH_LOGIN_COMMAND = "squad auth login";
10764
+ var PROBE_CACHE_TTL_MS = 6e4;
10765
+ var probeCache;
10766
+ function probeCacheKey(auth2) {
10767
+ const described = describeAuth(auth2);
10768
+ return `${described.mode}:${described.credentialHint}`;
10769
+ }
10770
+ async function probeWithCache(auth2, now) {
10771
+ const key = probeCacheKey(auth2);
10772
+ if (probeCache && probeCache.key === key && now - probeCache.at < PROBE_CACHE_TTL_MS) {
10773
+ return probeCache.result;
10774
+ }
10775
+ const result = await probeClaudeAuth(auth2);
10776
+ if (result.ok) probeCache = { key, at: now, result };
10777
+ return result;
10778
+ }
10779
+ function resetPlannerAuthProbeCache() {
10780
+ probeCache = void 0;
10781
+ }
10782
+ function subscriptionRuntimeConflict(provider, planner) {
10783
+ if (provider !== "anthropic") return plannerAuthRuntimeMismatchMessage(provider);
10784
+ if (runtimeNameFor(provider, planner) === "vercel") return plannerAuthRuntimeMismatchMessage(provider);
10785
+ return null;
10786
+ }
10787
+ function mountPlannerAuthApi(app, opts) {
10788
+ app.get("/api/planner-auth", async (c) => {
10789
+ const config2 = loadConfig(opts.paths.configFile);
10790
+ const planner = config2.planner;
10791
+ const provider = planner?.provider ?? "anthropic";
10792
+ const configuredMode = planner?.auth?.anthropic ?? "auto";
10793
+ const loginProbe = detectClaudeLogin(loadSecrets(opts.paths.secretsFile));
10794
+ const binary = resolveClaudeBinary();
10795
+ let auth2;
10796
+ let error = null;
10797
+ try {
10798
+ auth2 = resolvePlannerAuthForPaths(opts.paths, provider, planner);
10799
+ } catch (err2) {
10800
+ if (!(err2 instanceof PlannerAuthUnavailableError)) throw err2;
10801
+ error = err2.message;
10802
+ }
10803
+ const wantProbe = c.req.query("probe") === "1" && provider === "anthropic" && auth2 !== void 0;
10804
+ const probe = wantProbe ? await probeWithCache(auth2, Date.now()) : void 0;
10805
+ const status = auth2 ? buildPayload2({
10806
+ auth: describeAuth(auth2),
10807
+ provider,
10808
+ planner,
10809
+ probe,
10810
+ fallbackLoggedIn: credentialPresent(auth2)
10811
+ }) : void 0;
10812
+ const payload = {
10813
+ provider,
10814
+ configuredMode,
10815
+ resolved: status ? { mode: status.mode, reason: status.reason, credentialHint: status.credentialHint } : null,
10816
+ error,
10817
+ login: { present: loginProbe.present, hint: loginProbe.hint, detail: loginProbe.detail },
10818
+ account: status?.account ?? null,
10819
+ apiKeySource: status?.apiKeySource ?? null,
10820
+ runtime: runtimeNameFor(provider, planner) === "vercel" ? "vercel" : "agent-sdk",
10821
+ binary: { found: Boolean(binary), source: binary?.source ?? null },
10822
+ loginCommand: PLANNER_AUTH_LOGIN_COMMAND,
10823
+ loggedIn: status?.loggedIn ?? false,
10824
+ probe: probe ? { ran: true, ok: probe.ok, ...probe.ok ? {} : { kind: probe.kind, detail: probe.detail } } : null,
10825
+ runtimeConflict: subscriptionRuntimeConflict(provider, planner)
10826
+ };
10827
+ return c.json(payload);
10828
+ });
10829
+ const ModeBody = z11.object({ mode: z11.string() });
10830
+ app.post("/api/planner-auth/mode", async (c) => {
10831
+ const parsed = ModeBody.safeParse(await c.req.json().catch(() => null));
10832
+ if (!parsed.success) return c.json({ error: "invalid_body", issues: parsed.error.issues }, 400);
10833
+ const config2 = loadConfig(opts.paths.configFile);
10834
+ const next = {
10835
+ ...config2,
10836
+ planner: {
10837
+ ...config2.planner ?? {},
10838
+ auth: { ...config2.planner?.auth ?? {}, anthropic: parsed.data.mode }
10839
+ }
10840
+ };
10841
+ try {
10842
+ saveConfig(opts.paths.configFile, next);
10843
+ } catch (err2) {
10844
+ return c.json({ error: "invalid_auth_mode", detail: err2.message }, 400);
10845
+ }
10846
+ resetPlannerAuthProbeCache();
10847
+ return c.json({ ok: true, mode: parsed.data.mode });
10848
+ });
10849
+ }
10850
+
10851
+ // src/console/api/tracker.ts
10852
+ import { z as z12 } from "zod";
10853
+ var SearchQuery = z12.object({ q: z12.string().max(500) });
10854
+ var ImportBody = z12.object({
10855
+ issueId: z12.string().min(1),
10856
+ feature: z12.string().min(1).max(80),
10857
+ withAttachments: z12.boolean().optional()
9557
10858
  });
9558
10859
  function mountTrackerApi(app, opts) {
9559
10860
  app.get("/api/tracker/search", async (c) => {
@@ -9694,17 +10995,17 @@ function countStories(stories) {
9694
10995
  }
9695
10996
 
9696
10997
  // src/console/api/projects.ts
9697
- import fs41 from "fs/promises";
9698
- import path34 from "path";
9699
- import os from "os";
10998
+ import fs44 from "fs/promises";
10999
+ import path35 from "path";
11000
+ import os2 from "os";
9700
11001
  var MAX = 10;
9701
11002
  function recentFilePath() {
9702
- return path34.join(os.homedir(), ".squad", "recent-projects.json");
11003
+ return path35.join(os2.homedir(), ".squad", "recent-projects.json");
9703
11004
  }
9704
11005
  async function loadRecent() {
9705
11006
  const RECENT_FILE = recentFilePath();
9706
11007
  try {
9707
- const raw = await fs41.readFile(RECENT_FILE, "utf8");
11008
+ const raw = await fs44.readFile(RECENT_FILE, "utf8");
9708
11009
  const parsed = JSON.parse(raw);
9709
11010
  return Array.isArray(parsed) ? parsed.slice(0, MAX) : [];
9710
11011
  } catch {
@@ -9713,8 +11014,8 @@ async function loadRecent() {
9713
11014
  }
9714
11015
  async function saveRecent(list) {
9715
11016
  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");
11017
+ await fs44.mkdir(path35.dirname(RECENT_FILE), { recursive: true });
11018
+ await fs44.writeFile(RECENT_FILE, JSON.stringify(list.slice(0, MAX), null, 2) + "\n", "utf8");
9718
11019
  }
9719
11020
  function mountProjectsApi(app, opts) {
9720
11021
  app.get("/api/projects/recent", async (c) => c.json(await loadRecent()));
@@ -9737,14 +11038,15 @@ function mountApi(app, opts) {
9737
11038
  mountConfigApi(app, opts);
9738
11039
  mountRunsApi(app, opts);
9739
11040
  mountSecretsApi(app, opts);
11041
+ mountPlannerAuthApi(app, opts);
9740
11042
  mountTrackerApi(app, opts);
9741
11043
  mountDoctorApi(app, opts);
9742
11044
  mountCopyPlanPromptApi(app, opts);
9743
11045
  }
9744
11046
 
9745
11047
  // src/console/static-assets.ts
9746
- import fs42 from "fs";
9747
- import path35 from "path";
11048
+ import fs45 from "fs";
11049
+ import path36 from "path";
9748
11050
  import { fileURLToPath as fileURLToPath2 } from "url";
9749
11051
  import { Readable } from "stream";
9750
11052
  function pathnameForStatic(c) {
@@ -9774,18 +11076,18 @@ function mountStaticAssets(app, _opts) {
9774
11076
  }
9775
11077
  if (c.req.path.startsWith("/api/") || c.req.path.startsWith("/healthz")) return c.notFound();
9776
11078
  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" });
11079
+ const indexHtml = path36.join(distDir, "index.html");
11080
+ if (!fs45.existsSync(indexHtml)) return c.text("UI not built", 404);
11081
+ return c.html(fs45.readFileSync(indexHtml, "utf8"), 200, { "cache-control": "no-store" });
9780
11082
  });
9781
11083
  }
9782
11084
  function assetResponse(c, distDir, pathname) {
9783
11085
  if (!distDir) return c.text("UI not built", 404);
9784
11086
  const rel = pathname.replace(/^\//, "");
9785
- const abs = path35.normalize(path35.join(distDir, rel));
11087
+ const abs = path36.normalize(path36.join(distDir, rel));
9786
11088
  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));
11089
+ if (!fs45.existsSync(abs) || !fs45.statSync(abs).isFile()) return c.text("not found", 404);
11090
+ const body = Readable.toWeb(fs45.createReadStream(abs));
9789
11091
  return new Response(body, {
9790
11092
  headers: {
9791
11093
  "content-type": contentTypeFor(abs),
@@ -9795,9 +11097,9 @@ function assetResponse(c, distDir, pathname) {
9795
11097
  }
9796
11098
  function serveFile(c, distDir, name, ct) {
9797
11099
  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), {
11100
+ const abs = path36.join(distDir, name);
11101
+ if (!fs45.existsSync(abs)) return c.text("not found", 404);
11102
+ return new Response(fs45.readFileSync(abs), {
9801
11103
  headers: { "content-type": ct, "cache-control": "public, max-age=86400" }
9802
11104
  });
9803
11105
  }
@@ -9812,13 +11114,13 @@ function contentTypeFor(p) {
9812
11114
  return "application/octet-stream";
9813
11115
  }
9814
11116
  function isInside2(abs, parent) {
9815
- const a = path35.resolve(abs) + path35.sep;
9816
- const p = path35.resolve(parent) + path35.sep;
11117
+ const a = path36.resolve(abs) + path36.sep;
11118
+ const p = path36.resolve(parent) + path36.sep;
9817
11119
  return a.startsWith(p);
9818
11120
  }
9819
11121
  function resolveConsoleUiDist() {
9820
11122
  const fromEnv = process.env.SQUAD_CONSOLE_UI_DIST;
9821
- if (fromEnv && fs42.existsSync(path35.join(fromEnv, "index.html"))) return fromEnv;
11123
+ if (fromEnv && fs45.existsSync(path36.join(fromEnv, "index.html"))) return fromEnv;
9822
11124
  const candidates = [
9823
11125
  fileURLToPath2(new URL("./console-ui/", import.meta.url)),
9824
11126
  // dist/cli.js → dist/console-ui/
@@ -9830,7 +11132,7 @@ function resolveConsoleUiDist() {
9830
11132
  // src/console/*.ts → project/console-ui/dist
9831
11133
  ];
9832
11134
  for (const c of candidates) {
9833
- if (fs42.existsSync(path35.join(c, "index.html"))) return c;
11135
+ if (fs45.existsSync(path36.join(c, "index.html"))) return c;
9834
11136
  }
9835
11137
  return null;
9836
11138
  }
@@ -9846,9 +11148,9 @@ function authMiddleware(expected) {
9846
11148
  };
9847
11149
  }
9848
11150
  function readPresentedToken(req) {
9849
- const auth = req.headers.get("authorization");
9850
- if (auth && auth.toLowerCase().startsWith("bearer ")) {
9851
- return auth.slice("bearer ".length).trim();
11151
+ const auth2 = req.headers.get("authorization");
11152
+ if (auth2 && auth2.toLowerCase().startsWith("bearer ")) {
11153
+ return auth2.slice("bearer ".length).trim();
9852
11154
  }
9853
11155
  const url = new URL(req.url);
9854
11156
  return url.searchParams.get("t");
@@ -10012,18 +11314,18 @@ async function runConsole(opts) {
10012
11314
  }
10013
11315
 
10014
11316
  // src/commands/rm/story.ts
10015
- import fs44 from "fs";
10016
- import path37 from "path";
10017
- import { confirm as confirm10 } from "@inquirer/prompts";
11317
+ import fs47 from "fs";
11318
+ import path38 from "path";
11319
+ import { confirm as confirm11 } from "@inquirer/prompts";
10018
11320
 
10019
11321
  // src/commands/rm/feature.ts
10020
- import fs43 from "fs";
10021
- import path36 from "path";
10022
- import { confirm as confirm9 } from "@inquirer/prompts";
11322
+ import fs46 from "fs";
11323
+ import path37 from "path";
11324
+ import { confirm as confirm10 } from "@inquirer/prompts";
10023
11325
  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;
11326
+ const d = path37.join(storiesDir, feature);
11327
+ if (!fs46.existsSync(d)) return 0;
11328
+ return fs46.readdirSync(d, { withFileTypes: true }).filter((x) => x.isDirectory()).length;
10027
11329
  }
10028
11330
  async function runRmFeature(featureSlug, opts = {}) {
10029
11331
  const root = requireSquadRoot();
@@ -10057,17 +11359,17 @@ async function runRmFeature(featureSlug, opts = {}) {
10057
11359
  feature = await pickFeatureInteractive(paths);
10058
11360
  }
10059
11361
  }
10060
- const storyDir = path36.join(paths.storiesDir, feature);
10061
- const planDir = path36.join(paths.plansDir, feature);
11362
+ const storyDir = path37.join(paths.storiesDir, feature);
11363
+ const planDir = path37.join(paths.plansDir, feature);
10062
11364
  const nStories = countStoryDirs(paths.storiesDir, feature);
10063
11365
  const nPlans = listAllPlanListEntries(paths, feature).length;
10064
- const hasOverview = fs43.existsSync(path36.join(planDir, "00-overview.md")) ? 1 : 0;
11366
+ const hasOverview = fs46.existsSync(path37.join(planDir, "00-overview.md")) ? 1 : 0;
10065
11367
  const targets = [];
10066
- if (fs43.existsSync(storyDir)) {
10067
- targets.push({ label: path36.relative(root, storyDir), absPath: storyDir, kind: "dir" });
11368
+ if (fs46.existsSync(storyDir)) {
11369
+ targets.push({ label: path37.relative(root, storyDir), absPath: storyDir, kind: "dir" });
10068
11370
  }
10069
- if (fs43.existsSync(planDir)) {
10070
- targets.push({ label: path36.relative(root, planDir), absPath: planDir, kind: "dir" });
11371
+ if (fs46.existsSync(planDir)) {
11372
+ targets.push({ label: path37.relative(root, planDir), absPath: planDir, kind: "dir" });
10071
11373
  }
10072
11374
  divider("squad rm feature");
10073
11375
  info(`Feature: ${feature}`);
@@ -10082,7 +11384,7 @@ async function runRmFeature(featureSlug, opts = {}) {
10082
11384
  kv(t.kind, t.label, 6);
10083
11385
  }
10084
11386
  if (useTrash) {
10085
- info(`Items will move to ${path36.relative(process.cwd(), paths.trashDir)}/<timestamp>/`);
11387
+ info(`Items will move to ${path37.relative(process.cwd(), paths.trashDir)}/<timestamp>/`);
10086
11388
  } else if (!dryRun) {
10087
11389
  warning("This is a permanent delete. Pass --trash to move to .squad/.trash/ instead.");
10088
11390
  }
@@ -10100,7 +11402,7 @@ async function runRmFeature(featureSlug, opts = {}) {
10100
11402
  "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
11403
  );
10102
11404
  }
10103
- const ok = await confirm9({
11405
+ const ok = await confirm10({
10104
11406
  message: `Delete all of feature "${feature}" (stories + plans)?`,
10105
11407
  default: false
10106
11408
  });
@@ -10115,7 +11417,7 @@ async function runRmFeature(featureSlug, opts = {}) {
10115
11417
  useTrash
10116
11418
  );
10117
11419
  if (result.trashed) {
10118
- success(`Feature removed. Items moved to ${path36.relative(root, result.trashed)}.`);
11420
+ success(`Feature removed. Items moved to ${path37.relative(root, result.trashed)}.`);
10119
11421
  } else {
10120
11422
  success("Feature removed (stories and plans directories).");
10121
11423
  }
@@ -10156,11 +11458,11 @@ async function runRmStory(storyPathOrId, opts = {}) {
10156
11458
  }
10157
11459
  }
10158
11460
  const targets = [
10159
- { label: path37.relative(root, story.storyDir), absPath: story.storyDir, kind: "dir" }
11461
+ { label: path38.relative(root, story.storyDir), absPath: story.storyDir, kind: "dir" }
10160
11462
  ];
10161
11463
  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" });
11464
+ const planAbs = path38.join(paths.plansDir, story.feature, story.planFile);
11465
+ targets.push({ label: path38.relative(root, planAbs), absPath: planAbs, kind: "file" });
10164
11466
  }
10165
11467
  renderPreview("story", targets, {
10166
11468
  dryRun,
@@ -10177,7 +11479,7 @@ async function runRmStory(storyPathOrId, opts = {}) {
10177
11479
  "Pass --yes to confirm deletion in a non-interactive environment, or run `squad rm story` in a TTY to confirm the prompt interactively."
10178
11480
  );
10179
11481
  }
10180
- const ok = await confirm10({ message: "Delete this story and related plan/overview row?", default: false });
11482
+ const ok = await confirm11({ message: "Delete this story and related plan/overview row?", default: false });
10181
11483
  if (!ok) {
10182
11484
  info("Cancelled.");
10183
11485
  return;
@@ -10185,18 +11487,18 @@ async function runRmStory(storyPathOrId, opts = {}) {
10185
11487
  }
10186
11488
  const result = deleteStoryRecord({ paths, story, trash: useTrash });
10187
11489
  if (result.trashed) {
10188
- success(`Removed (moved to ${path37.relative(root, result.trashed)}).`);
11490
+ success(`Removed (moved to ${path38.relative(root, result.trashed)}).`);
10189
11491
  } else {
10190
11492
  success("Removed story folder, plan file (if any), and overview row.");
10191
11493
  }
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");
11494
+ const featurePlanDir = path38.join(paths.plansDir, story.feature);
11495
+ const overviewPath = path38.join(featurePlanDir, "00-overview.md");
11496
+ if (fs47.existsSync(overviewPath)) {
11497
+ const body = fs47.readFileSync(overviewPath, "utf8");
10196
11498
  const onlyShell = countOverviewDataRows(body) === 0;
10197
11499
  if (onlyShell && featureStoriesDirEmpty(paths, story.feature)) {
10198
11500
  if (canPrompt) {
10199
- const also = await confirm10({
11501
+ const also = await confirm11({
10200
11502
  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
11503
  default: false
10202
11504
  });
@@ -10212,8 +11514,8 @@ async function runRmStory(storyPathOrId, opts = {}) {
10212
11514
  }
10213
11515
 
10214
11516
  // src/commands/rm/plan.ts
10215
- import path38 from "path";
10216
- import { confirm as confirm11 } from "@inquirer/prompts";
11517
+ import path39 from "path";
11518
+ import { confirm as confirm12 } from "@inquirer/prompts";
10217
11519
  async function runRmPlan(planPathOrSequence, opts = {}) {
10218
11520
  const root = requireSquadRoot();
10219
11521
  const paths = buildPaths(root);
@@ -10248,7 +11550,7 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
10248
11550
  }
10249
11551
  const storyRows = listStories(paths, { feature: plan.feature });
10250
11552
  const linked = storyRows.find((s) => s.planFile === plan.file);
10251
- const targets = [{ label: path38.relative(root, plan.abs), absPath: plan.abs, kind: "file" }];
11553
+ const targets = [{ label: path39.relative(root, plan.abs), absPath: plan.abs, kind: "file" }];
10252
11554
  renderPreview("plan", targets, {
10253
11555
  dryRun,
10254
11556
  trash: useTrash,
@@ -10264,18 +11566,54 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
10264
11566
  "Pass --yes to confirm deletion in a non-interactive environment, or run `squad rm plan` in a TTY."
10265
11567
  );
10266
11568
  }
10267
- const ok = await confirm11({ message: "Delete this plan file only? (intake and config are kept.)", default: false });
11569
+ const ok = await confirm12({ message: "Delete this plan file only? (intake and config are kept.)", default: false });
10268
11570
  if (!ok) {
10269
11571
  info("Cancelled.");
10270
11572
  return;
10271
11573
  }
10272
11574
  }
10273
11575
  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}/`;
11576
+ removeOverviewRowForPlanFile(path39.join(paths.plansDir, plan.feature), plan.file);
11577
+ const intakeHint = linked ? path39.relative(root, linked.intakePath) : `under .squad/stories/${plan.feature}/`;
10276
11578
  success(`Plan removed. Run \`squad new-plan ${intakeHint}\` to regenerate.`);
10277
11579
  }
10278
11580
 
11581
+ // src/commands/auth/logout.ts
11582
+ import fs48 from "fs";
11583
+ import { confirm as confirm13 } from "@inquirer/prompts";
11584
+ async function runAuthLogout(opts = {}) {
11585
+ const paths = buildPaths(requireSquadRoot());
11586
+ const secrets = fs48.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
11587
+ if (!secrets.planner?.anthropicOauthToken) {
11588
+ info("No Claude OAuth token is stored in .squad/secrets.yaml \u2014 nothing to remove.");
11589
+ info("Your Claude Code login (OS credential store) is separate and was not touched.");
11590
+ return;
11591
+ }
11592
+ if (!opts.yes) {
11593
+ if (!isInteractive()) {
11594
+ warning("Pass -y in non-interactive mode to confirm, or run `squad auth logout` in a TTY.");
11595
+ return;
11596
+ }
11597
+ const ok = await confirm13({
11598
+ message: "Remove the Claude OAuth token squad-kit stored in .squad/secrets.yaml?",
11599
+ default: false
11600
+ });
11601
+ if (!ok) return;
11602
+ }
11603
+ delete secrets.planner.anthropicOauthToken;
11604
+ saveSecrets(paths.secretsFile, secrets);
11605
+ success("Removed the OAuth token squad-kit stored.");
11606
+ info(
11607
+ "Your Claude Code login is untouched \u2014 run `claude` and `/logout` if you also want to sign out of Claude Code."
11608
+ );
11609
+ const mode = loadConfig(paths.configFile).planner?.auth?.anthropic;
11610
+ if (mode === "subscription") {
11611
+ info(
11612
+ "`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`."
11613
+ );
11614
+ }
11615
+ }
11616
+
10279
11617
  // src/cli.ts
10280
11618
  var program = new Command();
10281
11619
  program.name("squad").description("Plan once, execute cheap. A 3-step SDD workflow CLI.").version(readInstalledPackage().version);
@@ -10344,6 +11682,14 @@ rm.command("plan [plan-path-or-sequence]").description("Delete a single plan fil
10344
11682
  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
11683
  var tracker = program.command("tracker").description("Tracker id helpers");
10346
11684
  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));
11685
+ var auth = program.command("auth").description(
11686
+ "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"
11687
+ );
11688
+ auth.command("login").description(
11689
+ "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`."
11690
+ ).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));
11691
+ 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));
11692
+ 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
11693
  var config = program.command("config").description("Inspect and change .squad/ settings");
10348
11694
  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
11695
  var set = config.command("set").description("Interactively change a section");