pipe-kan 0.33.1 → 0.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,14 +1598,250 @@ 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";
1844
+ var COMMON_WORKSPACE_SLUGS = ["personal", "work"];
1529
1845
  var STATE_GROUPS = ["backlog", "unstarted", "started", "completed", "cancelled"];
1530
1846
  function planeHost(env = process.env) {
1531
1847
  return (env.PLANE_HOST ?? DEFAULT_PLANE_HOST).replace(/\/$/, "");
@@ -1692,12 +2008,14 @@ function workItemToIssue(item, project, opts) {
1692
2008
  ...opts.moduleKey ? { parent: { key: opts.moduleKey } } : {},
1693
2009
  ...labels.length ? { labels } : {},
1694
2010
  ...asString(item.created_at) ? { created: item.created_at } : {},
2011
+ ...asString(item.updated_at) ? { updated: item.updated_at } : {},
1695
2012
  ...description ? { description } : {}
1696
2013
  }
1697
2014
  };
1698
2015
  }
1699
2016
  function moduleToIssue(module, project) {
1700
2017
  const target = asString(module.target_date);
2018
+ const priority = titleCasePriority(module.priority);
1701
2019
  return {
1702
2020
  key: planeModuleKey(project.identifier, module.id),
1703
2021
  fields: {
@@ -1705,8 +2023,10 @@ function moduleToIssue(module, project) {
1705
2023
  status: { name: moduleStatusName(module.status) },
1706
2024
  issuetype: { name: "Epic" },
1707
2025
  issueType: { name: "Epic" },
2026
+ ...priority ? { priority: { name: priority } } : {},
1708
2027
  ...target ? { duedate: target, "Target End Date": target } : {},
1709
- ...asString(module.created_at) ? { created: module.created_at } : {}
2028
+ ...asString(module.created_at) ? { created: module.created_at } : {},
2029
+ ...asString(module.updated_at) ? { updated: module.updated_at } : {}
1710
2030
  }
1711
2031
  };
1712
2032
  }
@@ -1898,6 +2218,35 @@ function createPlaneCli(opts) {
1898
2218
  return;
1899
2219
  return { id: asString(row?.id) ?? slug, name: asString(row?.name) ?? slug, slug };
1900
2220
  }
2221
+ const known = new Set;
2222
+ async function probeWorkspaces() {
2223
+ const env = opts.env ?? process.env;
2224
+ const fromFlags = parseFlags(`${defaultFlags} ${lastFlags}`);
2225
+ const candidates = new Set([
2226
+ ...known,
2227
+ ...fromFlags.workspaces,
2228
+ ...fromFlags.workspace ? [fromFlags.workspace] : [],
2229
+ ...fromFlags.scopes.map((scope) => scope.workspace),
2230
+ ...env.PLANE_WORKSPACE ? [env.PLANE_WORKSPACE] : [],
2231
+ ...(env.PLANE_WORKSPACES ?? "").split(","),
2232
+ ...(env.PIPE_KAN_WORKSPACES ?? "").split(","),
2233
+ ...COMMON_WORKSPACE_SLUGS
2234
+ ]);
2235
+ const found = [];
2236
+ for (const raw of candidates) {
2237
+ const slug = raw.trim();
2238
+ if (!slug || !/^[A-Za-z0-9_-]+$/.test(slug))
2239
+ continue;
2240
+ try {
2241
+ await requestJson(queryPath(`/workspaces/${slug}/projects/`, { per_page: "1" }));
2242
+ known.add(slug);
2243
+ found.push({ id: slug, name: slug, slug });
2244
+ } catch {
2245
+ known.delete(slug);
2246
+ }
2247
+ }
2248
+ return found;
2249
+ }
1901
2250
  function parseWorkspaceList(raw) {
1902
2251
  const rows = Array.isArray(raw) ? raw : asRecord(raw)?.results;
1903
2252
  if (!Array.isArray(rows))
@@ -1936,25 +2285,76 @@ function createPlaneCli(opts) {
1936
2285
  status: asString(row?.status),
1937
2286
  created_at: asString(row?.created_at),
1938
2287
  target_date: asString(row?.target_date),
1939
- start_date: asString(row?.start_date)
2288
+ start_date: asString(row?.start_date),
2289
+ priority: asString(row?.priority),
2290
+ updated_at: asString(row?.updated_at)
2291
+ };
2292
+ }
2293
+ function catalogPlan(flags) {
2294
+ const parsed = parseFlags(flags || defaultFlags);
2295
+ if (parsed.scopes.length) {
2296
+ return {
2297
+ slugs: [...new Set(parsed.scopes.map((scope) => scope.workspace))],
2298
+ pairs: parsed.scopes
2299
+ };
2300
+ }
2301
+ if (parsed.workspaces.length)
2302
+ return { slugs: parsed.workspaces, pairs: null };
2303
+ return { slugs: [workspaceOf(flags)], pairs: null };
2304
+ }
2305
+ function exposeKey(slug, key, multi) {
2306
+ return multi ? `${slug}/${key}` : key;
2307
+ }
2308
+ function prefixIssue(issue, slug, multi) {
2309
+ if (!multi || typeof issue.key !== "string")
2310
+ return issue;
2311
+ const parent = asRecord(issue.fields?.parent);
2312
+ const parentKey = asString(parent?.key);
2313
+ return {
2314
+ ...issue,
2315
+ key: exposeKey(slug, issue.key, true),
2316
+ fields: {
2317
+ ...issue.fields,
2318
+ ...parentKey ? { parent: { ...parent, key: exposeKey(slug, parentKey, true) } } : {}
2319
+ }
1940
2320
  };
1941
2321
  }
2322
+ const projectCache = new Map;
2323
+ const cacheTtlMs = opts.cacheTtlMs ?? 30000;
2324
+ async function projectsFor(slug) {
2325
+ const hit = projectCache.get(slug);
2326
+ const now = nowFn();
2327
+ if (cacheTtlMs > 0 && hit && now - hit.at < cacheTtlMs)
2328
+ return hit.rows;
2329
+ const rows = (await paginate(`/workspaces/${slug}/projects/`)).map(parseProject).filter((row) => !!row).map((project) => ({ ...project, workspace: slug }));
2330
+ projectCache.set(slug, { at: now, rows });
2331
+ return rows;
2332
+ }
1942
2333
  async function loadCatalog(flags) {
1943
2334
  lastFlags = flags || defaultFlags;
1944
- const workspace = workspaceOf(lastFlags);
1945
- if (catalog && catalog.flags === lastFlags && catalog.workspace === workspace)
2335
+ const plan = catalogPlan(lastFlags);
2336
+ const cacheKey = plan.slugs.join(",");
2337
+ if (catalog && catalog.flags === lastFlags && catalog.cacheKey === cacheKey)
1946
2338
  return catalog;
1947
- const projects = scopedProjects((await paginate(`/workspaces/${workspace}/projects/`)).map(parseProject).filter((row) => !!row), lastFlags);
2339
+ const multi = plan.slugs.length > 1;
2340
+ const projects = [];
1948
2341
  const states = new Map;
1949
2342
  const labels = new Map;
1950
2343
  const works = new Map;
1951
2344
  const modules = new Map;
1952
2345
  const columnStates = [];
2346
+ for (const slug of plan.slugs) {
2347
+ const listed = await projectsFor(slug);
2348
+ const chosen = plan.pairs ? listed.filter((project) => plan.pairs.some((pair) => pair.workspace === slug && pair.project === project.identifier.toUpperCase())) : scopedProjects(listed, lastFlags);
2349
+ for (const project of chosen)
2350
+ projects.push(project);
2351
+ }
1953
2352
  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}/`, {
2353
+ const slug = project.workspace || plan.slugs[0] || workspaceOf(lastFlags);
2354
+ const stateRows = await paginate(`/workspaces/${slug}/projects/${project.id}/states/`);
2355
+ const labelRows = await paginate(`/workspaces/${slug}/projects/${project.id}/labels/`);
2356
+ const moduleRows = await paginate(`/workspaces/${slug}/projects/${project.id}/modules/`);
2357
+ const workRows = await withResource((resource) => paginate(queryPath(`/workspaces/${slug}/projects/${project.id}/${resource}/`, {
1958
2358
  expand: "assignees,labels,state,module"
1959
2359
  })));
1960
2360
  const stateById = new Map;
@@ -1976,7 +2376,7 @@ function createPlaneCli(opts) {
1976
2376
  const module = parseModule(row);
1977
2377
  if (!module)
1978
2378
  continue;
1979
- const issue = moduleToIssue(module, project);
2379
+ const issue = prefixIssue(moduleToIssue(module, project), slug, multi);
1980
2380
  const cached = { issue, module, project };
1981
2381
  moduleById.set(module.id, cached);
1982
2382
  modules.set(String(issue.key), cached);
@@ -1988,7 +2388,7 @@ function createPlaneCli(opts) {
1988
2388
  const issueByModule = new Map;
1989
2389
  if (missingModules && moduleById.size) {
1990
2390
  for (const moduleId of moduleById.keys()) {
1991
- const rows = await paginate(`/workspaces/${workspace}/projects/${project.id}/modules/${moduleId}/module-issues/`);
2391
+ const rows = await paginate(`/workspaces/${slug}/projects/${project.id}/modules/${moduleId}/module-issues/`);
1992
2392
  const ids = rows.map((row) => asId(asRecord(row)?.issue) ?? asId(row)).filter((id) => !!id);
1993
2393
  issueByModule.set(moduleId, ids);
1994
2394
  }
@@ -1998,26 +2398,29 @@ function createPlaneCli(opts) {
1998
2398
  const fromItem = moduleIdsOf(item)[0];
1999
2399
  const fromJoin = [...issueByModule.entries()].find(([, ids]) => ids.includes(item.id))?.[0];
2000
2400
  const moduleId = fromItem ?? fromJoin;
2001
- const moduleKey = moduleId ? moduleById.get(moduleId)?.issue.key : undefined;
2401
+ const bareModuleKey = moduleId ? planeModuleKey(project.identifier, moduleId) : undefined;
2002
2402
  const issue = workItemToIssue(item, project, {
2003
2403
  states: stateById,
2004
2404
  labels: labelById,
2005
- moduleKey: typeof moduleKey === "string" ? moduleKey : undefined
2405
+ ...bareModuleKey ? { moduleKey: bareModuleKey } : {}
2006
2406
  });
2007
- const issueKey = typeof issue?.key === "string" ? issue.key : undefined;
2008
- if (!issue || !issueKey)
2407
+ const stamped = issue ? prefixIssue(issue, slug, multi) : undefined;
2408
+ const issueKey = typeof stamped?.key === "string" ? stamped.key : undefined;
2409
+ if (!stamped || !issueKey)
2009
2410
  continue;
2411
+ const linked = bareModuleKey ? exposeKey(slug, bareModuleKey, multi) : undefined;
2010
2412
  works.set(issueKey, {
2011
- issue,
2413
+ issue: stamped,
2012
2414
  item,
2013
2415
  project,
2014
- ...typeof moduleKey === "string" ? { moduleKey } : {}
2416
+ ...linked ? { moduleKey: linked } : {}
2015
2417
  });
2016
2418
  }
2017
2419
  }
2018
2420
  catalog = {
2019
2421
  flags: lastFlags,
2020
- workspace,
2422
+ workspace: plan.slugs[0] || workspaceOf(lastFlags),
2423
+ cacheKey,
2021
2424
  projects,
2022
2425
  states,
2023
2426
  labels,
@@ -2027,6 +2430,73 @@ function createPlaneCli(opts) {
2027
2430
  };
2028
2431
  return catalog;
2029
2432
  }
2433
+ function projectSlug(project) {
2434
+ return project.workspace || workspaceOf(lastFlags);
2435
+ }
2436
+ function buildParity(loaded) {
2437
+ const projects = loaded.projects.map((project) => {
2438
+ const issues = [...loaded.works.values()].filter((row) => row.project.id === project.id && row.project.workspace === project.workspace);
2439
+ const modules = [...loaded.modules.values()].filter((row) => row.project.id === project.id && row.project.workspace === project.workspace);
2440
+ const total = issues.length;
2441
+ const completed = issues.filter((row) => {
2442
+ const name = typeof row.issue.fields?.status?.name === "string" ? row.issue.fields.status.name : "";
2443
+ const key = name.toLowerCase();
2444
+ return key === "done" || key === "completed";
2445
+ }).length;
2446
+ const started = issues.some((row) => {
2447
+ const name = typeof row.issue.fields?.status?.name === "string" ? row.issue.fields.status.name : "";
2448
+ return name.toLowerCase() === "in progress";
2449
+ });
2450
+ const status = total > 0 && completed === total ? "done" : started ? "in_progress" : "backlog";
2451
+ const dates = [
2452
+ ...modules.map((row) => row.module.target_date),
2453
+ ...issues.map((row) => row.item.target_date)
2454
+ ].filter((value) => Boolean(value));
2455
+ const starts = [
2456
+ ...modules.map((row) => row.module.start_date),
2457
+ ...issues.map((row) => row.item.start_date)
2458
+ ].filter((value) => Boolean(value));
2459
+ const targetDate = dates.sort().at(-1);
2460
+ const startDate = starts.sort()[0];
2461
+ return {
2462
+ workspace: project.workspace || loaded.workspace,
2463
+ id: project.id,
2464
+ identifier: project.identifier,
2465
+ name: project.name,
2466
+ total,
2467
+ completed,
2468
+ status,
2469
+ ...startDate ? { startDate } : {},
2470
+ ...targetDate ? { targetDate } : {},
2471
+ health: computeHealth(status, targetDate)
2472
+ };
2473
+ });
2474
+ const active = new Map(projects.map((project) => [`${project.workspace}/${project.id}`, project.completed < project.total || project.total === 0]));
2475
+ const initiatives = [...loaded.modules.values()].map((row) => {
2476
+ const raw = row.module.status ?? "backlog";
2477
+ const priority = titleCasePriority(row.module.priority);
2478
+ const open = active.get(`${row.project.workspace}/${row.project.id}`) !== false;
2479
+ return {
2480
+ key: String(row.issue.key),
2481
+ name: row.module.name,
2482
+ workspace: row.project.workspace || loaded.workspace,
2483
+ project: row.project.identifier,
2484
+ status: moduleStatusName(raw),
2485
+ rawStatus: raw,
2486
+ ...priority ? { priority } : {},
2487
+ ...row.module.start_date ? { startDate: row.module.start_date } : {},
2488
+ ...row.module.target_date ? { targetDate: row.module.target_date } : {},
2489
+ ...row.module.updated_at ? { updated: row.module.updated_at } : {},
2490
+ health: computeHealth(raw, row.module.target_date),
2491
+ activeProjects: isCompletedStatus(raw) || !open ? 0 : 1
2492
+ };
2493
+ });
2494
+ return {
2495
+ initiatives,
2496
+ projects,
2497
+ workspaces: [...new Set(loaded.projects.map((project) => project.workspace || loaded.workspace))]
2498
+ };
2499
+ }
2030
2500
  function filterWorks(flags, works) {
2031
2501
  const parsed = parseFlags(flags || defaultFlags);
2032
2502
  return works.filter(({ issue }) => {
@@ -2122,21 +2592,31 @@ function createPlaneCli(opts) {
2122
2592
  async listWorkspaces() {
2123
2593
  for (const path of ["/users/me/workspaces/", "/workspaces/"]) {
2124
2594
  try {
2125
- return parseWorkspaceList(await requestJson(path)) ?? [];
2126
- } catch (err) {
2127
- const status = err.status;
2128
- if (status === 404 && path === "/users/me/workspaces/")
2129
- continue;
2130
- return [];
2131
- }
2595
+ const listed = parseWorkspaceList(await requestJson(path));
2596
+ if (listed?.length)
2597
+ return listed;
2598
+ } catch {}
2132
2599
  }
2133
- return [];
2600
+ return probeWorkspaces();
2134
2601
  },
2135
2602
  async listProjectIdentifiers(workspace) {
2136
2603
  const slug = workspace.trim();
2137
2604
  if (!slug)
2138
2605
  return [];
2139
- return (await paginate(`/workspaces/${slug}/projects/`)).map(parseProject).filter((row) => !!row).map((row) => row.identifier);
2606
+ return (await projectsFor(slug)).map((row) => row.identifier);
2607
+ },
2608
+ async listProjects(workspace) {
2609
+ const slug = workspace.trim();
2610
+ if (!slug)
2611
+ return [];
2612
+ return (await projectsFor(slug)).map((row) => ({
2613
+ id: row.id,
2614
+ identifier: row.identifier,
2615
+ name: row.name
2616
+ }));
2617
+ },
2618
+ async parity(flags) {
2619
+ return buildParity(await loadCatalog(flags || lastFlags));
2140
2620
  },
2141
2621
  async move(key, status) {
2142
2622
  try {
@@ -2155,7 +2635,7 @@ function createPlaneCli(opts) {
2155
2635
  }).find(([name]) => name === status.toLowerCase())?.[1];
2156
2636
  if (!mapped)
2157
2637
  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 }) });
2638
+ await request(`/workspaces/${projectSlug(module.project)}/projects/${module.project.id}/modules/${module.module.id}/`, { method: "PATCH", body: JSON.stringify({ status: mapped }) });
2159
2639
  catalog = undefined;
2160
2640
  return { ok: true };
2161
2641
  }
@@ -2165,7 +2645,7 @@ function createPlaneCli(opts) {
2165
2645
  const state = await resolveStateId(work.project, status, lastFlags);
2166
2646
  if (!state)
2167
2647
  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 }) }));
2648
+ await withResource((resource) => request(`/workspaces/${projectSlug(work.project)}/projects/${work.project.id}/${resource}/${work.item.id}/`, { method: "PATCH", body: JSON.stringify({ state }) }));
2169
2649
  catalog = undefined;
2170
2650
  return { ok: true };
2171
2651
  } catch (err) {
@@ -2188,14 +2668,14 @@ function createPlaneCli(opts) {
2188
2668
  body.state = state;
2189
2669
  if (labels.length)
2190
2670
  body.labels = labels;
2191
- const created = asRecord(await withResource((resource) => requestJson(`/workspaces/${workspaceOf(lastFlags)}/projects/${project.id}/${resource}/`, {
2671
+ const created = asRecord(await withResource((resource) => requestJson(`/workspaces/${projectSlug(project)}/projects/${project.id}/${resource}/`, {
2192
2672
  method: "POST",
2193
2673
  body: JSON.stringify(body)
2194
2674
  })));
2195
2675
  const sequence = Number(created?.sequence_id);
2196
2676
  const key = Number.isFinite(sequence) && sequence > 0 ? planeWorkItemKey(project.identifier, sequence) : undefined;
2197
2677
  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] }) });
2678
+ await request(`/workspaces/${projectSlug(project)}/projects/${project.id}/modules/${parent.module.id}/module-issues/`, { method: "POST", body: JSON.stringify({ issues: [created.id] }) });
2199
2679
  }
2200
2680
  catalog = undefined;
2201
2681
  return key ? { ok: true, key } : { ok: true };
@@ -2205,6 +2685,24 @@ function createPlaneCli(opts) {
2205
2685
  },
2206
2686
  async edit(key, input) {
2207
2687
  try {
2688
+ const loaded = await loadCatalog(lastFlags);
2689
+ const module = loaded.modules.get(key);
2690
+ if (module) {
2691
+ if (input.assignee !== undefined)
2692
+ return { ok: false, error: "Plane modules have no assignee field" };
2693
+ if (input.labels)
2694
+ return { ok: false, error: "Plane modules have no labels field" };
2695
+ const body = {};
2696
+ if (input.summary !== undefined)
2697
+ body.name = input.summary;
2698
+ if (input.priority !== undefined)
2699
+ body.priority = input.priority.toLowerCase();
2700
+ if (Object.keys(body).length) {
2701
+ await request(`/workspaces/${projectSlug(module.project)}/projects/${module.project.id}/modules/${module.module.id}/`, { method: "PATCH", body: JSON.stringify(body) });
2702
+ }
2703
+ catalog = undefined;
2704
+ return { ok: true };
2705
+ }
2208
2706
  const work = await lookupByKey(key, lastFlags);
2209
2707
  if (!work)
2210
2708
  return { ok: false, error: `Work item ${key} not found` };
@@ -2215,7 +2713,19 @@ function createPlaneCli(opts) {
2215
2713
  body.description_html = htmlParagraph(input.description);
2216
2714
  if (input.labels)
2217
2715
  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) }));
2716
+ if (input.priority !== undefined)
2717
+ body.priority = input.priority.toLowerCase();
2718
+ if (input.assignee !== undefined) {
2719
+ const slug = projectSlug(work.project);
2720
+ const members = (await paginate(`/workspaces/${slug}/members/`)).map((row) => asRecord(row));
2721
+ const wanted = input.assignee.trim().toLowerCase();
2722
+ const member = members.find((row) => asString(row?.display_name)?.toLowerCase() === wanted);
2723
+ const memberId = asString(member?.id);
2724
+ if (wanted && !memberId)
2725
+ return { ok: false, error: `Plane member "${input.assignee}" was not found` };
2726
+ body.assignees = memberId ? [memberId] : [];
2727
+ }
2728
+ await withResource((resource) => request(`/workspaces/${projectSlug(work.project)}/projects/${work.project.id}/${resource}/${work.item.id}/`, { method: "PATCH", body: JSON.stringify(body) }));
2219
2729
  catalog = undefined;
2220
2730
  return { ok: true };
2221
2731
  } catch (err) {
@@ -2223,7 +2733,12 @@ function createPlaneCli(opts) {
2223
2733
  }
2224
2734
  },
2225
2735
  async open(key) {
2226
- return `${host}/${workspaceOf(lastFlags)}/browse/${key}`;
2736
+ const loaded = await loadCatalog(lastFlags);
2737
+ const module = loaded.modules.get(key);
2738
+ const work = loaded.works.get(key);
2739
+ const slug = module?.project.workspace || work?.project.workspace || workspaceOf(lastFlags);
2740
+ const bare = key.includes("/") ? key.slice(key.indexOf("/") + 1) : key;
2741
+ return `${host}/${slug}/browse/${bare}`;
2227
2742
  },
2228
2743
  async view(key) {
2229
2744
  const loaded = await loadCatalog(lastFlags);
@@ -2245,13 +2760,16 @@ async function createBoardApp(opts) {
2245
2760
  const store = IssueStore.fromRaw(opts.raw);
2246
2761
  const plane = wantsPlane(flags);
2247
2762
  const bin = resolveJiraBin(env.JIRA_BIN ?? "jira", env.PATH ?? "");
2763
+ const fixtureMode = Boolean(env.PLANE_FIXTURE?.trim());
2764
+ const useJira = Boolean(bin) && !fixtureMode;
2248
2765
  const cli = plane ? createPlaneCli({
2249
2766
  host: planeHost(env),
2250
2767
  apiKey: env.PLANE_API_KEY ?? "",
2251
2768
  workspace: planeWorkspace(flags, env),
2252
2769
  flags,
2253
- fetch: opts.fetch
2254
- }) : bin ? createJiraCli({
2770
+ env,
2771
+ fetch: opts.fetch ?? resolvePlaneFetch(env)
2772
+ }) : useJira ? createJiraCli({
2255
2773
  bin,
2256
2774
  configPath: env.JIRA_CONFIG_FILE,
2257
2775
  token: env.JIRA_API_TOKEN,
@@ -2270,7 +2788,7 @@ async function createBoardApp(opts) {
2270
2788
  const local = createStoreCli(store, flags);
2271
2789
  app.hydrate(JSON.parse(await local.list(app.flags)), { fromStore: true });
2272
2790
  }
2273
- return { app, store, kind: plane ? "plane" : bin ? "jira" : "store" };
2791
+ return { app, store, kind: plane ? "plane" : useJira ? "jira" : "store" };
2274
2792
  }
2275
2793
  async function refreshFromJira(app, kind, opts = {}) {
2276
2794
  if (opts.piped)
@@ -2281,7 +2799,7 @@ async function refreshFromJira(app, kind, opts = {}) {
2281
2799
  }
2282
2800
 
2283
2801
  // src/factory-ledger.ts
2284
- import { existsSync as existsSync2, readFileSync as readFileSync3, statSync } from "node:fs";
2802
+ import { existsSync as existsSync2, readFileSync as readFileSync4, statSync } from "node:fs";
2285
2803
  import { homedir as homedir3 } from "node:os";
2286
2804
  import { join as join4 } from "node:path";
2287
2805
  var DEFAULT_LEDGER_PATH = "~/code/software-factory/asf/runtime/factory-coordinator/jobs.jsonl";
@@ -2388,7 +2906,7 @@ function readLedger(env = process.env) {
2388
2906
  if (_cache && _cache.path === path && _cache.mtime === mtime) {
2389
2907
  return { jobs: _cache.jobs };
2390
2908
  }
2391
- const text = readFileSync3(path, "utf8");
2909
+ const text = readFileSync4(path, "utf8");
2392
2910
  const jobs = classifyJobs(parseJobsJsonl(text));
2393
2911
  _cache = { path, mtime, jobs };
2394
2912
  return { jobs };
@@ -2456,7 +2974,9 @@ function handleAppApi(req, res, app, opts = {}) {
2456
2974
  return;
2457
2975
  }
2458
2976
  try {
2459
- json(res, 200, { projects: await app.listProjectIdentifiers?.(workspace) ?? [] });
2977
+ const projects = await app.listProjectIdentifiers?.(workspace) ?? [];
2978
+ const details = await app.listProjects?.(workspace) ?? [];
2979
+ json(res, 200, { projects, ...details.length ? { details } : {} });
2460
2980
  } catch (err) {
2461
2981
  const message = err instanceof Error ? err.message : "catalog failed";
2462
2982
  json(res, 409, { error: message });
@@ -2464,6 +2984,38 @@ function handleAppApi(req, res, app, opts = {}) {
2464
2984
  });
2465
2985
  return true;
2466
2986
  }
2987
+ if (url.pathname === "/api/parity" && method === "GET") {
2988
+ reply(req, res, async () => {
2989
+ json(res, 200, await app.parity?.() ?? { initiatives: [], projects: [], workspaces: [] });
2990
+ });
2991
+ return true;
2992
+ }
2993
+ if (url.pathname === "/api/sources" && method === "GET") {
2994
+ reply(req, res, async () => {
2995
+ const current = kind === "plane" ? "plane" : "jira";
2996
+ json(res, 200, {
2997
+ source: current,
2998
+ kind,
2999
+ sources: sourceOptions({
3000
+ planeKey: Boolean(env.PLANE_API_KEY?.trim() || env.PLANE_FIXTURE?.trim()),
3001
+ jiraBin: Boolean(resolveJiraBin(env.JIRA_BIN ?? "jira", env.PATH ?? ""))
3002
+ })
3003
+ });
3004
+ });
3005
+ return true;
3006
+ }
3007
+ if (url.pathname === "/api/source" && method === "POST") {
3008
+ reply(req, res, async (text) => {
3009
+ if (!opts.onSource) {
3010
+ json(res, 404, { error: "source switch unavailable" });
3011
+ return;
3012
+ }
3013
+ const body = text ? JSON.parse(text) : {};
3014
+ const result = await opts.onSource(String(body.source ?? ""));
3015
+ json(res, result.ok ? 200 : 409, result);
3016
+ });
3017
+ return true;
3018
+ }
2467
3019
  if (url.pathname === "/api/refresh" && method === "POST") {
2468
3020
  reply(req, res, async (text) => {
2469
3021
  const body = text ? JSON.parse(text) : {};
@@ -2533,6 +3085,10 @@ function handleAppApi(req, res, app, opts = {}) {
2533
3085
  input.summary = String(body.summary);
2534
3086
  if (body.description !== undefined)
2535
3087
  input.description = String(body.description);
3088
+ if (body.assignee !== undefined)
3089
+ input.assignee = String(body.assignee);
3090
+ if (body.priority !== undefined)
3091
+ input.priority = String(body.priority);
2536
3092
  if (Array.isArray(body.labels)) {
2537
3093
  input.labels = body.labels.filter((item) => typeof item === "string");
2538
3094
  }
@@ -2650,7 +3206,7 @@ function formatCard(card) {
2650
3206
  }
2651
3207
 
2652
3208
  // src/server/agent/config.ts
2653
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
3209
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
2654
3210
  import { homedir as homedir4 } from "node:os";
2655
3211
  import { delimiter as delimiter2, dirname as dirname2, join as join5 } from "node:path";
2656
3212
  var DEFAULT_CONFIG_PATH = join5(homedir4(), ".config", "pipe-kan", "agent.json");
@@ -2709,7 +3265,7 @@ function loadAgentConfig(path = agentConfigPath()) {
2709
3265
  if (!existsSync3(path))
2710
3266
  return structuredClone(DEFAULT_CONFIG);
2711
3267
  try {
2712
- const raw = JSON.parse(readFileSync4(path, "utf8"));
3268
+ const raw = JSON.parse(readFileSync5(path, "utf8"));
2713
3269
  return mergeConfig(raw);
2714
3270
  } catch {
2715
3271
  return structuredClone(DEFAULT_CONFIG);
@@ -3236,7 +3792,7 @@ function parsedType(data) {
3236
3792
  }
3237
3793
  return t;
3238
3794
  }
3239
- function issue(...args) {
3795
+ function issue2(...args) {
3240
3796
  const [iss, input, inst] = args;
3241
3797
  if (typeof iss === "string") {
3242
3798
  return {
@@ -5663,7 +6219,7 @@ function handleRefineResult(result, payload, input, inst) {
5663
6219
  };
5664
6220
  if (inst._zod.def.params)
5665
6221
  _iss.params = inst._zod.def.params;
5666
- payload.issues.push(issue(_iss));
6222
+ payload.issues.push(issue2(_iss));
5667
6223
  }
5668
6224
  }
5669
6225
  // node_modules/zod/v4/core/memoizer.js
@@ -6489,11 +7045,11 @@ function _refine(Class, fn, _params) {
6489
7045
  }
6490
7046
  function _superRefine(fn, params) {
6491
7047
  const ch = _check((payload) => {
6492
- payload.addIssue = (issue2) => {
6493
- if (typeof issue2 === "string") {
6494
- payload.issues.push(issue(issue2, payload.value, ch._zod.def));
7048
+ payload.addIssue = (issue) => {
7049
+ if (typeof issue === "string") {
7050
+ payload.issues.push(issue2(issue, payload.value, ch._zod.def));
6495
7051
  } else {
6496
- const _issue = issue2;
7052
+ const _issue = issue;
6497
7053
  if (_issue.fatal)
6498
7054
  _issue.continue = false;
6499
7055
  _issue.code ?? (_issue.code = "custom");
@@ -6501,7 +7057,7 @@ function _superRefine(fn, params) {
6501
7057
  _issue.input = payload.value;
6502
7058
  _issue.inst ?? (_issue.inst = ch);
6503
7059
  _issue.continue ?? (_issue.continue = !ch._zod.def.abort);
6504
- payload.issues.push(issue(_issue));
7060
+ payload.issues.push(issue2(_issue));
6505
7061
  }
6506
7062
  };
6507
7063
  return fn(payload.value, payload);
@@ -8243,18 +8799,18 @@ var ZodTransform = /* @__PURE__ */ $constructor("ZodTransform", (inst, def) => {
8243
8799
  if (_ctx.direction === "backward") {
8244
8800
  throw new $ZodEncodeError(inst.constructor.name);
8245
8801
  }
8246
- payload.addIssue = (issue2) => {
8247
- if (typeof issue2 === "string") {
8248
- payload.issues.push(issue(issue2, payload.value, def));
8802
+ payload.addIssue = (issue) => {
8803
+ if (typeof issue === "string") {
8804
+ payload.issues.push(issue2(issue, payload.value, def));
8249
8805
  } else {
8250
- const _issue = issue2;
8806
+ const _issue = issue;
8251
8807
  if (_issue.fatal)
8252
8808
  _issue.continue = false;
8253
8809
  _issue.code ?? (_issue.code = "custom");
8254
8810
  if (!("input" in _issue))
8255
8811
  _issue.input = payload.value;
8256
8812
  _issue.inst ?? (_issue.inst = inst);
8257
- payload.issues.push(issue(_issue));
8813
+ payload.issues.push(issue2(_issue));
8258
8814
  }
8259
8815
  };
8260
8816
  const output = def.transform(payload.value, payload);
@@ -12791,7 +13347,7 @@ function permissionOptionForDecision(options, decision) {
12791
13347
  }
12792
13348
 
12793
13349
  // src/server/agent/skills.ts
12794
- import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync } from "node:fs";
13350
+ import { existsSync as existsSync4, readFileSync as readFileSync6, readdirSync } from "node:fs";
12795
13351
  import { homedir as homedir5 } from "node:os";
12796
13352
  import { join as join7 } from "node:path";
12797
13353
  import { fileURLToPath } from "node:url";
@@ -12840,7 +13396,7 @@ function skillPath(dir, id) {
12840
13396
  }
12841
13397
  function readSkill(path, id) {
12842
13398
  try {
12843
- const text = readFileSync5(path, "utf8");
13399
+ const text = readFileSync6(path, "utf8");
12844
13400
  const front = parseFrontMatter(text);
12845
13401
  return {
12846
13402
  id,
@@ -12867,7 +13423,7 @@ function parseFrontMatter(text) {
12867
13423
  }
12868
13424
 
12869
13425
  // src/server/agent/tools.ts
12870
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
13426
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
12871
13427
  import { join as join8 } from "node:path";
12872
13428
  var TOOLS = [
12873
13429
  {
@@ -13017,7 +13573,7 @@ var EXECUTORS = {
13017
13573
  return { ok: false, error: "Path traversal not allowed" };
13018
13574
  const repoRoot = process.cwd();
13019
13575
  try {
13020
- const text = readFileSync6(join8(repoRoot, relPath), "utf8");
13576
+ const text = readFileSync7(join8(repoRoot, relPath), "utf8");
13021
13577
  return { ok: true, value: { path: relPath, text } };
13022
13578
  } catch (err) {
13023
13579
  return { ok: false, error: String(err) };
@@ -13641,7 +14197,7 @@ function handleFakeJira(req, res, store) {
13641
14197
 
13642
14198
  // src/http.ts
13643
14199
  function handleRequest(req, res, ctx) {
13644
- if (handleAppApi(req, res, ctx.app, { kind: ctx.kind }) || handleAgentApi(req, res, ctx.app)) {
14200
+ if (handleAppApi(req, res, ctx.app, { kind: ctx.kind, env: ctx.env, onSource: ctx.switchSource }) || handleAgentApi(req, res, ctx.app)) {
13645
14201
  return true;
13646
14202
  }
13647
14203
  if (ctx.kind === "plane")
@@ -13757,15 +14313,51 @@ function announce(line) {
13757
14313
  }
13758
14314
  async function runServer(opts) {
13759
14315
  const piped = await readPipe();
13760
- const raw = piped ?? JSON.parse(readFileSync7(join11(opts.root, "fixtures/issues.json"), "utf8"));
14316
+ const raw = piped ?? JSON.parse(readFileSync8(join11(opts.root, "fixtures/issues.json"), "utf8"));
13761
14317
  const flags = argvToFlags(process.argv);
13762
- const { app, store, kind } = await createBoardApp({
14318
+ const booted = await createBoardApp({
13763
14319
  raw,
13764
14320
  piped: Boolean(piped),
13765
14321
  flags
13766
14322
  });
14323
+ const live = {
14324
+ app: booted.app,
14325
+ store: booted.store,
14326
+ kind: booted.kind,
14327
+ raw
14328
+ };
14329
+ async function switchSource(source) {
14330
+ if (source === "taskwarrior") {
14331
+ return { ok: false, kind: live.kind, source, reason: "Taskwarrior is not connected" };
14332
+ }
14333
+ if (source !== "plane" && source !== "jira") {
14334
+ return { ok: false, kind: live.kind, source, error: `Unknown source ${source}` };
14335
+ }
14336
+ if (source === "plane" && !process.env.PLANE_API_KEY?.trim()) {
14337
+ return { ok: false, kind: live.kind, source, reason: "Set PLANE_API_KEY to use Plane" };
14338
+ }
14339
+ const nextFlags = setPlaneFlag(live.app.flags, source === "plane");
14340
+ try {
14341
+ const next = await createBoardApp({ raw: live.raw, piped: false, flags: nextFlags });
14342
+ live.app = next.app;
14343
+ live.store = next.store;
14344
+ live.kind = next.kind;
14345
+ if (next.kind === "plane" || next.kind === "jira") {
14346
+ try {
14347
+ await refreshFromJira(live.app, next.kind);
14348
+ } catch (err) {
14349
+ const message = err instanceof Error ? err.message : String(err);
14350
+ return { ok: true, kind: live.kind, source, error: message };
14351
+ }
14352
+ }
14353
+ return { ok: true, kind: live.kind, source };
14354
+ } catch (err) {
14355
+ const message = err instanceof Error ? err.message : String(err);
14356
+ return { ok: false, kind: live.kind, source, error: message };
14357
+ }
14358
+ }
13767
14359
  const server = createServer((req, res) => {
13768
- if (handleRequest(req, res, { app, store, kind }))
14360
+ if (handleRequest(req, res, { app: live.app, store: live.store, kind: live.kind, switchSource }))
13769
14361
  return;
13770
14362
  if (opts.fallback) {
13771
14363
  opts.fallback(req, res);
@@ -13776,34 +14368,34 @@ async function runServer(opts) {
13776
14368
  res.end("pipe-kan UI is missing. Run bun run build.");
13777
14369
  }
13778
14370
  });
13779
- if (kind === "plane") {
14371
+ if (live.kind === "plane") {
13780
14372
  try {
13781
- await refreshFromJira(app, kind, { piped: Boolean(piped) });
14373
+ await refreshFromJira(live.app, live.kind, { piped: Boolean(piped) });
13782
14374
  } catch (err) {
13783
- console.error(`${kind} Refresh failed`, err);
14375
+ console.error(`${live.kind} Refresh failed`, err);
13784
14376
  }
13785
- const error = app.board().error;
14377
+ const error = live.app.board().error;
13786
14378
  if (error)
13787
- console.error(`${kind} Refresh failed; keeping last live Board`, error);
14379
+ console.error(`${live.kind} Refresh failed; keeping last live Board`, error);
13788
14380
  }
13789
14381
  const { host, port } = resolveListen();
13790
14382
  await bindListen(server, host, port);
13791
14383
  const origin = `http://127.0.0.1:${port}`;
13792
14384
  announce(`pipe-kan http://${host}:${port}`);
13793
- if (kind === "plane") {
14385
+ if (live.kind === "plane") {
13794
14386
  announce(`cli plane ${planeHost()} ${planeWorkspace(flags)}`);
13795
14387
  announce(`Plane host default ${DEFAULT_PLANE_HOST}`);
13796
14388
  } else {
13797
14389
  const fakeConfig = writeJiraConfig(join11(tmpdir3(), "pipe-kan"), origin);
13798
- announce(`cli ${kind === "jira" ? resolveJiraBin() : "store"}`);
14390
+ announce(`cli ${live.kind === "jira" ? resolveJiraBin() : "store"}`);
13799
14391
  announce(`Fake Jira ${origin}/rest/api/2/search`);
13800
14392
  announce(`Fake Jira config ${fakeConfig}`);
13801
14393
  }
13802
- if (kind === "jira") {
14394
+ if (live.kind === "jira") {
13803
14395
  try {
13804
- await refreshFromJira(app, kind, { piped: Boolean(piped) });
14396
+ await refreshFromJira(live.app, live.kind, { piped: Boolean(piped) });
13805
14397
  } catch (err) {
13806
- console.error(`${kind} Refresh failed; keeping Fixture Board`);
14398
+ console.error(`${live.kind} Refresh failed; keeping Fixture Board`);
13807
14399
  console.error(err);
13808
14400
  }
13809
14401
  }