pipe-kan 0.33.0 → 0.34.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/pipe-kan.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/server.ts
4
- import { readFileSync as readFileSync7, writeSync } from "node:fs";
4
+ import { readFileSync as readFileSync8, writeSync } from "node:fs";
5
5
  import { createServer } from "node:http";
6
6
  import { tmpdir as tmpdir3 } from "node:os";
7
7
  import { join as join11 } from "node:path";
@@ -156,6 +156,9 @@ function formatDueDate(value) {
156
156
  function createdAt(fields) {
157
157
  return typeof fields?.created === "string" && fields.created ? fields.created : undefined;
158
158
  }
159
+ function updatedAt(fields) {
160
+ return typeof fields?.updated === "string" && fields.updated ? fields.updated : undefined;
161
+ }
159
162
  function issueType(fields) {
160
163
  const name = fields?.issuetype?.name ?? fields?.issueType?.name;
161
164
  return typeof name === "string" ? name : undefined;
@@ -247,6 +250,7 @@ function toCard(issue, key, fieldId) {
247
250
  const epic = epicKey(issue.fields);
248
251
  const labels = issueLabels(issue.fields);
249
252
  const created = createdAt(issue.fields);
253
+ const updated = updatedAt(issue.fields);
250
254
  return {
251
255
  key,
252
256
  summary,
@@ -257,7 +261,8 @@ function toCard(issue, key, fieldId) {
257
261
  ...type ? { type } : {},
258
262
  ...epic ? { epic } : {},
259
263
  ...labels ? { labels } : {},
260
- ...created ? { created } : {}
264
+ ...created ? { created } : {},
265
+ ...updated ? { updated } : {}
261
266
  };
262
267
  }
263
268
  function issuesToBoard(raw, opts = {}) {
@@ -360,6 +365,8 @@ function parseFlags(flags) {
360
365
  let type = "";
361
366
  let raw = "";
362
367
  let projectsValue = "";
368
+ let workspacesValue = "";
369
+ let scopesValue = "";
363
370
  let plane = false;
364
371
  let workspace = "";
365
372
  const statusEq = [];
@@ -372,6 +379,28 @@ function parseFlags(flags) {
372
379
  plane = true;
373
380
  continue;
374
381
  }
382
+ if (token === "--workspaces" || token.startsWith("--workspaces=")) {
383
+ const parts = [];
384
+ if (token.startsWith("--workspaces="))
385
+ parts.push(token.slice("--workspaces=".length));
386
+ while (i + 1 < list.length && !list[i + 1].startsWith("-")) {
387
+ i++;
388
+ parts.push(list[i]);
389
+ }
390
+ workspacesValue = parts.join(",");
391
+ continue;
392
+ }
393
+ if (token === "--scopes" || token.startsWith("--scopes=")) {
394
+ const parts = [];
395
+ if (token.startsWith("--scopes="))
396
+ parts.push(token.slice("--scopes=".length));
397
+ while (i + 1 < list.length && !list[i + 1].startsWith("-")) {
398
+ i++;
399
+ parts.push(list[i]);
400
+ }
401
+ scopesValue = parts.join(",");
402
+ continue;
403
+ }
375
404
  if (token === "--workspace" || token.startsWith("--workspace=")) {
376
405
  const [value, next] = takeValue(list, i, "--workspace");
377
406
  workspace = value.startsWith("=") ? value.slice(1) : value;
@@ -420,9 +449,11 @@ function parseFlags(flags) {
420
449
  }
421
450
  }
422
451
  const projects = projectsValue.split(",").map((p) => p.trim().toUpperCase()).filter(Boolean);
452
+ const workspaces = uniqueSlugs(workspacesValue);
453
+ const scopes = parseScopes(scopesValue);
423
454
  const workspaceSlug = workspace.trim() || undefined;
424
455
  if (raw) {
425
- return { jql: raw, projects, plane, workspace: workspaceSlug };
456
+ return { jql: raw, projects, plane, workspace: workspaceSlug, workspaces, scopes };
426
457
  }
427
458
  const projectClause = projects.length ? `project in (${projects.map((p) => `"${p}"`).join(", ")})` : "";
428
459
  const clauses = [];
@@ -438,7 +469,47 @@ function parseFlags(flags) {
438
469
  clauses.push(`status="${status}"`);
439
470
  for (const status of statusNeq)
440
471
  clauses.push(`status!="${status}"`);
441
- return { jql: clauses.join(" AND "), projects, plane, workspace: workspaceSlug };
472
+ return { jql: clauses.join(" AND "), projects, plane, workspace: workspaceSlug, workspaces, scopes };
473
+ }
474
+ function uniqueSlugs(value) {
475
+ const seen = new Set;
476
+ const slugs = [];
477
+ for (const part of value.split(",")) {
478
+ const slug = part.trim().toLowerCase();
479
+ if (!slug || seen.has(slug))
480
+ continue;
481
+ seen.add(slug);
482
+ slugs.push(slug);
483
+ }
484
+ return slugs;
485
+ }
486
+ function parseScopes(value) {
487
+ const seen = new Set;
488
+ const scopes = [];
489
+ for (const part of value.split(",")) {
490
+ const trimmed = part.trim();
491
+ if (!trimmed)
492
+ continue;
493
+ const slash = trimmed.indexOf("/");
494
+ if (slash <= 0)
495
+ continue;
496
+ const workspace = trimmed.slice(0, slash).trim().toLowerCase();
497
+ const project = trimmed.slice(slash + 1).trim().toUpperCase();
498
+ if (!workspace || !project)
499
+ continue;
500
+ const key = `${workspace}/${project}`;
501
+ if (seen.has(key))
502
+ continue;
503
+ seen.add(key);
504
+ scopes.push({ workspace, project });
505
+ }
506
+ return scopes;
507
+ }
508
+ function setPlaneFlag(flags, on) {
509
+ const list = tokens(flags.trim()).filter((token) => token !== "--plane");
510
+ if (on)
511
+ list.unshift("--plane");
512
+ return list.join(" ");
442
513
  }
443
514
  function wantsPlane(flags) {
444
515
  return parseFlags(flags).plane;
@@ -621,6 +692,11 @@ Available states for issue ${key}: ${available}`
621
692
  issue.fields.description = input.description;
622
693
  if (input.labels !== undefined)
623
694
  issue.fields.labels = input.labels;
695
+ if (input.priority !== undefined)
696
+ issue.fields.priority = { name: input.priority };
697
+ if (input.assignee !== undefined) {
698
+ issue.fields.assignee = input.assignee ? { displayName: input.assignee } : undefined;
699
+ }
624
700
  return { ok: true };
625
701
  }
626
702
  }
@@ -972,6 +1048,10 @@ ${result.stderr}`);
972
1048
  for (const label of input.labels)
973
1049
  args.push("-l", label);
974
1050
  }
1051
+ if (input.assignee !== undefined)
1052
+ args.push("-a", input.assignee);
1053
+ if (input.priority !== undefined)
1054
+ args.push("--priority", input.priority);
975
1055
  const result = await runRetry(args);
976
1056
  if (result.code !== 0) {
977
1057
  return { ok: false, error: (result.stderr || result.stdout).trim() };
@@ -1518,11 +1598,246 @@ function createApp(opts) {
1518
1598
  },
1519
1599
  async listProjectIdentifiers(workspace) {
1520
1600
  return cli.listProjectIdentifiers?.(workspace) ?? [];
1601
+ },
1602
+ async listProjects(workspace) {
1603
+ return cli.listProjects?.(workspace) ?? [];
1604
+ },
1605
+ async parity() {
1606
+ return cli.parity?.() ?? { initiatives: [], projects: [], workspaces: [] };
1521
1607
  }
1522
1608
  };
1523
1609
  return app;
1524
1610
  }
1525
1611
 
1612
+ // src/plane-fixture.ts
1613
+ import { readFileSync as readFileSync3 } from "node:fs";
1614
+ var STATES = [
1615
+ { id: "st-backlog", name: "Backlog", group: "backlog", sequence: 1 },
1616
+ { id: "st-todo", name: "Todo", group: "unstarted", sequence: 2 },
1617
+ { id: "st-progress", name: "In Progress", group: "started", sequence: 3 },
1618
+ { id: "st-done", name: "Done", group: "completed", sequence: 4 },
1619
+ { id: "st-cancelled", name: "Cancelled", group: "cancelled", sequence: 5 }
1620
+ ];
1621
+ function mod(id, name, status, extra = {}) {
1622
+ return { id, name, status, created_at: "2026-01-04T00:00:00Z", updated_at: "2026-02-02T00:00:00Z", ...extra };
1623
+ }
1624
+ function issue(id, name, sequence, state, extra = {}) {
1625
+ return { id, name, sequence_id: sequence, state, created_at: "2026-01-06T00:00:00Z", updated_at: "2026-03-03T00:00:00Z", ...extra };
1626
+ }
1627
+ var SYNTHETIC_PLANE = {
1628
+ workspaces: [
1629
+ { id: "ws-work", name: "Work", slug: "work" },
1630
+ { id: "ws-personal", name: "Personal", slug: "personal" }
1631
+ ],
1632
+ projects: {
1633
+ work: [
1634
+ { id: "work-pulse", identifier: "PULSE", name: "Pulse" },
1635
+ { id: "work-aph", identifier: "APH", name: "Aphrodite" }
1636
+ ],
1637
+ personal: [
1638
+ { id: "personal-pulse", identifier: "PULSE", name: "Pulse" },
1639
+ { id: "personal-pkan", identifier: "PKAN", name: "Pipe kan" }
1640
+ ]
1641
+ },
1642
+ states: STATES,
1643
+ labels: [
1644
+ { id: "lbl-needs", name: "needs-input" },
1645
+ { id: "lbl-roadmap", name: "roadmap" }
1646
+ ],
1647
+ members: [
1648
+ { id: "user-ada", display_name: "Ada" },
1649
+ { id: "user-grace", display_name: "Grace" }
1650
+ ],
1651
+ modules: {
1652
+ "work-pulse": [
1653
+ mod("mod-00000000000000000000000000000001", "Backlog objective", "backlog", { priority: "low" }),
1654
+ mod("mod-00000000000000000000000000000002", "Planned objective", "todo", { priority: "medium", start_date: "2026-04-01", target_date: "2026-12-01" }),
1655
+ mod("mod-00000000000000000000000000000003", "Slipped objective", "in_progress", { priority: "high", start_date: "2025-01-01", target_date: "2025-06-01" }),
1656
+ mod("mod-00000000000000000000000000000004", "On track objective", "in_progress", { priority: "urgent", start_date: "2026-03-01", target_date: "2026-11-15" }),
1657
+ mod("mod-00000000000000000000000000000005", "Shipped objective", "done", { priority: "medium", start_date: "2025-11-01", target_date: "2026-01-20" }),
1658
+ mod("mod-00000000000000000000000000000006", "Dropped objective", "cancelled", { priority: "low", target_date: "2025-03-01" }),
1659
+ mod("mod-00000000000000000000000000000007", "Undated objective", "in_progress", { priority: "low", start_date: "2026-05-01" })
1660
+ ],
1661
+ "personal-pulse": [
1662
+ mod("mod-00000000000000000000000000000011", "Home objective", "in_progress", { priority: "medium", start_date: "2026-02-01", target_date: "2026-08-01" })
1663
+ ],
1664
+ "work-aph": [
1665
+ mod("mod-00000000000000000000000000000021", "Aphrodite objective", "todo", { priority: "high", target_date: "2026-07-01" })
1666
+ ],
1667
+ "personal-pkan": []
1668
+ },
1669
+ issues: {
1670
+ "work-pulse": [
1671
+ issue("wi-work-1", "Work pulse story", 1, "st-progress", { priority: "high", module_ids: ["mod-00000000000000000000000000000004"], assignee: "Ada", labels: ["lbl-roadmap"], target_date: "2026-11-01" }),
1672
+ issue("wi-work-2", "Work pulse done", 2, "st-done", { priority: "low", module_ids: ["mod-00000000000000000000000000000005"], assignee: "Grace" })
1673
+ ],
1674
+ "personal-pulse": [
1675
+ issue("wi-home-1", "Personal pulse story", 1, "st-todo", { priority: "medium", module_ids: ["mod-00000000000000000000000000000011"], assignee: "Ada", labels: ["lbl-needs"], target_date: "2026-08-01" })
1676
+ ],
1677
+ "work-aph": [issue("wi-aph-1", "Aphrodite story", 4, "st-backlog", { priority: "medium" })],
1678
+ "personal-pkan": [issue("wi-pkan-1", "Pipe kan story", 3, "st-todo", { priority: "low", assignee: "Grace" })]
1679
+ }
1680
+ };
1681
+ function page(results) {
1682
+ return { results, next_page_results: false, next_cursor: "" };
1683
+ }
1684
+ function planeFixtureFetch(fixture = SYNTHETIC_PLANE) {
1685
+ const data = JSON.parse(JSON.stringify(fixture));
1686
+ const stateById = new Map(data.states.map((state) => [state.id, state]));
1687
+ const labelById = new Map(data.labels.map((label) => [label.id, label]));
1688
+ return async (input, init) => {
1689
+ const url = new URL(String(input), "https://plane.test");
1690
+ const method = (init?.method ?? "GET").toUpperCase();
1691
+ const path = url.pathname.replace(/\/+$/, "") || "/";
1692
+ const parts = path.split("/").filter(Boolean);
1693
+ let body;
1694
+ if (init?.body)
1695
+ body = JSON.parse(String(init.body));
1696
+ if (method === "GET" && parts[2] === "users" && parts[4] === "workspaces") {
1697
+ return Response.json(data.workspaces);
1698
+ }
1699
+ if (method === "GET" && parts[2] === "workspaces" && parts.length === 3) {
1700
+ return Response.json(page(data.workspaces));
1701
+ }
1702
+ if (method === "GET" && /\/workspaces\/[^/]+\/projects$/.test(path)) {
1703
+ return Response.json(page(data.projects[parts[3] ?? ""] ?? []));
1704
+ }
1705
+ if (method === "GET" && parts[4] === "members" && parts.length === 5)
1706
+ return Response.json(page(data.members));
1707
+ const projectId = parts[5] ?? "";
1708
+ const collection = parts[6];
1709
+ const itemId = parts[7];
1710
+ const projectIssues = data.issues[projectId] ?? [];
1711
+ const projectModules = data.modules[projectId] ?? [];
1712
+ if (method === "GET" && collection === "states")
1713
+ return Response.json(page(data.states));
1714
+ if (method === "GET" && collection === "labels")
1715
+ return Response.json(page(data.labels));
1716
+ if (method === "GET" && collection === "modules" && !itemId)
1717
+ return Response.json(page(projectModules));
1718
+ if (method === "GET" && collection === "modules" && parts[8] === "module-issues") {
1719
+ const ids = projectIssues.filter((row) => row.module_ids?.includes(itemId ?? "")).map((row) => row.id);
1720
+ return Response.json(page(ids.map((id) => ({ issue: id, module: itemId }))));
1721
+ }
1722
+ if (method === "GET" && (collection === "work-items" || collection === "issues") && !itemId) {
1723
+ return Response.json(page(projectIssues.map((row) => toWorkItem(row, stateById, labelById))));
1724
+ }
1725
+ if (method === "PATCH" && collection === "modules" && itemId) {
1726
+ const module = projectModules.find((row) => row.id === itemId);
1727
+ if (module && typeof body?.status === "string")
1728
+ module.status = body.status;
1729
+ return Response.json({ id: itemId, ...body });
1730
+ }
1731
+ if (method === "PATCH" && (collection === "work-items" || collection === "issues") && itemId) {
1732
+ const issueRow = projectIssues.find((row) => row.id === itemId);
1733
+ if (issueRow && typeof body?.state === "string")
1734
+ issueRow.state = body.state;
1735
+ if (issueRow && typeof body?.priority === "string")
1736
+ issueRow.priority = body.priority;
1737
+ if (issueRow && typeof body?.name === "string")
1738
+ issueRow.name = body.name;
1739
+ return Response.json({ id: itemId, ...body });
1740
+ }
1741
+ if (method === "POST" && (collection === "work-items" || collection === "issues") && !itemId) {
1742
+ const sequence = 100 + projectIssues.length;
1743
+ projectIssues.push(issue(`wi-new-${sequence}`, String(body?.name ?? "New"), sequence, "st-todo"));
1744
+ return Response.json({ id: `wi-new-${sequence}`, sequence_id: sequence, name: body?.name }, { status: 201 });
1745
+ }
1746
+ return new Response(`unhandled ${method} ${path}`, { status: 404 });
1747
+ };
1748
+ }
1749
+ function toWorkItem(row, states, labels) {
1750
+ const state = states.get(row.state);
1751
+ return {
1752
+ id: row.id,
1753
+ name: row.name,
1754
+ sequence_id: row.sequence_id,
1755
+ priority: row.priority,
1756
+ state: state ? { id: state.id, name: state.name } : row.state,
1757
+ labels: row.labels ?? [],
1758
+ label_details: (row.labels ?? []).map((id) => labels.get(id)).filter(Boolean),
1759
+ module_ids: row.module_ids ?? [],
1760
+ assignees: row.assignee ? [{ display_name: row.assignee }] : [],
1761
+ created_at: row.created_at,
1762
+ updated_at: row.updated_at,
1763
+ target_date: row.target_date,
1764
+ start_date: row.start_date
1765
+ };
1766
+ }
1767
+ function resolvePlaneFetch(env = process.env) {
1768
+ const fixture = env.PLANE_FIXTURE?.trim();
1769
+ if (!fixture)
1770
+ return;
1771
+ if (fixture === "synthetic")
1772
+ return planeFixtureFetch(SYNTHETIC_PLANE);
1773
+ const parsed = JSON.parse(readFileSync3(fixture, "utf8"));
1774
+ return planeFixtureFetch(parsed);
1775
+ }
1776
+
1777
+ // src/parity.ts
1778
+ function normalizeStatus(status) {
1779
+ return status.trim().toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ");
1780
+ }
1781
+ function parseDay(value) {
1782
+ if (!value)
1783
+ return;
1784
+ const day = /^(\d{4})-(\d{2})-(\d{2})/.exec(value.trim());
1785
+ if (day)
1786
+ return new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3]));
1787
+ const parsed = new Date(value);
1788
+ if (Number.isNaN(parsed.getTime()))
1789
+ return;
1790
+ return new Date(parsed.getFullYear(), parsed.getMonth(), parsed.getDate());
1791
+ }
1792
+ function startOfToday(now) {
1793
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate());
1794
+ }
1795
+ function computeHealth(status, targetDate, now = new Date) {
1796
+ const key = normalizeStatus(status);
1797
+ if (key === "done" || key === "completed" || key === "complete")
1798
+ return "green";
1799
+ if (key === "cancelled" || key === "canceled")
1800
+ return "red";
1801
+ if (key === "in progress" || key === "started" || key === "active" || key === "paused") {
1802
+ const target = parseDay(targetDate);
1803
+ if (key === "paused")
1804
+ return "yellow";
1805
+ if (target && target.getTime() < startOfToday(now).getTime())
1806
+ return "yellow";
1807
+ return "green";
1808
+ }
1809
+ if (key === "todo" || key === "to do" || key === "backlog" || key === "planned" || key === "unstarted" || key === "") {
1810
+ return "gray";
1811
+ }
1812
+ return "gray";
1813
+ }
1814
+ function isCompletedStatus(status) {
1815
+ const key = normalizeStatus(status);
1816
+ return key === "done" || key === "completed" || key === "complete" || key === "cancelled" || key === "canceled";
1817
+ }
1818
+ function sourceOptions(input) {
1819
+ return [
1820
+ {
1821
+ id: "plane",
1822
+ label: "Plane",
1823
+ enabled: input.planeKey,
1824
+ ...input.planeKey ? {} : { reason: "Set PLANE_API_KEY to use Plane" }
1825
+ },
1826
+ {
1827
+ id: "jira",
1828
+ label: "Jira",
1829
+ enabled: true,
1830
+ ...input.jiraBin ? {} : { reason: "jira-cli is not on PATH; Fake Jira is used" }
1831
+ },
1832
+ {
1833
+ id: "taskwarrior",
1834
+ label: "Taskwarrior",
1835
+ enabled: false,
1836
+ reason: "Taskwarrior is not connected"
1837
+ }
1838
+ ];
1839
+ }
1840
+
1526
1841
  // src/plane.ts
1527
1842
  var DEFAULT_PLANE_HOST = "https://plane.tail48fe8.ts.net";
1528
1843
  var DEFAULT_PLANE_WORKSPACE = "personal";
@@ -1692,12 +2007,14 @@ function workItemToIssue(item, project, opts) {
1692
2007
  ...opts.moduleKey ? { parent: { key: opts.moduleKey } } : {},
1693
2008
  ...labels.length ? { labels } : {},
1694
2009
  ...asString(item.created_at) ? { created: item.created_at } : {},
2010
+ ...asString(item.updated_at) ? { updated: item.updated_at } : {},
1695
2011
  ...description ? { description } : {}
1696
2012
  }
1697
2013
  };
1698
2014
  }
1699
2015
  function moduleToIssue(module, project) {
1700
2016
  const target = asString(module.target_date);
2017
+ const priority = titleCasePriority(module.priority);
1701
2018
  return {
1702
2019
  key: planeModuleKey(project.identifier, module.id),
1703
2020
  fields: {
@@ -1705,8 +2022,10 @@ function moduleToIssue(module, project) {
1705
2022
  status: { name: moduleStatusName(module.status) },
1706
2023
  issuetype: { name: "Epic" },
1707
2024
  issueType: { name: "Epic" },
2025
+ ...priority ? { priority: { name: priority } } : {},
1708
2026
  ...target ? { duedate: target, "Target End Date": target } : {},
1709
- ...asString(module.created_at) ? { created: module.created_at } : {}
2027
+ ...asString(module.created_at) ? { created: module.created_at } : {},
2028
+ ...asString(module.updated_at) ? { updated: module.updated_at } : {}
1710
2029
  }
1711
2030
  };
1712
2031
  }
@@ -1936,25 +2255,76 @@ function createPlaneCli(opts) {
1936
2255
  status: asString(row?.status),
1937
2256
  created_at: asString(row?.created_at),
1938
2257
  target_date: asString(row?.target_date),
1939
- start_date: asString(row?.start_date)
2258
+ start_date: asString(row?.start_date),
2259
+ priority: asString(row?.priority),
2260
+ updated_at: asString(row?.updated_at)
1940
2261
  };
1941
2262
  }
2263
+ function catalogPlan(flags) {
2264
+ const parsed = parseFlags(flags || defaultFlags);
2265
+ if (parsed.scopes.length) {
2266
+ return {
2267
+ slugs: [...new Set(parsed.scopes.map((scope) => scope.workspace))],
2268
+ pairs: parsed.scopes
2269
+ };
2270
+ }
2271
+ if (parsed.workspaces.length)
2272
+ return { slugs: parsed.workspaces, pairs: null };
2273
+ return { slugs: [workspaceOf(flags)], pairs: null };
2274
+ }
2275
+ function exposeKey(slug, key, multi) {
2276
+ return multi ? `${slug}/${key}` : key;
2277
+ }
2278
+ function prefixIssue(issue, slug, multi) {
2279
+ if (!multi || typeof issue.key !== "string")
2280
+ return issue;
2281
+ const parent = asRecord(issue.fields?.parent);
2282
+ const parentKey = asString(parent?.key);
2283
+ return {
2284
+ ...issue,
2285
+ key: exposeKey(slug, issue.key, true),
2286
+ fields: {
2287
+ ...issue.fields,
2288
+ ...parentKey ? { parent: { ...parent, key: exposeKey(slug, parentKey, true) } } : {}
2289
+ }
2290
+ };
2291
+ }
2292
+ const projectCache = new Map;
2293
+ const cacheTtlMs = opts.cacheTtlMs ?? 30000;
2294
+ async function projectsFor(slug) {
2295
+ const hit = projectCache.get(slug);
2296
+ const now = nowFn();
2297
+ if (cacheTtlMs > 0 && hit && now - hit.at < cacheTtlMs)
2298
+ return hit.rows;
2299
+ const rows = (await paginate(`/workspaces/${slug}/projects/`)).map(parseProject).filter((row) => !!row).map((project) => ({ ...project, workspace: slug }));
2300
+ projectCache.set(slug, { at: now, rows });
2301
+ return rows;
2302
+ }
1942
2303
  async function loadCatalog(flags) {
1943
2304
  lastFlags = flags || defaultFlags;
1944
- const workspace = workspaceOf(lastFlags);
1945
- if (catalog && catalog.flags === lastFlags && catalog.workspace === workspace)
2305
+ const plan = catalogPlan(lastFlags);
2306
+ const cacheKey = plan.slugs.join(",");
2307
+ if (catalog && catalog.flags === lastFlags && catalog.cacheKey === cacheKey)
1946
2308
  return catalog;
1947
- const projects = scopedProjects((await paginate(`/workspaces/${workspace}/projects/`)).map(parseProject).filter((row) => !!row), lastFlags);
2309
+ const multi = plan.slugs.length > 1;
2310
+ const projects = [];
1948
2311
  const states = new Map;
1949
2312
  const labels = new Map;
1950
2313
  const works = new Map;
1951
2314
  const modules = new Map;
1952
2315
  const columnStates = [];
2316
+ for (const slug of plan.slugs) {
2317
+ const listed = await projectsFor(slug);
2318
+ const chosen = plan.pairs ? listed.filter((project) => plan.pairs.some((pair) => pair.workspace === slug && pair.project === project.identifier.toUpperCase())) : scopedProjects(listed, lastFlags);
2319
+ for (const project of chosen)
2320
+ projects.push(project);
2321
+ }
1953
2322
  for (const project of projects) {
1954
- const stateRows = await paginate(`/workspaces/${workspace}/projects/${project.id}/states/`);
1955
- const labelRows = await paginate(`/workspaces/${workspace}/projects/${project.id}/labels/`);
1956
- const moduleRows = await paginate(`/workspaces/${workspace}/projects/${project.id}/modules/`);
1957
- const workRows = await withResource((resource) => paginate(queryPath(`/workspaces/${workspace}/projects/${project.id}/${resource}/`, {
2323
+ const slug = project.workspace || plan.slugs[0] || workspaceOf(lastFlags);
2324
+ const stateRows = await paginate(`/workspaces/${slug}/projects/${project.id}/states/`);
2325
+ const labelRows = await paginate(`/workspaces/${slug}/projects/${project.id}/labels/`);
2326
+ const moduleRows = await paginate(`/workspaces/${slug}/projects/${project.id}/modules/`);
2327
+ const workRows = await withResource((resource) => paginate(queryPath(`/workspaces/${slug}/projects/${project.id}/${resource}/`, {
1958
2328
  expand: "assignees,labels,state,module"
1959
2329
  })));
1960
2330
  const stateById = new Map;
@@ -1976,7 +2346,7 @@ function createPlaneCli(opts) {
1976
2346
  const module = parseModule(row);
1977
2347
  if (!module)
1978
2348
  continue;
1979
- const issue = moduleToIssue(module, project);
2349
+ const issue = prefixIssue(moduleToIssue(module, project), slug, multi);
1980
2350
  const cached = { issue, module, project };
1981
2351
  moduleById.set(module.id, cached);
1982
2352
  modules.set(String(issue.key), cached);
@@ -1988,7 +2358,7 @@ function createPlaneCli(opts) {
1988
2358
  const issueByModule = new Map;
1989
2359
  if (missingModules && moduleById.size) {
1990
2360
  for (const moduleId of moduleById.keys()) {
1991
- const rows = await paginate(`/workspaces/${workspace}/projects/${project.id}/modules/${moduleId}/module-issues/`);
2361
+ const rows = await paginate(`/workspaces/${slug}/projects/${project.id}/modules/${moduleId}/module-issues/`);
1992
2362
  const ids = rows.map((row) => asId(asRecord(row)?.issue) ?? asId(row)).filter((id) => !!id);
1993
2363
  issueByModule.set(moduleId, ids);
1994
2364
  }
@@ -1998,26 +2368,29 @@ function createPlaneCli(opts) {
1998
2368
  const fromItem = moduleIdsOf(item)[0];
1999
2369
  const fromJoin = [...issueByModule.entries()].find(([, ids]) => ids.includes(item.id))?.[0];
2000
2370
  const moduleId = fromItem ?? fromJoin;
2001
- const moduleKey = moduleId ? moduleById.get(moduleId)?.issue.key : undefined;
2371
+ const bareModuleKey = moduleId ? planeModuleKey(project.identifier, moduleId) : undefined;
2002
2372
  const issue = workItemToIssue(item, project, {
2003
2373
  states: stateById,
2004
2374
  labels: labelById,
2005
- moduleKey: typeof moduleKey === "string" ? moduleKey : undefined
2375
+ ...bareModuleKey ? { moduleKey: bareModuleKey } : {}
2006
2376
  });
2007
- const issueKey = typeof issue?.key === "string" ? issue.key : undefined;
2008
- if (!issue || !issueKey)
2377
+ const stamped = issue ? prefixIssue(issue, slug, multi) : undefined;
2378
+ const issueKey = typeof stamped?.key === "string" ? stamped.key : undefined;
2379
+ if (!stamped || !issueKey)
2009
2380
  continue;
2381
+ const linked = bareModuleKey ? exposeKey(slug, bareModuleKey, multi) : undefined;
2010
2382
  works.set(issueKey, {
2011
- issue,
2383
+ issue: stamped,
2012
2384
  item,
2013
2385
  project,
2014
- ...typeof moduleKey === "string" ? { moduleKey } : {}
2386
+ ...linked ? { moduleKey: linked } : {}
2015
2387
  });
2016
2388
  }
2017
2389
  }
2018
2390
  catalog = {
2019
2391
  flags: lastFlags,
2020
- workspace,
2392
+ workspace: plan.slugs[0] || workspaceOf(lastFlags),
2393
+ cacheKey,
2021
2394
  projects,
2022
2395
  states,
2023
2396
  labels,
@@ -2027,6 +2400,73 @@ function createPlaneCli(opts) {
2027
2400
  };
2028
2401
  return catalog;
2029
2402
  }
2403
+ function projectSlug(project) {
2404
+ return project.workspace || workspaceOf(lastFlags);
2405
+ }
2406
+ function buildParity(loaded) {
2407
+ const projects = loaded.projects.map((project) => {
2408
+ const issues = [...loaded.works.values()].filter((row) => row.project.id === project.id && row.project.workspace === project.workspace);
2409
+ const modules = [...loaded.modules.values()].filter((row) => row.project.id === project.id && row.project.workspace === project.workspace);
2410
+ const total = issues.length;
2411
+ const completed = issues.filter((row) => {
2412
+ const name = typeof row.issue.fields?.status?.name === "string" ? row.issue.fields.status.name : "";
2413
+ const key = name.toLowerCase();
2414
+ return key === "done" || key === "completed";
2415
+ }).length;
2416
+ const started = issues.some((row) => {
2417
+ const name = typeof row.issue.fields?.status?.name === "string" ? row.issue.fields.status.name : "";
2418
+ return name.toLowerCase() === "in progress";
2419
+ });
2420
+ const status = total > 0 && completed === total ? "done" : started ? "in_progress" : "backlog";
2421
+ const dates = [
2422
+ ...modules.map((row) => row.module.target_date),
2423
+ ...issues.map((row) => row.item.target_date)
2424
+ ].filter((value) => Boolean(value));
2425
+ const starts = [
2426
+ ...modules.map((row) => row.module.start_date),
2427
+ ...issues.map((row) => row.item.start_date)
2428
+ ].filter((value) => Boolean(value));
2429
+ const targetDate = dates.sort().at(-1);
2430
+ const startDate = starts.sort()[0];
2431
+ return {
2432
+ workspace: project.workspace || loaded.workspace,
2433
+ id: project.id,
2434
+ identifier: project.identifier,
2435
+ name: project.name,
2436
+ total,
2437
+ completed,
2438
+ status,
2439
+ ...startDate ? { startDate } : {},
2440
+ ...targetDate ? { targetDate } : {},
2441
+ health: computeHealth(status, targetDate)
2442
+ };
2443
+ });
2444
+ const active = new Map(projects.map((project) => [`${project.workspace}/${project.id}`, project.completed < project.total || project.total === 0]));
2445
+ const initiatives = [...loaded.modules.values()].map((row) => {
2446
+ const raw = row.module.status ?? "backlog";
2447
+ const priority = titleCasePriority(row.module.priority);
2448
+ const open = active.get(`${row.project.workspace}/${row.project.id}`) !== false;
2449
+ return {
2450
+ key: String(row.issue.key),
2451
+ name: row.module.name,
2452
+ workspace: row.project.workspace || loaded.workspace,
2453
+ project: row.project.identifier,
2454
+ status: moduleStatusName(raw),
2455
+ rawStatus: raw,
2456
+ ...priority ? { priority } : {},
2457
+ ...row.module.start_date ? { startDate: row.module.start_date } : {},
2458
+ ...row.module.target_date ? { targetDate: row.module.target_date } : {},
2459
+ ...row.module.updated_at ? { updated: row.module.updated_at } : {},
2460
+ health: computeHealth(raw, row.module.target_date),
2461
+ activeProjects: isCompletedStatus(raw) || !open ? 0 : 1
2462
+ };
2463
+ });
2464
+ return {
2465
+ initiatives,
2466
+ projects,
2467
+ workspaces: [...new Set(loaded.projects.map((project) => project.workspace || loaded.workspace))]
2468
+ };
2469
+ }
2030
2470
  function filterWorks(flags, works) {
2031
2471
  const parsed = parseFlags(flags || defaultFlags);
2032
2472
  return works.filter(({ issue }) => {
@@ -2136,7 +2576,20 @@ function createPlaneCli(opts) {
2136
2576
  const slug = workspace.trim();
2137
2577
  if (!slug)
2138
2578
  return [];
2139
- return (await paginate(`/workspaces/${slug}/projects/`)).map(parseProject).filter((row) => !!row).map((row) => row.identifier);
2579
+ return (await projectsFor(slug)).map((row) => row.identifier);
2580
+ },
2581
+ async listProjects(workspace) {
2582
+ const slug = workspace.trim();
2583
+ if (!slug)
2584
+ return [];
2585
+ return (await projectsFor(slug)).map((row) => ({
2586
+ id: row.id,
2587
+ identifier: row.identifier,
2588
+ name: row.name
2589
+ }));
2590
+ },
2591
+ async parity(flags) {
2592
+ return buildParity(await loadCatalog(flags || lastFlags));
2140
2593
  },
2141
2594
  async move(key, status) {
2142
2595
  try {
@@ -2155,7 +2608,7 @@ function createPlaneCli(opts) {
2155
2608
  }).find(([name]) => name === status.toLowerCase())?.[1];
2156
2609
  if (!mapped)
2157
2610
  return { ok: false, error: `Unknown Plane module status "${status}"` };
2158
- await request(`/workspaces/${workspaceOf(lastFlags)}/projects/${module.project.id}/modules/${module.module.id}/`, { method: "PATCH", body: JSON.stringify({ status: mapped }) });
2611
+ await request(`/workspaces/${projectSlug(module.project)}/projects/${module.project.id}/modules/${module.module.id}/`, { method: "PATCH", body: JSON.stringify({ status: mapped }) });
2159
2612
  catalog = undefined;
2160
2613
  return { ok: true };
2161
2614
  }
@@ -2165,7 +2618,7 @@ function createPlaneCli(opts) {
2165
2618
  const state = await resolveStateId(work.project, status, lastFlags);
2166
2619
  if (!state)
2167
2620
  return { ok: false, error: `Unknown Plane state "${status}" for ${key}` };
2168
- await withResource((resource) => request(`/workspaces/${workspaceOf(lastFlags)}/projects/${work.project.id}/${resource}/${work.item.id}/`, { method: "PATCH", body: JSON.stringify({ state }) }));
2621
+ await withResource((resource) => request(`/workspaces/${projectSlug(work.project)}/projects/${work.project.id}/${resource}/${work.item.id}/`, { method: "PATCH", body: JSON.stringify({ state }) }));
2169
2622
  catalog = undefined;
2170
2623
  return { ok: true };
2171
2624
  } catch (err) {
@@ -2188,14 +2641,14 @@ function createPlaneCli(opts) {
2188
2641
  body.state = state;
2189
2642
  if (labels.length)
2190
2643
  body.labels = labels;
2191
- const created = asRecord(await withResource((resource) => requestJson(`/workspaces/${workspaceOf(lastFlags)}/projects/${project.id}/${resource}/`, {
2644
+ const created = asRecord(await withResource((resource) => requestJson(`/workspaces/${projectSlug(project)}/projects/${project.id}/${resource}/`, {
2192
2645
  method: "POST",
2193
2646
  body: JSON.stringify(body)
2194
2647
  })));
2195
2648
  const sequence = Number(created?.sequence_id);
2196
2649
  const key = Number.isFinite(sequence) && sequence > 0 ? planeWorkItemKey(project.identifier, sequence) : undefined;
2197
2650
  if (parent && created?.id) {
2198
- await request(`/workspaces/${workspaceOf(lastFlags)}/projects/${project.id}/modules/${parent.module.id}/module-issues/`, { method: "POST", body: JSON.stringify({ issues: [created.id] }) });
2651
+ await request(`/workspaces/${projectSlug(project)}/projects/${project.id}/modules/${parent.module.id}/module-issues/`, { method: "POST", body: JSON.stringify({ issues: [created.id] }) });
2199
2652
  }
2200
2653
  catalog = undefined;
2201
2654
  return key ? { ok: true, key } : { ok: true };
@@ -2205,6 +2658,24 @@ function createPlaneCli(opts) {
2205
2658
  },
2206
2659
  async edit(key, input) {
2207
2660
  try {
2661
+ const loaded = await loadCatalog(lastFlags);
2662
+ const module = loaded.modules.get(key);
2663
+ if (module) {
2664
+ if (input.assignee !== undefined)
2665
+ return { ok: false, error: "Plane modules have no assignee field" };
2666
+ if (input.labels)
2667
+ return { ok: false, error: "Plane modules have no labels field" };
2668
+ const body = {};
2669
+ if (input.summary !== undefined)
2670
+ body.name = input.summary;
2671
+ if (input.priority !== undefined)
2672
+ body.priority = input.priority.toLowerCase();
2673
+ if (Object.keys(body).length) {
2674
+ await request(`/workspaces/${projectSlug(module.project)}/projects/${module.project.id}/modules/${module.module.id}/`, { method: "PATCH", body: JSON.stringify(body) });
2675
+ }
2676
+ catalog = undefined;
2677
+ return { ok: true };
2678
+ }
2208
2679
  const work = await lookupByKey(key, lastFlags);
2209
2680
  if (!work)
2210
2681
  return { ok: false, error: `Work item ${key} not found` };
@@ -2215,7 +2686,19 @@ function createPlaneCli(opts) {
2215
2686
  body.description_html = htmlParagraph(input.description);
2216
2687
  if (input.labels)
2217
2688
  body.labels = await resolveLabelIds(work.project, input.labels, lastFlags);
2218
- await withResource((resource) => request(`/workspaces/${workspaceOf(lastFlags)}/projects/${work.project.id}/${resource}/${work.item.id}/`, { method: "PATCH", body: JSON.stringify(body) }));
2689
+ if (input.priority !== undefined)
2690
+ body.priority = input.priority.toLowerCase();
2691
+ if (input.assignee !== undefined) {
2692
+ const slug = projectSlug(work.project);
2693
+ const members = (await paginate(`/workspaces/${slug}/members/`)).map((row) => asRecord(row));
2694
+ const wanted = input.assignee.trim().toLowerCase();
2695
+ const member = members.find((row) => asString(row?.display_name)?.toLowerCase() === wanted);
2696
+ const memberId = asString(member?.id);
2697
+ if (wanted && !memberId)
2698
+ return { ok: false, error: `Plane member "${input.assignee}" was not found` };
2699
+ body.assignees = memberId ? [memberId] : [];
2700
+ }
2701
+ await withResource((resource) => request(`/workspaces/${projectSlug(work.project)}/projects/${work.project.id}/${resource}/${work.item.id}/`, { method: "PATCH", body: JSON.stringify(body) }));
2219
2702
  catalog = undefined;
2220
2703
  return { ok: true };
2221
2704
  } catch (err) {
@@ -2223,7 +2706,12 @@ function createPlaneCli(opts) {
2223
2706
  }
2224
2707
  },
2225
2708
  async open(key) {
2226
- return `${host}/${workspaceOf(lastFlags)}/browse/${key}`;
2709
+ const loaded = await loadCatalog(lastFlags);
2710
+ const module = loaded.modules.get(key);
2711
+ const work = loaded.works.get(key);
2712
+ const slug = module?.project.workspace || work?.project.workspace || workspaceOf(lastFlags);
2713
+ const bare = key.includes("/") ? key.slice(key.indexOf("/") + 1) : key;
2714
+ return `${host}/${slug}/browse/${bare}`;
2227
2715
  },
2228
2716
  async view(key) {
2229
2717
  const loaded = await loadCatalog(lastFlags);
@@ -2245,13 +2733,15 @@ async function createBoardApp(opts) {
2245
2733
  const store = IssueStore.fromRaw(opts.raw);
2246
2734
  const plane = wantsPlane(flags);
2247
2735
  const bin = resolveJiraBin(env.JIRA_BIN ?? "jira", env.PATH ?? "");
2736
+ const fixtureMode = Boolean(env.PLANE_FIXTURE?.trim());
2737
+ const useJira = Boolean(bin) && !fixtureMode;
2248
2738
  const cli = plane ? createPlaneCli({
2249
2739
  host: planeHost(env),
2250
2740
  apiKey: env.PLANE_API_KEY ?? "",
2251
2741
  workspace: planeWorkspace(flags, env),
2252
2742
  flags,
2253
- fetch: opts.fetch
2254
- }) : bin ? createJiraCli({
2743
+ fetch: opts.fetch ?? resolvePlaneFetch(env)
2744
+ }) : useJira ? createJiraCli({
2255
2745
  bin,
2256
2746
  configPath: env.JIRA_CONFIG_FILE,
2257
2747
  token: env.JIRA_API_TOKEN,
@@ -2270,7 +2760,7 @@ async function createBoardApp(opts) {
2270
2760
  const local = createStoreCli(store, flags);
2271
2761
  app.hydrate(JSON.parse(await local.list(app.flags)), { fromStore: true });
2272
2762
  }
2273
- return { app, store, kind: plane ? "plane" : bin ? "jira" : "store" };
2763
+ return { app, store, kind: plane ? "plane" : useJira ? "jira" : "store" };
2274
2764
  }
2275
2765
  async function refreshFromJira(app, kind, opts = {}) {
2276
2766
  if (opts.piped)
@@ -2281,7 +2771,7 @@ async function refreshFromJira(app, kind, opts = {}) {
2281
2771
  }
2282
2772
 
2283
2773
  // src/factory-ledger.ts
2284
- import { existsSync as existsSync2, readFileSync as readFileSync3, statSync } from "node:fs";
2774
+ import { existsSync as existsSync2, readFileSync as readFileSync4, statSync } from "node:fs";
2285
2775
  import { homedir as homedir3 } from "node:os";
2286
2776
  import { join as join4 } from "node:path";
2287
2777
  var DEFAULT_LEDGER_PATH = "~/code/software-factory/asf/runtime/factory-coordinator/jobs.jsonl";
@@ -2388,7 +2878,7 @@ function readLedger(env = process.env) {
2388
2878
  if (_cache && _cache.path === path && _cache.mtime === mtime) {
2389
2879
  return { jobs: _cache.jobs };
2390
2880
  }
2391
- const text = readFileSync3(path, "utf8");
2881
+ const text = readFileSync4(path, "utf8");
2392
2882
  const jobs = classifyJobs(parseJobsJsonl(text));
2393
2883
  _cache = { path, mtime, jobs };
2394
2884
  return { jobs };
@@ -2456,7 +2946,9 @@ function handleAppApi(req, res, app, opts = {}) {
2456
2946
  return;
2457
2947
  }
2458
2948
  try {
2459
- json(res, 200, { projects: await app.listProjectIdentifiers?.(workspace) ?? [] });
2949
+ const projects = await app.listProjectIdentifiers?.(workspace) ?? [];
2950
+ const details = await app.listProjects?.(workspace) ?? [];
2951
+ json(res, 200, { projects, ...details.length ? { details } : {} });
2460
2952
  } catch (err) {
2461
2953
  const message = err instanceof Error ? err.message : "catalog failed";
2462
2954
  json(res, 409, { error: message });
@@ -2464,6 +2956,38 @@ function handleAppApi(req, res, app, opts = {}) {
2464
2956
  });
2465
2957
  return true;
2466
2958
  }
2959
+ if (url.pathname === "/api/parity" && method === "GET") {
2960
+ reply(req, res, async () => {
2961
+ json(res, 200, await app.parity?.() ?? { initiatives: [], projects: [], workspaces: [] });
2962
+ });
2963
+ return true;
2964
+ }
2965
+ if (url.pathname === "/api/sources" && method === "GET") {
2966
+ reply(req, res, async () => {
2967
+ const current = kind === "plane" ? "plane" : "jira";
2968
+ json(res, 200, {
2969
+ source: current,
2970
+ kind,
2971
+ sources: sourceOptions({
2972
+ planeKey: Boolean(env.PLANE_API_KEY?.trim() || env.PLANE_FIXTURE?.trim()),
2973
+ jiraBin: Boolean(resolveJiraBin(env.JIRA_BIN ?? "jira", env.PATH ?? ""))
2974
+ })
2975
+ });
2976
+ });
2977
+ return true;
2978
+ }
2979
+ if (url.pathname === "/api/source" && method === "POST") {
2980
+ reply(req, res, async (text) => {
2981
+ if (!opts.onSource) {
2982
+ json(res, 404, { error: "source switch unavailable" });
2983
+ return;
2984
+ }
2985
+ const body = text ? JSON.parse(text) : {};
2986
+ const result = await opts.onSource(String(body.source ?? ""));
2987
+ json(res, result.ok ? 200 : 409, result);
2988
+ });
2989
+ return true;
2990
+ }
2467
2991
  if (url.pathname === "/api/refresh" && method === "POST") {
2468
2992
  reply(req, res, async (text) => {
2469
2993
  const body = text ? JSON.parse(text) : {};
@@ -2533,6 +3057,10 @@ function handleAppApi(req, res, app, opts = {}) {
2533
3057
  input.summary = String(body.summary);
2534
3058
  if (body.description !== undefined)
2535
3059
  input.description = String(body.description);
3060
+ if (body.assignee !== undefined)
3061
+ input.assignee = String(body.assignee);
3062
+ if (body.priority !== undefined)
3063
+ input.priority = String(body.priority);
2536
3064
  if (Array.isArray(body.labels)) {
2537
3065
  input.labels = body.labels.filter((item) => typeof item === "string");
2538
3066
  }
@@ -2650,7 +3178,7 @@ function formatCard(card) {
2650
3178
  }
2651
3179
 
2652
3180
  // src/server/agent/config.ts
2653
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
3181
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
2654
3182
  import { homedir as homedir4 } from "node:os";
2655
3183
  import { delimiter as delimiter2, dirname as dirname2, join as join5 } from "node:path";
2656
3184
  var DEFAULT_CONFIG_PATH = join5(homedir4(), ".config", "pipe-kan", "agent.json");
@@ -2709,7 +3237,7 @@ function loadAgentConfig(path = agentConfigPath()) {
2709
3237
  if (!existsSync3(path))
2710
3238
  return structuredClone(DEFAULT_CONFIG);
2711
3239
  try {
2712
- const raw = JSON.parse(readFileSync4(path, "utf8"));
3240
+ const raw = JSON.parse(readFileSync5(path, "utf8"));
2713
3241
  return mergeConfig(raw);
2714
3242
  } catch {
2715
3243
  return structuredClone(DEFAULT_CONFIG);
@@ -3236,7 +3764,7 @@ function parsedType(data) {
3236
3764
  }
3237
3765
  return t;
3238
3766
  }
3239
- function issue(...args) {
3767
+ function issue2(...args) {
3240
3768
  const [iss, input, inst] = args;
3241
3769
  if (typeof iss === "string") {
3242
3770
  return {
@@ -5663,7 +6191,7 @@ function handleRefineResult(result, payload, input, inst) {
5663
6191
  };
5664
6192
  if (inst._zod.def.params)
5665
6193
  _iss.params = inst._zod.def.params;
5666
- payload.issues.push(issue(_iss));
6194
+ payload.issues.push(issue2(_iss));
5667
6195
  }
5668
6196
  }
5669
6197
  // node_modules/zod/v4/core/memoizer.js
@@ -6489,11 +7017,11 @@ function _refine(Class, fn, _params) {
6489
7017
  }
6490
7018
  function _superRefine(fn, params) {
6491
7019
  const ch = _check((payload) => {
6492
- payload.addIssue = (issue2) => {
6493
- if (typeof issue2 === "string") {
6494
- payload.issues.push(issue(issue2, payload.value, ch._zod.def));
7020
+ payload.addIssue = (issue) => {
7021
+ if (typeof issue === "string") {
7022
+ payload.issues.push(issue2(issue, payload.value, ch._zod.def));
6495
7023
  } else {
6496
- const _issue = issue2;
7024
+ const _issue = issue;
6497
7025
  if (_issue.fatal)
6498
7026
  _issue.continue = false;
6499
7027
  _issue.code ?? (_issue.code = "custom");
@@ -6501,7 +7029,7 @@ function _superRefine(fn, params) {
6501
7029
  _issue.input = payload.value;
6502
7030
  _issue.inst ?? (_issue.inst = ch);
6503
7031
  _issue.continue ?? (_issue.continue = !ch._zod.def.abort);
6504
- payload.issues.push(issue(_issue));
7032
+ payload.issues.push(issue2(_issue));
6505
7033
  }
6506
7034
  };
6507
7035
  return fn(payload.value, payload);
@@ -8243,18 +8771,18 @@ var ZodTransform = /* @__PURE__ */ $constructor("ZodTransform", (inst, def) => {
8243
8771
  if (_ctx.direction === "backward") {
8244
8772
  throw new $ZodEncodeError(inst.constructor.name);
8245
8773
  }
8246
- payload.addIssue = (issue2) => {
8247
- if (typeof issue2 === "string") {
8248
- payload.issues.push(issue(issue2, payload.value, def));
8774
+ payload.addIssue = (issue) => {
8775
+ if (typeof issue === "string") {
8776
+ payload.issues.push(issue2(issue, payload.value, def));
8249
8777
  } else {
8250
- const _issue = issue2;
8778
+ const _issue = issue;
8251
8779
  if (_issue.fatal)
8252
8780
  _issue.continue = false;
8253
8781
  _issue.code ?? (_issue.code = "custom");
8254
8782
  if (!("input" in _issue))
8255
8783
  _issue.input = payload.value;
8256
8784
  _issue.inst ?? (_issue.inst = inst);
8257
- payload.issues.push(issue(_issue));
8785
+ payload.issues.push(issue2(_issue));
8258
8786
  }
8259
8787
  };
8260
8788
  const output = def.transform(payload.value, payload);
@@ -12791,7 +13319,7 @@ function permissionOptionForDecision(options, decision) {
12791
13319
  }
12792
13320
 
12793
13321
  // src/server/agent/skills.ts
12794
- import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync } from "node:fs";
13322
+ import { existsSync as existsSync4, readFileSync as readFileSync6, readdirSync } from "node:fs";
12795
13323
  import { homedir as homedir5 } from "node:os";
12796
13324
  import { join as join7 } from "node:path";
12797
13325
  import { fileURLToPath } from "node:url";
@@ -12840,7 +13368,7 @@ function skillPath(dir, id) {
12840
13368
  }
12841
13369
  function readSkill(path, id) {
12842
13370
  try {
12843
- const text = readFileSync5(path, "utf8");
13371
+ const text = readFileSync6(path, "utf8");
12844
13372
  const front = parseFrontMatter(text);
12845
13373
  return {
12846
13374
  id,
@@ -12867,7 +13395,7 @@ function parseFrontMatter(text) {
12867
13395
  }
12868
13396
 
12869
13397
  // src/server/agent/tools.ts
12870
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
13398
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
12871
13399
  import { join as join8 } from "node:path";
12872
13400
  var TOOLS = [
12873
13401
  {
@@ -13017,7 +13545,7 @@ var EXECUTORS = {
13017
13545
  return { ok: false, error: "Path traversal not allowed" };
13018
13546
  const repoRoot = process.cwd();
13019
13547
  try {
13020
- const text = readFileSync6(join8(repoRoot, relPath), "utf8");
13548
+ const text = readFileSync7(join8(repoRoot, relPath), "utf8");
13021
13549
  return { ok: true, value: { path: relPath, text } };
13022
13550
  } catch (err) {
13023
13551
  return { ok: false, error: String(err) };
@@ -13641,7 +14169,7 @@ function handleFakeJira(req, res, store) {
13641
14169
 
13642
14170
  // src/http.ts
13643
14171
  function handleRequest(req, res, ctx) {
13644
- if (handleAppApi(req, res, ctx.app, { kind: ctx.kind }) || handleAgentApi(req, res, ctx.app)) {
14172
+ if (handleAppApi(req, res, ctx.app, { kind: ctx.kind, env: ctx.env, onSource: ctx.switchSource }) || handleAgentApi(req, res, ctx.app)) {
13645
14173
  return true;
13646
14174
  }
13647
14175
  if (ctx.kind === "plane")
@@ -13757,15 +14285,51 @@ function announce(line) {
13757
14285
  }
13758
14286
  async function runServer(opts) {
13759
14287
  const piped = await readPipe();
13760
- const raw = piped ?? JSON.parse(readFileSync7(join11(opts.root, "fixtures/issues.json"), "utf8"));
14288
+ const raw = piped ?? JSON.parse(readFileSync8(join11(opts.root, "fixtures/issues.json"), "utf8"));
13761
14289
  const flags = argvToFlags(process.argv);
13762
- const { app, store, kind } = await createBoardApp({
14290
+ const booted = await createBoardApp({
13763
14291
  raw,
13764
14292
  piped: Boolean(piped),
13765
14293
  flags
13766
14294
  });
14295
+ const live = {
14296
+ app: booted.app,
14297
+ store: booted.store,
14298
+ kind: booted.kind,
14299
+ raw
14300
+ };
14301
+ async function switchSource(source) {
14302
+ if (source === "taskwarrior") {
14303
+ return { ok: false, kind: live.kind, source, reason: "Taskwarrior is not connected" };
14304
+ }
14305
+ if (source !== "plane" && source !== "jira") {
14306
+ return { ok: false, kind: live.kind, source, error: `Unknown source ${source}` };
14307
+ }
14308
+ if (source === "plane" && !process.env.PLANE_API_KEY?.trim()) {
14309
+ return { ok: false, kind: live.kind, source, reason: "Set PLANE_API_KEY to use Plane" };
14310
+ }
14311
+ const nextFlags = setPlaneFlag(live.app.flags, source === "plane");
14312
+ try {
14313
+ const next = await createBoardApp({ raw: live.raw, piped: false, flags: nextFlags });
14314
+ live.app = next.app;
14315
+ live.store = next.store;
14316
+ live.kind = next.kind;
14317
+ if (next.kind === "plane" || next.kind === "jira") {
14318
+ try {
14319
+ await refreshFromJira(live.app, next.kind);
14320
+ } catch (err) {
14321
+ const message = err instanceof Error ? err.message : String(err);
14322
+ return { ok: true, kind: live.kind, source, error: message };
14323
+ }
14324
+ }
14325
+ return { ok: true, kind: live.kind, source };
14326
+ } catch (err) {
14327
+ const message = err instanceof Error ? err.message : String(err);
14328
+ return { ok: false, kind: live.kind, source, error: message };
14329
+ }
14330
+ }
13767
14331
  const server = createServer((req, res) => {
13768
- if (handleRequest(req, res, { app, store, kind }))
14332
+ if (handleRequest(req, res, { app: live.app, store: live.store, kind: live.kind, switchSource }))
13769
14333
  return;
13770
14334
  if (opts.fallback) {
13771
14335
  opts.fallback(req, res);
@@ -13776,34 +14340,34 @@ async function runServer(opts) {
13776
14340
  res.end("pipe-kan UI is missing. Run bun run build.");
13777
14341
  }
13778
14342
  });
13779
- if (kind === "plane") {
14343
+ if (live.kind === "plane") {
13780
14344
  try {
13781
- await refreshFromJira(app, kind, { piped: Boolean(piped) });
14345
+ await refreshFromJira(live.app, live.kind, { piped: Boolean(piped) });
13782
14346
  } catch (err) {
13783
- console.error(`${kind} Refresh failed`, err);
14347
+ console.error(`${live.kind} Refresh failed`, err);
13784
14348
  }
13785
- const error = app.board().error;
14349
+ const error = live.app.board().error;
13786
14350
  if (error)
13787
- console.error(`${kind} Refresh failed; keeping last live Board`, error);
14351
+ console.error(`${live.kind} Refresh failed; keeping last live Board`, error);
13788
14352
  }
13789
14353
  const { host, port } = resolveListen();
13790
14354
  await bindListen(server, host, port);
13791
14355
  const origin = `http://127.0.0.1:${port}`;
13792
14356
  announce(`pipe-kan http://${host}:${port}`);
13793
- if (kind === "plane") {
14357
+ if (live.kind === "plane") {
13794
14358
  announce(`cli plane ${planeHost()} ${planeWorkspace(flags)}`);
13795
14359
  announce(`Plane host default ${DEFAULT_PLANE_HOST}`);
13796
14360
  } else {
13797
14361
  const fakeConfig = writeJiraConfig(join11(tmpdir3(), "pipe-kan"), origin);
13798
- announce(`cli ${kind === "jira" ? resolveJiraBin() : "store"}`);
14362
+ announce(`cli ${live.kind === "jira" ? resolveJiraBin() : "store"}`);
13799
14363
  announce(`Fake Jira ${origin}/rest/api/2/search`);
13800
14364
  announce(`Fake Jira config ${fakeConfig}`);
13801
14365
  }
13802
- if (kind === "jira") {
14366
+ if (live.kind === "jira") {
13803
14367
  try {
13804
- await refreshFromJira(app, kind, { piped: Boolean(piped) });
14368
+ await refreshFromJira(live.app, live.kind, { piped: Boolean(piped) });
13805
14369
  } catch (err) {
13806
- console.error(`${kind} Refresh failed; keeping Fixture Board`);
14370
+ console.error(`${live.kind} Refresh failed; keeping Fixture Board`);
13807
14371
  console.error(err);
13808
14372
  }
13809
14373
  }