squad-kit 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.1] — 2026-04-28
11
+
12
+ ### Bug fixes
13
+
14
+ - `squad --version` printed `0.6.0` on 0.7.0 — the version string was
15
+ hardcoded in the CLI and the console `/healthz` endpoint. Both now
16
+ read from `package.json` and stay in sync with the installed package.
17
+
18
+ ## [0.7.0] — 2026-04-28
19
+
20
+ ### What's new
21
+
22
+ - GitHub Issues works as the issue tracker: set owner/repo in config, PAT (or
23
+ `GITHUB_TOKEN` / `GITHUB_HOST`) in secrets, search and import like Jira or
24
+ Azure. Console Secrets page has GitHub host and token fields plus a
25
+ connectivity test; `squad doctor` probes the GitHub REST API.
26
+
27
+ ### Breaking changes
28
+
29
+ - Linear is no longer a tracker type. Use GitHub Issues, Jira, or Azure, or
30
+ set tracker to `none`. Update `.squad/config.yaml` if you had `linear`.
31
+
10
32
  ## [0.6.1] — 2026-04-26
11
33
 
12
34
  ### Bug fixes
package/dist/cli.js CHANGED
@@ -495,6 +495,10 @@ function mergeSecrets(base, patch) {
495
495
  azure: {
496
496
  ...base.tracker?.azure ?? {},
497
497
  ...filterEmpty(patch.tracker?.azure ?? {})
498
+ },
499
+ github: {
500
+ ...base.tracker?.github ?? {},
501
+ ...filterEmpty(patch.tracker?.github ?? {})
498
502
  }
499
503
  }
500
504
  };
@@ -768,6 +772,8 @@ function overlayTrackerEnv(base) {
768
772
  const azOrg = process.env.AZURE_DEVOPS_ORG;
769
773
  const azProj = process.env.AZURE_DEVOPS_PROJECT;
770
774
  const azPat = process.env.AZURE_DEVOPS_PAT ?? process.env.SQUAD_TRACKER_API_KEY;
775
+ const ghHost = process.env.GITHUB_HOST;
776
+ const ghPat = process.env.GITHUB_TOKEN ?? process.env.SQUAD_TRACKER_API_KEY;
771
777
  return {
772
778
  ...base,
773
779
  tracker: {
@@ -780,6 +786,10 @@ function overlayTrackerEnv(base) {
780
786
  organization: azOrg ?? base.tracker?.azure?.organization,
781
787
  project: azProj ?? base.tracker?.azure?.project,
782
788
  pat: azPat ?? base.tracker?.azure?.pat
789
+ },
790
+ github: {
791
+ host: ghHost ?? base.tracker?.github?.host,
792
+ pat: ghPat ?? base.tracker?.github?.pat
783
793
  }
784
794
  }
785
795
  };
@@ -858,6 +868,27 @@ async function probeAzureConnectivity(secrets, config2) {
858
868
  return { ok: false, detail: err.message };
859
869
  }
860
870
  }
871
+ async function probeGitHubConnectivity(secrets, _config) {
872
+ const gh = overlayTrackerEnv(secrets).tracker?.github ?? {};
873
+ const pat = gh.pat ?? "";
874
+ const rawHost = (gh.host ?? "api.github.com").replace(/^https?:\/\//i, "").replace(/\/+$/, "");
875
+ const apiPath = rawHost === "api.github.com" ? "" : "/api/v3";
876
+ const url = `https://${rawHost}${apiPath}/user`;
877
+ try {
878
+ const res = await fetch(url, {
879
+ headers: {
880
+ Authorization: `Bearer ${pat}`,
881
+ Accept: "application/vnd.github+json",
882
+ "X-GitHub-Api-Version": "2022-11-28",
883
+ "User-Agent": "squad-kit"
884
+ }
885
+ });
886
+ if (res.ok) return { ok: true };
887
+ return { ok: false, status: res.status, detail: (await res.text()).slice(0, 200) };
888
+ } catch (err) {
889
+ return { ok: false, detail: err.message };
890
+ }
891
+ }
861
892
 
862
893
  // src/commands/config/shared.ts
863
894
  import { input, password } from "@inquirer/prompts";
@@ -1379,6 +1410,166 @@ function stripHtml(html) {
1379
1410
  return out.trim();
1380
1411
  }
1381
1412
 
1413
+ // src/tracker/github.ts
1414
+ var PUBLIC_HOST = "api.github.com";
1415
+ var ID_PREFIX_RE = /^[\w.-]+\/[\w.-]+#/;
1416
+ var GitHubClient = class {
1417
+ constructor(cfg) {
1418
+ this.cfg = cfg;
1419
+ if (!cfg.owner) throw new Error('GitHubClient: missing "owner".');
1420
+ if (!cfg.repo) throw new Error('GitHubClient: missing "repo".');
1421
+ if (!cfg.pat) throw new Error('GitHubClient: missing "pat".');
1422
+ const host = (cfg.host ?? PUBLIC_HOST).replace(/^https?:\/\//i, "").replace(/\/+$/, "");
1423
+ const apiPath = host === PUBLIC_HOST ? "" : "/api/v3";
1424
+ this.baseUrl = `https://${host}${apiPath}`;
1425
+ this.webBase = host === PUBLIC_HOST ? "https://github.com" : `https://${host}`;
1426
+ this.authHeader = `Bearer ${cfg.pat}`;
1427
+ }
1428
+ cfg;
1429
+ name = "github";
1430
+ authHeader;
1431
+ baseUrl;
1432
+ webBase;
1433
+ async fetchIssue(id) {
1434
+ const number = stripIdPrefix(id);
1435
+ const url = `${this.baseUrl}/repos/${encodeURIComponent(this.cfg.owner)}/${encodeURIComponent(
1436
+ this.cfg.repo
1437
+ )}/issues/${encodeURIComponent(number)}`;
1438
+ let res;
1439
+ try {
1440
+ res = await fetch(url, { headers: this.headers() });
1441
+ } catch (err) {
1442
+ throw new TrackerError(`GitHub fetch failed: ${err.message}`, "network");
1443
+ }
1444
+ if (!res.ok) throw this.mapHttpError(res, number);
1445
+ const body = await res.json();
1446
+ return this.toFetchResult(body);
1447
+ }
1448
+ async searchIssues(query, opts) {
1449
+ const limit = Math.min(50, Math.max(1, opts?.limit ?? 25));
1450
+ const q = query.trim();
1451
+ const repoPath = `${encodeURIComponent(this.cfg.owner)}/${encodeURIComponent(this.cfg.repo)}`;
1452
+ if (q.length === 0) {
1453
+ const url2 = `${this.baseUrl}/repos/${repoPath}/issues?state=all&sort=updated&direction=desc&per_page=${limit}`;
1454
+ let res2;
1455
+ try {
1456
+ res2 = await fetch(url2, { headers: this.headers() });
1457
+ } catch (err) {
1458
+ throw new TrackerError(`GitHub search failed: ${err.message}`, "network");
1459
+ }
1460
+ if (!res2.ok) throw this.mapHttpError(res2, "search");
1461
+ const body2 = await res2.json();
1462
+ return (body2 ?? []).map((it) => this.toSearchRow(it));
1463
+ }
1464
+ if (/^\d+$/.test(q) || ID_PREFIX_RE.test(q)) {
1465
+ try {
1466
+ const issue = await this.fetchIssue(q);
1467
+ return [
1468
+ {
1469
+ id: issue.id,
1470
+ title: issue.title,
1471
+ type: issue.type,
1472
+ status: issue.status,
1473
+ url: issue.url
1474
+ }
1475
+ ];
1476
+ } catch (err) {
1477
+ if (err instanceof TrackerError && err.kind === "not-found") return [];
1478
+ throw err;
1479
+ }
1480
+ }
1481
+ const searchQuery = `${q} repo:${this.cfg.owner}/${this.cfg.repo}`;
1482
+ const url = `${this.baseUrl}/search/issues?q=${encodeURIComponent(searchQuery)}&sort=updated&order=desc&per_page=${limit}`;
1483
+ let res;
1484
+ try {
1485
+ res = await fetch(url, { headers: this.headers() });
1486
+ } catch (err) {
1487
+ throw new TrackerError(`GitHub search failed: ${err.message}`, "network");
1488
+ }
1489
+ if (!res.ok) throw this.mapHttpError(res, "search");
1490
+ const body = await res.json();
1491
+ return (body.items ?? []).map((it) => this.toSearchRow(it));
1492
+ }
1493
+ async downloadAttachments(refs, targetDir, opts) {
1494
+ if (refs.length === 0) return [];
1495
+ return downloadAttachmentsWith(refs, targetDir, this.headers(), opts);
1496
+ }
1497
+ toFetchResult(body) {
1498
+ return {
1499
+ id: String(body.number ?? ""),
1500
+ title: body.title ?? "(no title)",
1501
+ description: body.body ?? "",
1502
+ acceptanceCriteria: "",
1503
+ url: body.html_url ?? `${this.webBase}/${this.cfg.owner}/${this.cfg.repo}/issues/${body.number ?? ""}`,
1504
+ labels: Array.isArray(body.labels) ? body.labels.map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean) : [],
1505
+ type: body.pull_request ? "Pull Request" : "Issue",
1506
+ assignee: body.assignee?.login,
1507
+ status: body.state,
1508
+ attachments: [],
1509
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
1510
+ };
1511
+ }
1512
+ toSearchRow(body) {
1513
+ return {
1514
+ id: String(body.number ?? ""),
1515
+ title: body.title ?? "(no title)",
1516
+ type: body.pull_request ? "Pull Request" : "Issue",
1517
+ status: body.state,
1518
+ url: body.html_url ?? `${this.webBase}/${this.cfg.owner}/${this.cfg.repo}/issues/${body.number ?? ""}`
1519
+ };
1520
+ }
1521
+ headers() {
1522
+ return {
1523
+ authorization: this.authHeader,
1524
+ accept: "application/vnd.github+json",
1525
+ "x-github-api-version": "2022-11-28",
1526
+ "user-agent": "squad-kit"
1527
+ };
1528
+ }
1529
+ mapHttpError(res, id) {
1530
+ const status = res.status;
1531
+ if (status === 401 || status === 403) {
1532
+ if (res.headers.get("x-ratelimit-remaining") === "0") {
1533
+ return new TrackerError(
1534
+ `GitHub rate limit hit (HTTP ${status}). Wait until the limit resets and retry.`,
1535
+ "rate-limited",
1536
+ status
1537
+ );
1538
+ }
1539
+ return new TrackerError(
1540
+ `GitHub authentication failed (HTTP ${status}). Check your PAT in .squad/secrets.yaml; it needs "repo" scope (or "Issues: read" for fine-grained tokens).`,
1541
+ "auth",
1542
+ status
1543
+ );
1544
+ }
1545
+ if (status === 404) {
1546
+ return new TrackerError(
1547
+ id === "search" ? `GitHub search failed (HTTP 404) for ${this.cfg.owner}/${this.cfg.repo}.` : `GitHub issue "${id}" not found in ${this.cfg.owner}/${this.cfg.repo} (HTTP 404). Check the id and tracker.workspace / tracker.project in .squad/config.yaml.`,
1548
+ "not-found",
1549
+ status
1550
+ );
1551
+ }
1552
+ if (status === 422) {
1553
+ return new TrackerError(
1554
+ `GitHub rejected the search query (HTTP 422). Try a simpler query or a bare issue number.`,
1555
+ "other",
1556
+ status
1557
+ );
1558
+ }
1559
+ if (status === 429) {
1560
+ return new TrackerError(`GitHub rate limit hit (HTTP 429). Wait a minute and retry.`, "rate-limited", status);
1561
+ }
1562
+ return new TrackerError(
1563
+ id === "search" ? `GitHub search failed (HTTP ${status}).` : `GitHub request failed (HTTP ${status}).`,
1564
+ "other",
1565
+ status
1566
+ );
1567
+ }
1568
+ };
1569
+ function stripIdPrefix(id) {
1570
+ return id.replace(ID_PREFIX_RE, "");
1571
+ }
1572
+
1382
1573
  // src/tracker/adf.ts
1383
1574
  var BLOCK_TYPES = /* @__PURE__ */ new Set([
1384
1575
  "paragraph",
@@ -1565,29 +1756,44 @@ function clientFor(config2, secrets) {
1565
1756
  }
1566
1757
  return { client: new AzureDevOpsClient({ organization, project, pat: az.pat }) };
1567
1758
  }
1759
+ case "github": {
1760
+ const gh = secrets.tracker?.github ?? {};
1761
+ const owner = config2.tracker.workspace;
1762
+ const repo = config2.tracker.project;
1763
+ if (!owner || !repo || !gh.pat) {
1764
+ return {
1765
+ error: {
1766
+ kind: "missing-credentials",
1767
+ message: "GitHub credentials are incomplete.",
1768
+ detail: "Set tracker.workspace (owner) and tracker.project (repo) in .squad/config.yaml, then run `squad config set tracker` to enter the PAT, or set GITHUB_TOKEN (and optionally GITHUB_HOST for GHES). Run `squad doctor` to verify the connection."
1769
+ }
1770
+ };
1771
+ }
1772
+ return { client: new GitHubClient({ owner, repo, pat: gh.pat, host: gh.host }) };
1773
+ }
1568
1774
  case "none":
1569
1775
  default:
1570
1776
  return {
1571
1777
  error: {
1572
1778
  kind: "unsupported-tracker",
1573
1779
  message: `Tracker type "${config2.tracker.type}" does not support auto-fetch in 0.2.0.`,
1574
- detail: "Run `squad config set tracker` to use jira or azure for fetch, or pass `--no-fetch` on new-story. Run `squad doctor` to review tracker setup."
1780
+ detail: "Run `squad config set tracker` to use jira, azure, or github for fetch, or pass `--no-fetch` on new-story. Run `squad doctor` to review tracker setup."
1575
1781
  }
1576
1782
  };
1577
1783
  }
1578
1784
  }
1579
1785
 
1580
1786
  // src/commands/config/set-tracker.ts
1581
- var TYPES = ["none", "github", "linear", "jira", "azure"];
1787
+ var TYPES = ["none", "github", "jira", "azure"];
1582
1788
  function parseType(t) {
1583
1789
  if (!t) {
1584
1790
  throw new Error(
1585
- "Pass --type (none|jira|azure|github|linear) with --yes, or run `squad config set tracker` without --yes in a TTY to pick a type."
1791
+ "Pass --type (none|jira|azure|github) with --yes, or run `squad config set tracker` without --yes in a TTY to pick a type."
1586
1792
  );
1587
1793
  }
1588
1794
  if (!TYPES.includes(t)) {
1589
1795
  throw new Error(
1590
- `Invalid --type "${t}". Use none | jira | azure | github | linear, or run \`squad config set tracker\` interactively.`
1796
+ `Invalid --type "${t}". Use none | jira | azure | github, or run \`squad config set tracker\` interactively.`
1591
1797
  );
1592
1798
  }
1593
1799
  return t;
@@ -1620,7 +1826,6 @@ async function runConfigSetTracker(opts = {}) {
1620
1826
  choices: [
1621
1827
  { name: "None", value: "none" },
1622
1828
  { name: "GitHub Issues", value: "github" },
1623
- { name: "Linear", value: "linear" },
1624
1829
  { name: "Jira", value: "jira" },
1625
1830
  { name: "Azure DevOps", value: "azure" }
1626
1831
  ],
@@ -1630,7 +1835,7 @@ async function runConfigSetTracker(opts = {}) {
1630
1835
  let nextTracker;
1631
1836
  if (type === "none") {
1632
1837
  nextTracker = { type: "none" };
1633
- } else if (type === "github" || type === "linear") {
1838
+ } else if (type === "github") {
1634
1839
  nextTracker = { ...config2.tracker, type };
1635
1840
  } else if (type === "jira") {
1636
1841
  if (interactive) {
@@ -1864,7 +2069,6 @@ async function runInit(opts) {
1864
2069
  choices: [
1865
2070
  { name: "None", value: "none" },
1866
2071
  { name: "GitHub Issues", value: "github" },
1867
- { name: "Linear", value: "linear" },
1868
2072
  { name: "Jira", value: "jira" },
1869
2073
  { name: "Azure DevOps", value: "azure" }
1870
2074
  ],
@@ -2210,7 +2414,6 @@ function readPlanMetadata(planFileAbsPath) {
2210
2414
  // src/core/tracker.ts
2211
2415
  var VALIDATORS = {
2212
2416
  github: /^(?:[\w.-]+\/[\w.-]+#)?\d+$/,
2213
- linear: /^[A-Z][A-Z0-9]+-\d+$/i,
2214
2417
  jira: /^[A-Z][A-Z0-9]+-\d+$/,
2215
2418
  azure: /^\d+$/
2216
2419
  };
@@ -5403,6 +5606,14 @@ async function checkTrackerConfig(_paths, ctx) {
5403
5606
  detail: "Azure DevOps requires tracker.workspace (organization) and tracker.project"
5404
5607
  };
5405
5608
  }
5609
+ if (t.type === "github" && (!t.workspace?.trim() || !t.project?.trim())) {
5610
+ return {
5611
+ id: "tracker-config",
5612
+ name: "tracker configuration",
5613
+ status: "fail",
5614
+ detail: "GitHub requires tracker.workspace (owner) and tracker.project (repo)"
5615
+ };
5616
+ }
5406
5617
  return { id: "tracker-config", name: "tracker configuration", status: "ok" };
5407
5618
  }
5408
5619
  function formatClientError(err) {
@@ -5466,6 +5677,16 @@ async function checkTrackerConnectivity(paths, ctx) {
5466
5677
  detail: r.status !== void 0 ? `HTTP ${r.status}` : r.detail ?? "request failed"
5467
5678
  };
5468
5679
  }
5680
+ if (client.name === "github") {
5681
+ const r = await probeGitHubConnectivity(secrets, ctx.config);
5682
+ if (r.ok) return { id: "tracker-live", name: "tracker connectivity", status: "ok", detail: "GitHub REST" };
5683
+ return {
5684
+ id: "tracker-live",
5685
+ name: "tracker connectivity",
5686
+ status: "fail",
5687
+ detail: r.status !== void 0 ? `HTTP ${r.status}` : r.detail ?? "request failed"
5688
+ };
5689
+ }
5469
5690
  return {
5470
5691
  id: "tracker-live",
5471
5692
  name: "tracker connectivity",
@@ -6440,6 +6661,10 @@ function mountSecretsApi(app, opts) {
6440
6661
  organization: s.tracker?.azure?.organization ?? null,
6441
6662
  project: s.tracker?.azure?.project ?? null,
6442
6663
  pat: maskToken(s.tracker?.azure?.pat)
6664
+ },
6665
+ github: {
6666
+ host: s.tracker?.github?.host ?? null,
6667
+ pat: maskToken(s.tracker?.github?.pat)
6443
6668
  }
6444
6669
  }
6445
6670
  });
@@ -6460,6 +6685,10 @@ function mountSecretsApi(app, opts) {
6460
6685
  organization: z5.string().optional(),
6461
6686
  project: z5.string().optional(),
6462
6687
  pat: z5.string().optional()
6688
+ }).optional(),
6689
+ github: z5.object({
6690
+ host: z5.string().optional(),
6691
+ pat: z5.string().optional()
6463
6692
  }).optional()
6464
6693
  }).optional()
6465
6694
  });
@@ -6491,6 +6720,11 @@ function mountSecretsApi(app, opts) {
6491
6720
  const s = loadSecrets(opts.paths.secretsFile);
6492
6721
  return c.json(await probeAzureConnectivity(s, cfg));
6493
6722
  });
6723
+ app.post("/api/secrets/test/github", async (c) => {
6724
+ const cfg = loadConfig(opts.paths.configFile);
6725
+ const s = loadSecrets(opts.paths.secretsFile);
6726
+ return c.json(await probeGitHubConnectivity(s, cfg));
6727
+ });
6494
6728
  }
6495
6729
 
6496
6730
  // src/console/api/tracker.ts
@@ -6815,7 +7049,7 @@ async function startConsoleServer(opts) {
6815
7049
  app.get(
6816
7050
  "/healthz",
6817
7051
  (c) => c.json(
6818
- { ok: true, version: "0.6.0", port: bindPort },
7052
+ { ok: true, version: readInstalledPackage().version, port: bindPort },
6819
7053
  200,
6820
7054
  {
6821
7055
  "cache-control": "no-store"
@@ -7224,8 +7458,8 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
7224
7458
 
7225
7459
  // src/cli.ts
7226
7460
  var program = new Command();
7227
- program.name("squad").description("Plan once, execute cheap. A 3-step SDD workflow CLI.").version("0.6.0");
7228
- program.command("init").description("Bootstrap .squad/ in the current directory").option("--agents <list>", "Comma-separated agent list: claude-code,cursor,copilot,gemini").option("--tracker <type>", "Tracker type: none|github|linear|jira|azure").option(
7461
+ program.name("squad").description("Plan once, execute cheap. A 3-step SDD workflow CLI.").version(readInstalledPackage().version);
7462
+ program.command("init").description("Bootstrap .squad/ in the current directory").option("--agents <list>", "Comma-separated agent list: claude-code,cursor,copilot,gemini").option("--tracker <type>", "Tracker type: none|github|jira|azure").option(
7229
7463
  "--tracker-workspace <hostOrOrg>",
7230
7464
  "Jira host or Azure org (non-interactive default; also written to config for jira/azure)"
7231
7465
  ).option("--tracker-project <name>", "Azure project name (non-interactive; written to config when set)").option("--name <name>", "Project name").option("--language <lang>", "Primary language").option("--force", "Overwrite existing files", false).option("-y, --yes", "Accept defaults (non-interactive)", false).option(
@@ -7273,7 +7507,7 @@ var config = program.command("config").description("Inspect and change .squad/ s
7273
7507
  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));
7274
7508
  var set = config.command("set").description("Interactively change a section");
7275
7509
  set.command("planner").description("Set or change the planner provider, model override, and credentials").option("--provider <name>", "Skip the provider prompt (anthropic|openai|google)").option("-y, --yes", "Fail fast instead of prompting for missing values", false).action(wrap(runConfigSetPlanner));
7276
- set.command("tracker").description("Set or change the tracker type, workspace, and credentials").option("--type <name>", "Skip the tracker-type prompt (none|jira|azure|github|linear)").option("-y, --yes", "Fail fast instead of prompting for missing values", false).action(wrap(runConfigSetTracker));
7510
+ set.command("tracker").description("Set or change the tracker type, workspace, and credentials").option("--type <name>", "Skip the tracker-type prompt (none|jira|azure|github)").option("-y, --yes", "Fail fast instead of prompting for missing values", false).action(wrap(runConfigSetTracker));
7277
7511
  var unset = config.command("unset").description("Disable or clear a section");
7278
7512
  unset.command("planner").description("Disable the direct planner (keeps provider credentials unless --remove-credentials)").option("--remove-credentials", "Also delete planner keys from .squad/secrets.yaml", false).option("-y, --yes", "Skip the confirmation prompt", false).action(wrap(runConfigUnsetPlanner));
7279
7513
  unset.command("tracker").description('Set tracker.type back to "none" (keeps credentials unless --remove-credentials)').option("--remove-credentials", "Also delete tracker credentials from .squad/secrets.yaml", false).option("-y, --yes", "Skip the confirmation prompt", false).action(wrap(runConfigUnsetTracker));