jfl 0.8.0 → 0.9.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.
Files changed (249) hide show
  1. package/dist/commands/doctor.d.ts +1 -0
  2. package/dist/commands/doctor.d.ts.map +1 -1
  3. package/dist/commands/doctor.js +30 -1
  4. package/dist/commands/doctor.js.map +1 -1
  5. package/dist/commands/ide.d.ts +2 -1
  6. package/dist/commands/ide.d.ts.map +1 -1
  7. package/dist/commands/ide.js +60 -1
  8. package/dist/commands/ide.js.map +1 -1
  9. package/dist/commands/init-from-service.d.ts +15 -0
  10. package/dist/commands/init-from-service.d.ts.map +1 -0
  11. package/dist/commands/init-from-service.js +541 -0
  12. package/dist/commands/init-from-service.js.map +1 -0
  13. package/dist/commands/init.d.ts +1 -0
  14. package/dist/commands/init.d.ts.map +1 -1
  15. package/dist/commands/init.js +32 -1
  16. package/dist/commands/init.js.map +1 -1
  17. package/dist/commands/kanban.d.ts.map +1 -1
  18. package/dist/commands/kanban.js +13 -4
  19. package/dist/commands/kanban.js.map +1 -1
  20. package/dist/commands/linear.d.ts +41 -0
  21. package/dist/commands/linear.d.ts.map +1 -0
  22. package/dist/commands/linear.js +715 -0
  23. package/dist/commands/linear.js.map +1 -0
  24. package/dist/commands/peter.d.ts.map +1 -1
  25. package/dist/commands/peter.js +232 -25
  26. package/dist/commands/peter.js.map +1 -1
  27. package/dist/commands/services.d.ts.map +1 -1
  28. package/dist/commands/services.js +146 -0
  29. package/dist/commands/services.js.map +1 -1
  30. package/dist/commands/setup.d.ts.map +1 -1
  31. package/dist/commands/setup.js +173 -13
  32. package/dist/commands/setup.js.map +1 -1
  33. package/dist/commands/telemetry-monitor.d.ts +11 -0
  34. package/dist/commands/telemetry-monitor.d.ts.map +1 -0
  35. package/dist/commands/telemetry-monitor.js +224 -0
  36. package/dist/commands/telemetry-monitor.js.map +1 -0
  37. package/dist/commands/telemetry-test.d.ts +11 -0
  38. package/dist/commands/telemetry-test.d.ts.map +1 -0
  39. package/dist/commands/telemetry-test.js +67 -0
  40. package/dist/commands/telemetry-test.js.map +1 -0
  41. package/dist/commands/tenet-agents.d.ts +13 -0
  42. package/dist/commands/tenet-agents.d.ts.map +1 -0
  43. package/dist/commands/tenet-agents.js +191 -0
  44. package/dist/commands/tenet-agents.js.map +1 -0
  45. package/dist/commands/tenet-setup.d.ts +19 -0
  46. package/dist/commands/tenet-setup.d.ts.map +1 -0
  47. package/dist/commands/tenet-setup.js +131 -0
  48. package/dist/commands/tenet-setup.js.map +1 -0
  49. package/dist/commands/train.d.ts +18 -0
  50. package/dist/commands/train.d.ts.map +1 -1
  51. package/dist/commands/train.js +182 -0
  52. package/dist/commands/train.js.map +1 -1
  53. package/dist/commands/whoami.d.ts +2 -0
  54. package/dist/commands/whoami.d.ts.map +1 -0
  55. package/dist/commands/whoami.js +24 -0
  56. package/dist/commands/whoami.js.map +1 -0
  57. package/dist/index.js +159 -10
  58. package/dist/index.js.map +1 -1
  59. package/dist/lib/advanced-setup.d.ts +78 -0
  60. package/dist/lib/advanced-setup.d.ts.map +1 -0
  61. package/dist/lib/advanced-setup.js +433 -0
  62. package/dist/lib/advanced-setup.js.map +1 -0
  63. package/dist/lib/agent-config.d.ts +33 -0
  64. package/dist/lib/agent-config.d.ts.map +1 -1
  65. package/dist/lib/agent-config.js +26 -0
  66. package/dist/lib/agent-config.js.map +1 -1
  67. package/dist/lib/counterfactual-training-bridge.d.ts +114 -0
  68. package/dist/lib/counterfactual-training-bridge.d.ts.map +1 -0
  69. package/dist/lib/counterfactual-training-bridge.js +322 -0
  70. package/dist/lib/counterfactual-training-bridge.js.map +1 -0
  71. package/dist/lib/discovery-agent.d.ts +48 -0
  72. package/dist/lib/discovery-agent.d.ts.map +1 -0
  73. package/dist/lib/discovery-agent.js +111 -0
  74. package/dist/lib/discovery-agent.js.map +1 -0
  75. package/dist/lib/flow-engine.d.ts.map +1 -1
  76. package/dist/lib/flow-engine.js +46 -8
  77. package/dist/lib/flow-engine.js.map +1 -1
  78. package/dist/lib/gtm-generator.d.ts +29 -0
  79. package/dist/lib/gtm-generator.d.ts.map +1 -0
  80. package/dist/lib/gtm-generator.js +252 -0
  81. package/dist/lib/gtm-generator.js.map +1 -0
  82. package/dist/lib/hub-health.d.ts +40 -0
  83. package/dist/lib/hub-health.d.ts.map +1 -0
  84. package/dist/lib/hub-health.js +89 -0
  85. package/dist/lib/hub-health.js.map +1 -0
  86. package/dist/lib/invariant-monitor.d.ts +6 -2
  87. package/dist/lib/invariant-monitor.d.ts.map +1 -1
  88. package/dist/lib/invariant-monitor.js +89 -2
  89. package/dist/lib/invariant-monitor.js.map +1 -1
  90. package/dist/lib/journal-analyzer.d.ts +71 -0
  91. package/dist/lib/journal-analyzer.d.ts.map +1 -0
  92. package/dist/lib/journal-analyzer.js +306 -0
  93. package/dist/lib/journal-analyzer.js.map +1 -0
  94. package/dist/lib/linear-client.d.ts +73 -0
  95. package/dist/lib/linear-client.d.ts.map +1 -0
  96. package/dist/lib/linear-client.js +112 -0
  97. package/dist/lib/linear-client.js.map +1 -0
  98. package/dist/lib/linear-id-map.d.ts +20 -0
  99. package/dist/lib/linear-id-map.d.ts.map +1 -0
  100. package/dist/lib/linear-id-map.js +57 -0
  101. package/dist/lib/linear-id-map.js.map +1 -0
  102. package/dist/lib/linear-kanban.d.ts +66 -0
  103. package/dist/lib/linear-kanban.d.ts.map +1 -0
  104. package/dist/lib/linear-kanban.js +175 -0
  105. package/dist/lib/linear-kanban.js.map +1 -0
  106. package/dist/lib/onboarding.d.ts +40 -0
  107. package/dist/lib/onboarding.d.ts.map +1 -0
  108. package/dist/lib/onboarding.js +213 -0
  109. package/dist/lib/onboarding.js.map +1 -0
  110. package/dist/lib/physical-world-model.d.ts +50 -0
  111. package/dist/lib/physical-world-model.d.ts.map +1 -0
  112. package/dist/lib/physical-world-model.js +251 -0
  113. package/dist/lib/physical-world-model.js.map +1 -0
  114. package/dist/lib/planning-loop.d.ts +157 -0
  115. package/dist/lib/planning-loop.d.ts.map +1 -0
  116. package/dist/lib/planning-loop.js +537 -0
  117. package/dist/lib/planning-loop.js.map +1 -0
  118. package/dist/lib/policy-head.d.ts +13 -0
  119. package/dist/lib/policy-head.d.ts.map +1 -1
  120. package/dist/lib/policy-head.js +168 -2
  121. package/dist/lib/policy-head.js.map +1 -1
  122. package/dist/lib/resource-optimizer-middleware.d.ts +39 -0
  123. package/dist/lib/resource-optimizer-middleware.d.ts.map +1 -0
  124. package/dist/lib/resource-optimizer-middleware.js +222 -0
  125. package/dist/lib/resource-optimizer-middleware.js.map +1 -0
  126. package/dist/lib/resource-optimizer.d.ts +71 -0
  127. package/dist/lib/resource-optimizer.d.ts.map +1 -0
  128. package/dist/lib/resource-optimizer.js +228 -0
  129. package/dist/lib/resource-optimizer.js.map +1 -0
  130. package/dist/lib/rl-manager.d.ts +74 -0
  131. package/dist/lib/rl-manager.d.ts.map +1 -0
  132. package/dist/lib/rl-manager.js +244 -0
  133. package/dist/lib/rl-manager.js.map +1 -0
  134. package/dist/lib/service-analyzer.d.ts +76 -0
  135. package/dist/lib/service-analyzer.d.ts.map +1 -0
  136. package/dist/lib/service-analyzer.js +704 -0
  137. package/dist/lib/service-analyzer.js.map +1 -0
  138. package/dist/lib/service-gtm.js +2 -2
  139. package/dist/lib/service-gtm.js.map +1 -1
  140. package/dist/lib/service-questionnaire.d.ts +11 -0
  141. package/dist/lib/service-questionnaire.d.ts.map +1 -0
  142. package/dist/lib/service-questionnaire.js +89 -0
  143. package/dist/lib/service-questionnaire.js.map +1 -0
  144. package/dist/lib/setup/agent-generator.d.ts +2 -0
  145. package/dist/lib/setup/agent-generator.d.ts.map +1 -1
  146. package/dist/lib/setup/agent-generator.js +128 -4
  147. package/dist/lib/setup/agent-generator.js.map +1 -1
  148. package/dist/lib/setup/flow-generator.d.ts +10 -0
  149. package/dist/lib/setup/flow-generator.d.ts.map +1 -0
  150. package/dist/lib/setup/flow-generator.js +113 -0
  151. package/dist/lib/setup/flow-generator.js.map +1 -0
  152. package/dist/lib/setup/invariant-bridge.d.ts +91 -0
  153. package/dist/lib/setup/invariant-bridge.d.ts.map +1 -0
  154. package/dist/lib/setup/invariant-bridge.js +384 -0
  155. package/dist/lib/setup/invariant-bridge.js.map +1 -0
  156. package/dist/lib/setup/spec-generator.d.ts +41 -5
  157. package/dist/lib/setup/spec-generator.d.ts.map +1 -1
  158. package/dist/lib/setup/spec-generator.js +503 -29
  159. package/dist/lib/setup/spec-generator.js.map +1 -1
  160. package/dist/lib/stratus-client.js +1 -1
  161. package/dist/lib/stratus-client.js.map +1 -1
  162. package/dist/lib/surface-agent.d.ts +78 -0
  163. package/dist/lib/surface-agent.d.ts.map +1 -0
  164. package/dist/lib/surface-agent.js +105 -0
  165. package/dist/lib/surface-agent.js.map +1 -0
  166. package/dist/lib/surface-coordination-example.d.ts +30 -0
  167. package/dist/lib/surface-coordination-example.d.ts.map +1 -0
  168. package/dist/lib/surface-coordination-example.js +164 -0
  169. package/dist/lib/surface-coordination-example.js.map +1 -0
  170. package/dist/lib/telemetry/physical-world-collector.d.ts +15 -0
  171. package/dist/lib/telemetry/physical-world-collector.d.ts.map +1 -0
  172. package/dist/lib/telemetry/physical-world-collector.js +177 -0
  173. package/dist/lib/telemetry/physical-world-collector.js.map +1 -0
  174. package/dist/lib/telemetry/training-bridge.d.ts +51 -0
  175. package/dist/lib/telemetry/training-bridge.d.ts.map +1 -0
  176. package/dist/lib/telemetry/training-bridge.js +185 -0
  177. package/dist/lib/telemetry/training-bridge.js.map +1 -0
  178. package/dist/lib/telemetry.d.ts +2 -1
  179. package/dist/lib/telemetry.d.ts.map +1 -1
  180. package/dist/lib/telemetry.js +23 -2
  181. package/dist/lib/telemetry.js.map +1 -1
  182. package/dist/lib/tenet-board-agent.d.ts +52 -0
  183. package/dist/lib/tenet-board-agent.d.ts.map +1 -0
  184. package/dist/lib/tenet-board-agent.js +226 -0
  185. package/dist/lib/tenet-board-agent.js.map +1 -0
  186. package/dist/lib/tenet-ide-agent.d.ts +40 -0
  187. package/dist/lib/tenet-ide-agent.d.ts.map +1 -0
  188. package/dist/lib/tenet-ide-agent.js +199 -0
  189. package/dist/lib/tenet-ide-agent.js.map +1 -0
  190. package/dist/lib/workspace/data-pipeline.d.ts.map +1 -1
  191. package/dist/lib/workspace/data-pipeline.js +27 -5
  192. package/dist/lib/workspace/data-pipeline.js.map +1 -1
  193. package/dist/lib/workspace/sidebar-runner.d.ts +13 -0
  194. package/dist/lib/workspace/sidebar-runner.d.ts.map +1 -0
  195. package/dist/lib/workspace/sidebar-runner.js +419 -0
  196. package/dist/lib/workspace/sidebar-runner.js.map +1 -0
  197. package/dist/lib/workspace/surface-registry.d.ts.map +1 -1
  198. package/dist/lib/workspace/surface-registry.js +4 -1
  199. package/dist/lib/workspace/surface-registry.js.map +1 -1
  200. package/dist/lib/workspace/surfaces/agent-overview.d.ts +3 -3
  201. package/dist/lib/workspace/surfaces/agent-overview.d.ts.map +1 -1
  202. package/dist/lib/workspace/surfaces/agent-overview.js +3 -3
  203. package/dist/lib/workspace/surfaces/agent-overview.js.map +1 -1
  204. package/dist/lib/workspace/surfaces/index.d.ts +3 -0
  205. package/dist/lib/workspace/surfaces/index.d.ts.map +1 -1
  206. package/dist/lib/workspace/surfaces/index.js +3 -0
  207. package/dist/lib/workspace/surfaces/index.js.map +1 -1
  208. package/dist/lib/workspace/surfaces/kanban.d.ts +15 -0
  209. package/dist/lib/workspace/surfaces/kanban.d.ts.map +1 -0
  210. package/dist/lib/workspace/surfaces/kanban.js +43 -0
  211. package/dist/lib/workspace/surfaces/kanban.js.map +1 -0
  212. package/dist/lib/workspace/surfaces/physical-world.d.ts +15 -0
  213. package/dist/lib/workspace/surfaces/physical-world.d.ts.map +1 -0
  214. package/dist/lib/workspace/surfaces/physical-world.js +37 -0
  215. package/dist/lib/workspace/surfaces/physical-world.js.map +1 -0
  216. package/dist/lib/workspace/surfaces/sidebar.d.ts +22 -0
  217. package/dist/lib/workspace/surfaces/sidebar.d.ts.map +1 -0
  218. package/dist/lib/workspace/surfaces/sidebar.js +90 -0
  219. package/dist/lib/workspace/surfaces/sidebar.js.map +1 -0
  220. package/dist/types/flows.d.ts +2 -1
  221. package/dist/types/flows.d.ts.map +1 -1
  222. package/dist/types/physical-world-model.d.ts +65 -0
  223. package/dist/types/physical-world-model.d.ts.map +1 -0
  224. package/dist/types/physical-world-model.js +43 -0
  225. package/dist/types/physical-world-model.js.map +1 -0
  226. package/dist/types/telemetry.d.ts +37 -0
  227. package/dist/types/telemetry.d.ts.map +1 -1
  228. package/dist/types/world-model.d.ts.map +1 -1
  229. package/dist/types/world-model.js +14 -7
  230. package/dist/types/world-model.js.map +1 -1
  231. package/dist/utils/context-hub-port.d.ts.map +1 -1
  232. package/dist/utils/context-hub-port.js +6 -1
  233. package/dist/utils/context-hub-port.js.map +1 -1
  234. package/package.json +3 -2
  235. package/packages/pi/extensions/index.ts +34 -6
  236. package/packages/pi/extensions/onboarding-v1.ts +8 -8
  237. package/packages/pi/extensions/onboarding-v2.ts +5 -5
  238. package/scripts/telemetry-dashboard.sh +44 -0
  239. package/scripts/test-planning-loop-e2e.ts +181 -0
  240. package/scripts/test-server-inference.ts +49 -0
  241. package/scripts/test-state-sensitivity.ts +32 -0
  242. package/scripts/train/v2/benchmark.py +661 -0
  243. package/scripts/train/v2/generate_balanced.py +439 -0
  244. package/scripts/train/v2/generate_hard_negatives.py +219 -0
  245. package/scripts/train/v2/infer.py +149 -36
  246. package/scripts/train/v2/infer_server.py +224 -0
  247. package/scripts/train/v2/online_train.py +576 -0
  248. package/scripts/train/v2/precompute.py +24 -6
  249. package/template/CLAUDE.md +74 -132
@@ -0,0 +1,715 @@
1
+ /**
2
+ * jfl linear — Linear project sync
3
+ *
4
+ * Bidirectional sync between Linear projects and GitHub Issues (jfl kanban).
5
+ * Subcommands: link, unlink, status, sync
6
+ *
7
+ * @purpose CLI commands for Linear ↔ GitHub kanban bidirectional sync
8
+ */
9
+ import chalk from "chalk";
10
+ import ora from "ora";
11
+ import inquirer from "inquirer";
12
+ import { execSync } from "child_process";
13
+ import { existsSync, readFileSync, writeFileSync } from "fs";
14
+ import { join } from "path";
15
+ import { getLinearClient, getLinearTeams, getLinearProjects, getTeamStates, getProjectIssues, createLinearIssue, updateLinearIssue, } from "../lib/linear-client.js";
16
+ import { readMap, writeMap, addMapping, } from "../lib/linear-id-map.js";
17
+ import { linearToTenet, kanbanToTenet, tenetToLinearCreate, tenetToGitHubBody, tenetToGitHubLabels, findLinearStateId, } from "../lib/linear-kanban.js";
18
+ import { GitHubKanban } from "../lib/kanban-github.js";
19
+ import { getProjectDataDir } from "../utils/jfl-config.js";
20
+ // ============================================================================
21
+ // Helpers
22
+ // ============================================================================
23
+ function gh(args, cwd) {
24
+ return execSync(`gh ${args}`, {
25
+ cwd,
26
+ encoding: "utf-8",
27
+ timeout: 15000,
28
+ stdio: ["pipe", "pipe", "pipe"],
29
+ }).trim();
30
+ }
31
+ function readProjectConfig(projectRoot) {
32
+ const dataDir = getProjectDataDir(projectRoot);
33
+ const configPath = join(projectRoot, dataDir, "config.json");
34
+ if (!existsSync(configPath))
35
+ return {};
36
+ try {
37
+ return JSON.parse(readFileSync(configPath, "utf-8"));
38
+ }
39
+ catch {
40
+ return {};
41
+ }
42
+ }
43
+ function writeProjectConfig(projectRoot, config) {
44
+ const dataDir = getProjectDataDir(projectRoot);
45
+ const configPath = join(projectRoot, dataDir, "config.json");
46
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
47
+ }
48
+ // Env vars override file config — enables CI/GitHub Actions usage without .jfl/config.json
49
+ function requireLinearConfig(projectRoot) {
50
+ const config = readProjectConfig(projectRoot);
51
+ const base = config.linear ?? {};
52
+ const merged = {
53
+ apiKey: process.env.LINEAR_API_KEY ?? base.apiKey ?? "",
54
+ teamId: process.env.LINEAR_TEAM_ID ?? base.teamId ?? "",
55
+ projectId: process.env.LINEAR_PROJECT_ID ?? base.projectId ?? "",
56
+ scope: process.env.LINEAR_SCOPE ?? base.scope,
57
+ kanbanRepo: base.kanbanRepo,
58
+ webhookUrl: base.webhookUrl,
59
+ syncEnabled: base.syncEnabled,
60
+ teamName: base.teamName,
61
+ projectName: base.projectName,
62
+ };
63
+ if (!merged.apiKey || !merged.teamId || !merged.projectId) {
64
+ console.error(chalk.red("No Linear config found. Run: jfl linear link (or set LINEAR_API_KEY, LINEAR_TEAM_ID, LINEAR_PROJECT_ID env vars)"));
65
+ process.exit(1);
66
+ }
67
+ return merged;
68
+ }
69
+ function getGitHubRepo(projectRoot) {
70
+ try {
71
+ const remote = execSync("git remote get-url origin", {
72
+ cwd: projectRoot, encoding: "utf-8", timeout: 5000,
73
+ }).trim();
74
+ const match = remote.match(/github\.com[:/](.+?)(?:\.git)?$/);
75
+ return match ? match[1] : "";
76
+ }
77
+ catch {
78
+ return "";
79
+ }
80
+ }
81
+ function getColumnLabel(status) {
82
+ const map = {
83
+ backlog: "jfl/backlog",
84
+ in_progress: "jfl/in-progress",
85
+ eval: "jfl/eval",
86
+ done: "jfl/done",
87
+ };
88
+ return map[status] ?? "jfl/backlog";
89
+ }
90
+ // ============================================================================
91
+ // jfl linear link
92
+ // ============================================================================
93
+ export async function linearLink(projectRoot) {
94
+ console.log(chalk.bold("\n Link Linear Project\n"));
95
+ const { apiKey } = await inquirer.prompt([
96
+ {
97
+ type: "password",
98
+ name: "apiKey",
99
+ message: "Linear API key (lin_api_...):",
100
+ validate: (v) => v.startsWith("lin_api_") ? true : "Must start with lin_api_",
101
+ },
102
+ ]);
103
+ const spinner = ora("Fetching Linear teams...").start();
104
+ let client;
105
+ let teams;
106
+ try {
107
+ client = getLinearClient(apiKey);
108
+ teams = await getLinearTeams(client);
109
+ spinner.stop();
110
+ }
111
+ catch (err) {
112
+ spinner.fail("Failed to connect to Linear: " + (err.message ?? err));
113
+ return;
114
+ }
115
+ if (teams.length === 0) {
116
+ console.error(chalk.red("No Linear teams found for this API key."));
117
+ return;
118
+ }
119
+ const { teamId } = await inquirer.prompt([
120
+ {
121
+ type: "list",
122
+ name: "teamId",
123
+ message: "Select Linear team:",
124
+ choices: teams.map(t => ({ name: `${t.name} (${t.key})`, value: t.id })),
125
+ },
126
+ ]);
127
+ spinner.start("Fetching Linear projects...");
128
+ let projects;
129
+ try {
130
+ projects = await getLinearProjects(client, teamId);
131
+ spinner.stop();
132
+ }
133
+ catch (err) {
134
+ spinner.fail("Failed to fetch projects: " + (err.message ?? err));
135
+ return;
136
+ }
137
+ if (projects.length === 0) {
138
+ console.error(chalk.red("No Linear projects found for this team."));
139
+ console.log(chalk.gray(" Create a project in Linear first, then run jfl linear link again."));
140
+ return;
141
+ }
142
+ const { projectId } = await inquirer.prompt([
143
+ {
144
+ type: "list",
145
+ name: "projectId",
146
+ message: "Select Linear project:",
147
+ choices: projects.map(p => ({
148
+ name: p.description ? `${p.name} — ${p.description.slice(0, 50)}` : p.name,
149
+ value: p.id,
150
+ })),
151
+ },
152
+ ]);
153
+ // ── Scope detection ────────────────────────────────────────────────────────
154
+ // Detect if we're in a service (has gtm_parent) or a GTM with registered services.
155
+ // Linear project → JFL scope is a natural 1:1 mapping.
156
+ const projectConfig = readProjectConfig(projectRoot);
157
+ const registeredServices = projectConfig.registered_services ?? [];
158
+ const gtmParent = projectConfig.gtm_parent;
159
+ // Determine kanban repo: if this is a service, the kanban lives in the parent GTM
160
+ let kanbanRoot = projectRoot;
161
+ let kanbanRepoHint = "";
162
+ if (gtmParent && existsSync(gtmParent)) {
163
+ kanbanRoot = gtmParent;
164
+ kanbanRepoHint = getGitHubRepo(gtmParent);
165
+ }
166
+ const detectedRepo = getGitHubRepo(kanbanRoot) || kanbanRepoHint;
167
+ // Scope detection — build list of available scopes
168
+ const availableScopes = [];
169
+ if (gtmParent) {
170
+ // Service context — scope is this service's name
171
+ const { basename } = await import("path");
172
+ const serviceName = projectConfig.name ?? basename(projectRoot);
173
+ availableScopes.push(serviceName);
174
+ }
175
+ else if (registeredServices.length > 0) {
176
+ for (const svc of registeredServices)
177
+ availableScopes.push(svc.name);
178
+ }
179
+ let scopes = [];
180
+ if (availableScopes.length > 0) {
181
+ const checkboxChoices = [
182
+ { name: chalk.dim("— no scope (all issues, no service label) —"), value: "__none__" },
183
+ new inquirer.Separator(" "),
184
+ ...availableScopes.map(s => ({ name: `scope:${s}`, value: s })),
185
+ ];
186
+ const answer = await inquirer.prompt([
187
+ {
188
+ type: "checkbox",
189
+ name: "scopes",
190
+ message: "Which scopes should this Linear project sync to? (space to select, enter to confirm)",
191
+ choices: checkboxChoices,
192
+ validate: (v) => true,
193
+ },
194
+ ]);
195
+ // Filter out the sentinel none value
196
+ scopes = answer.scopes.filter(s => s !== "__none__");
197
+ }
198
+ // Build webhook URL(s): one per scope (or one without scope if none selected)
199
+ const platformUrl = process.env.JFL_PLATFORM_URL ?? "https://jfl-platform.fly.dev";
200
+ const baseUrl = detectedRepo
201
+ ? `${platformUrl}/api/webhooks/linear?repo=${detectedRepo}`
202
+ : `${platformUrl}/api/webhooks/linear?repo=owner/repo`;
203
+ const defaultWebhookUrl = scopes.length > 0
204
+ ? scopes.map(s => `${baseUrl}&scope=${s}`).join("\n")
205
+ : baseUrl;
206
+ const { webhookUrl } = await inquirer.prompt([
207
+ {
208
+ type: "input",
209
+ name: "webhookUrl",
210
+ message: "Webhook URL for real-time sync (press enter to accept or skip):",
211
+ default: scopes.length === 1 ? defaultWebhookUrl : defaultWebhookUrl.split("\n")[0],
212
+ },
213
+ ]);
214
+ // Save config
215
+ const config = readProjectConfig(projectRoot);
216
+ const team = teams.find(t => t.id === teamId);
217
+ const project = projects.find(p => p.id === projectId);
218
+ config.linear = {
219
+ apiKey,
220
+ teamId,
221
+ teamName: team.name,
222
+ projectId,
223
+ projectName: project.name,
224
+ scopes: scopes.length > 0 ? scopes : undefined,
225
+ scope: scopes.length === 1 ? scopes[0] : undefined, // legacy compat
226
+ kanbanRepo: detectedRepo || undefined,
227
+ webhookUrl: webhookUrl || undefined,
228
+ syncEnabled: true,
229
+ linkedAt: new Date().toISOString(),
230
+ };
231
+ writeProjectConfig(projectRoot, config);
232
+ // Initialize map
233
+ const existingMap = readMap(projectRoot);
234
+ writeMap(projectRoot, {
235
+ ...existingMap,
236
+ projectId,
237
+ teamId,
238
+ mappings: existingMap.mappings ?? {},
239
+ });
240
+ console.log();
241
+ console.log(chalk.green(` ✓ Linked to Linear project: ${project.name} (team: ${team.name})`));
242
+ if (scopes.length > 0)
243
+ console.log(chalk.gray(` Scopes: ${scopes.map(s => `scope:${s}`).join(", ")}`));
244
+ if (detectedRepo)
245
+ console.log(chalk.gray(` Kanban repo: ${detectedRepo}`));
246
+ if (scopes.length > 1) {
247
+ console.log(chalk.yellow(" → Register one webhook URL per scope in Linear: Settings → API → Webhooks"));
248
+ for (const s of scopes)
249
+ console.log(chalk.gray(` ${baseUrl}&scope=${s}`));
250
+ }
251
+ else if (webhookUrl) {
252
+ console.log(chalk.gray(` Webhook URL: ${webhookUrl}`));
253
+ console.log(chalk.yellow(" → Register this URL in Linear: Settings → API → Webhooks"));
254
+ }
255
+ console.log();
256
+ console.log(chalk.gray(" Run 'jfl linear sync' to do an initial sync"));
257
+ console.log();
258
+ }
259
+ // ============================================================================
260
+ // jfl linear unlink
261
+ // ============================================================================
262
+ export async function linearUnlink(projectRoot) {
263
+ const config = readProjectConfig(projectRoot);
264
+ if (!config.linear) {
265
+ console.log(chalk.gray("No Linear project linked."));
266
+ return;
267
+ }
268
+ const { confirm } = await inquirer.prompt([
269
+ {
270
+ type: "confirm",
271
+ name: "confirm",
272
+ message: `Unlink Linear project "${config.linear.projectName}"?`,
273
+ default: false,
274
+ },
275
+ ]);
276
+ if (!confirm)
277
+ return;
278
+ delete config.linear;
279
+ writeProjectConfig(projectRoot, config);
280
+ console.log(chalk.green(" ✓ Unlinked Linear project"));
281
+ }
282
+ // ============================================================================
283
+ // jfl linear status
284
+ // ============================================================================
285
+ export async function linearStatus(projectRoot) {
286
+ const config = readProjectConfig(projectRoot);
287
+ if (!config.linear) {
288
+ console.log(chalk.gray("\n No Linear project linked. Run: jfl linear link\n"));
289
+ return;
290
+ }
291
+ const { teamName, projectName, linkedAt, webhookUrl, syncEnabled } = config.linear;
292
+ const map = readMap(projectRoot);
293
+ const mappingCount = Object.keys(map.mappings).length;
294
+ console.log(chalk.bold("\n Linear Sync Status\n"));
295
+ console.log(` Project: ${chalk.cyan(projectName)} (team: ${teamName})`);
296
+ console.log(` Linked: ${chalk.gray(new Date(linkedAt).toLocaleString())}`);
297
+ console.log(` Sync: ${syncEnabled ? chalk.green("enabled") : chalk.yellow("disabled")}`);
298
+ console.log(` Webhook: ${webhookUrl ? chalk.gray(webhookUrl) : chalk.yellow("not configured")}`);
299
+ console.log(` Mapped: ${chalk.bold(String(mappingCount))} issues`);
300
+ console.log();
301
+ if (mappingCount > 0) {
302
+ const repo = getGitHubRepo(projectRoot);
303
+ console.log(chalk.gray(" Issue Mappings:"));
304
+ for (const [ghNum, linearId] of Object.entries(map.mappings).slice(0, 10)) {
305
+ const linearUrl = `https://linear.app/issue/${linearId}`;
306
+ console.log(chalk.gray(` GitHub #${ghNum} ↔ ${linearId}`));
307
+ }
308
+ if (mappingCount > 10) {
309
+ console.log(chalk.gray(` ... and ${mappingCount - 10} more`));
310
+ }
311
+ }
312
+ console.log();
313
+ }
314
+ // ============================================================================
315
+ // jfl linear sync
316
+ // ============================================================================
317
+ export async function linearSync(projectRoot, options) {
318
+ const linearConfig = requireLinearConfig(projectRoot);
319
+ const jsonMode = options.json ?? false;
320
+ // Resolve kanban root: service → parent GTM, otherwise this repo
321
+ const projectConf = readProjectConfig(projectRoot);
322
+ const gtmParent = linearConfig.kanbanRepo
323
+ ? undefined
324
+ : projectConf.gtm_parent;
325
+ const kanbanRoot = gtmParent && existsSync(gtmParent) ? gtmParent : projectRoot;
326
+ const repo = linearConfig.kanbanRepo ?? getGitHubRepo(kanbanRoot);
327
+ if (!repo) {
328
+ console.error(chalk.red("Cannot determine GitHub repo. Set kanbanRepo in .jfl/config.json linear section."));
329
+ process.exit(1);
330
+ }
331
+ const scope = linearConfig.scope;
332
+ const direction = options.direction ?? "both";
333
+ const dryRun = options.dryRun ?? false;
334
+ const result = { direction, dryRun, repo, scope, changes: [], skipped: 0, errors: [] };
335
+ const log = (msg) => { if (!jsonMode)
336
+ console.log(msg); };
337
+ const logSection = (msg) => { if (!jsonMode)
338
+ console.log(chalk.bold(msg)); };
339
+ if (dryRun)
340
+ log(chalk.yellow(" [dry run] No changes will be made\n"));
341
+ const client = getLinearClient(linearConfig.apiKey);
342
+ if (!jsonMode)
343
+ ora("Fetching data...").start().stop();
344
+ let linearIssues;
345
+ let states;
346
+ let kanban;
347
+ let githubCards;
348
+ try {
349
+ ;
350
+ [linearIssues, states] = await Promise.all([
351
+ getProjectIssues(client, linearConfig.projectId),
352
+ getTeamStates(client, linearConfig.teamId),
353
+ ]);
354
+ kanban = new GitHubKanban(kanbanRoot);
355
+ githubCards = await kanban.getCards(scope ? { scope } : undefined);
356
+ }
357
+ catch (err) {
358
+ const msg = err.message ?? String(err);
359
+ if (jsonMode) {
360
+ process.stdout.write(JSON.stringify({ ...result, errors: [{ title: "fetch", error: msg }] }) + "\n");
361
+ }
362
+ else {
363
+ console.error(chalk.red("Failed to fetch: " + msg));
364
+ }
365
+ process.exit(1);
366
+ }
367
+ const map = readMap(projectRoot);
368
+ const linearToGhMap = {};
369
+ for (const [ghNum, lid] of Object.entries(map.mappings)) {
370
+ linearToGhMap[lid] = parseInt(ghNum);
371
+ }
372
+ const ghByNumber = {};
373
+ for (const card of githubCards)
374
+ ghByNumber[card.number] = card;
375
+ // ── GitHub → Linear ──────────────────────────────────────────────────────
376
+ if (direction === "github" || direction === "both") {
377
+ logSection("\n GitHub → Linear");
378
+ for (const card of githubCards) {
379
+ const existingLinearId = map.mappings[card.number];
380
+ if (existingLinearId) {
381
+ const linearIssue = linearIssues.find(i => i.id === existingLinearId);
382
+ if (linearIssue) {
383
+ const expectedColumn = linearToTenet(linearIssue).status;
384
+ if (expectedColumn !== card.column) {
385
+ const stateId = findLinearStateId(states, card.column);
386
+ if (stateId && !dryRun) {
387
+ try {
388
+ await updateLinearIssue(client, existingLinearId, { stateId });
389
+ result.changes.push({ side: "linear", action: "updated", githubNumber: card.number, linearId: existingLinearId, title: card.title, field: "status", from: expectedColumn, to: card.column });
390
+ log(chalk.gray(` ↑ Linear ${linearIssue.identifier} "${card.title}" → ${card.column}`));
391
+ }
392
+ catch (err) {
393
+ result.errors.push({ title: card.title, error: err.message });
394
+ }
395
+ }
396
+ else {
397
+ result.skipped++;
398
+ }
399
+ }
400
+ else {
401
+ result.skipped++;
402
+ }
403
+ }
404
+ continue;
405
+ }
406
+ // New → create in Linear
407
+ const tenet = kanbanToTenet(card);
408
+ const input = tenetToLinearCreate(tenet, linearConfig.teamId, linearConfig.projectId, states);
409
+ if (dryRun) {
410
+ result.changes.push({ side: "linear", action: "created", githubNumber: card.number, title: card.title });
411
+ log(chalk.gray(` [dry] Would create in Linear: "${card.title}"`));
412
+ continue;
413
+ }
414
+ try {
415
+ const created_issue = await createLinearIssue(client, input);
416
+ if (created_issue) {
417
+ addMapping(projectRoot, card.number, created_issue.id);
418
+ result.changes.push({ side: "linear", action: "created", githubNumber: card.number, linearIdentifier: created_issue.identifier, linearId: created_issue.id, title: card.title });
419
+ log(chalk.green(` ✓ Linear ${created_issue.identifier} ← GitHub #${card.number} "${card.title}"`));
420
+ }
421
+ }
422
+ catch (err) {
423
+ result.errors.push({ title: card.title, error: err.message });
424
+ log(chalk.red(` ✗ "${card.title}": ${err.message}`));
425
+ }
426
+ }
427
+ }
428
+ // ── Linear → GitHub ──────────────────────────────────────────────────────
429
+ if (direction === "linear" || direction === "both") {
430
+ logSection("\n Linear → GitHub");
431
+ for (const issue of linearIssues) {
432
+ const existingGhNum = linearToGhMap[issue.id];
433
+ if (existingGhNum) {
434
+ const ghCard = ghByNumber[existingGhNum];
435
+ if (ghCard) {
436
+ const tenet = linearToTenet(issue);
437
+ if (tenet.status !== ghCard.column) {
438
+ const newLabel = getColumnLabel(tenet.status);
439
+ const allColLabels = ["jfl/backlog", "jfl/in-progress", "jfl/eval", "jfl/done"];
440
+ const removeArgs = allColLabels.filter(l => l !== newLabel).map(l => `--remove-label "${l}"`).join(" ");
441
+ if (!dryRun) {
442
+ try {
443
+ gh(`issue edit ${existingGhNum} --repo ${repo} ${removeArgs} --add-label "${newLabel}"`, kanbanRoot);
444
+ if (tenet.status === "done") {
445
+ try {
446
+ gh(`issue edit ${existingGhNum} --repo ${repo} --state closed`, kanbanRoot);
447
+ }
448
+ catch { }
449
+ }
450
+ result.changes.push({ side: "github", action: "updated", githubNumber: existingGhNum, linearIdentifier: issue.identifier, title: issue.title, field: "status", from: ghCard.column, to: tenet.status });
451
+ log(chalk.gray(` ↓ GitHub #${existingGhNum} "${issue.title}" → ${tenet.status}`));
452
+ }
453
+ catch (err) {
454
+ result.errors.push({ title: issue.title, error: err.message });
455
+ }
456
+ }
457
+ else {
458
+ result.changes.push({ side: "github", action: "updated", githubNumber: existingGhNum, title: issue.title, field: "status", from: ghCard.column, to: tenet.status });
459
+ log(chalk.gray(` [dry] GitHub #${existingGhNum} → ${tenet.status}`));
460
+ }
461
+ }
462
+ else {
463
+ result.skipped++;
464
+ }
465
+ }
466
+ continue;
467
+ }
468
+ // New → create GitHub issue
469
+ const tenet = linearToTenet(issue);
470
+ const labels = tenetToGitHubLabels(tenet, scope);
471
+ const body = tenetToGitHubBody(tenet);
472
+ if (dryRun) {
473
+ result.changes.push({ side: "github", action: "created", linearIdentifier: issue.identifier, linearId: issue.id, title: issue.title });
474
+ log(chalk.gray(` [dry] Would create GitHub issue: "${issue.title}"`));
475
+ continue;
476
+ }
477
+ try {
478
+ const labelArgs = labels.map(l => `--label "${l}"`).join(" ");
479
+ const { join: pathJoin } = await import("path");
480
+ const { writeFileSync: wf, unlinkSync: ul } = await import("fs");
481
+ const tmpFile = pathJoin(kanbanRoot, ".jfl", `.linear-sync-${Date.now()}.tmp`);
482
+ wf(tmpFile, body);
483
+ let ghNum = 0;
484
+ try {
485
+ const ghResult = gh(`issue create --repo ${repo} --title "${issue.title.replace(/"/g, '\\"')}" --body-file "${tmpFile}" ${labelArgs}`, kanbanRoot);
486
+ const numMatch = ghResult.match(/\/issues\/(\d+)/);
487
+ ghNum = numMatch ? parseInt(numMatch[1]) : 0;
488
+ }
489
+ finally {
490
+ try {
491
+ ul(tmpFile);
492
+ }
493
+ catch { }
494
+ }
495
+ if (ghNum > 0) {
496
+ addMapping(projectRoot, ghNum, issue.id);
497
+ result.changes.push({ side: "github", action: "created", githubNumber: ghNum, linearIdentifier: issue.identifier, linearId: issue.id, title: issue.title });
498
+ log(chalk.green(` ✓ GitHub #${ghNum} ← Linear ${issue.identifier} "${issue.title}"`));
499
+ }
500
+ }
501
+ catch (err) {
502
+ result.errors.push({ title: issue.title, error: err.message });
503
+ log(chalk.red(` ✗ "${issue.title}": ${err.message}`));
504
+ }
505
+ }
506
+ }
507
+ // Output
508
+ if (jsonMode) {
509
+ process.stdout.write(JSON.stringify(result) + "\n");
510
+ }
511
+ else {
512
+ const created = result.changes.filter(c => c.action === "created").length;
513
+ const updated = result.changes.filter(c => c.action === "updated").length;
514
+ console.log();
515
+ console.log(chalk.bold(" Sync complete:"), chalk.green(`${created} created`), chalk.yellow(`${updated} updated`), chalk.gray(`${result.skipped} skipped`), result.errors.length > 0 ? chalk.red(`${result.errors.length} errors`) : "");
516
+ console.log();
517
+ }
518
+ return result;
519
+ }
520
+ // ============================================================================
521
+ // jfl linear bootstrap — deploy GitHub Actions workflow + set secrets
522
+ // ============================================================================
523
+ const WORKFLOW_TEMPLATE = `name: Linear Sync
524
+
525
+ on:
526
+ issues:
527
+ types: [opened, edited, closed, labeled, unlabeled]
528
+ schedule:
529
+ - cron: '*/30 * * * *'
530
+ workflow_dispatch:
531
+ inputs:
532
+ direction:
533
+ description: 'Sync direction: github, linear, or both'
534
+ default: 'both'
535
+ type: choice
536
+ options: [both, github, linear]
537
+
538
+ jobs:
539
+ sync:
540
+ name: Sync Linear ↔ GitHub Issues
541
+ runs-on: ubuntu-latest
542
+ permissions:
543
+ issues: write
544
+ contents: write
545
+
546
+ steps:
547
+ - uses: actions/checkout@v4
548
+
549
+ - uses: actions/setup-node@v4
550
+ with:
551
+ node-version: '20'
552
+
553
+ - name: Install jfl
554
+ run: npm install -g jfl@latest
555
+
556
+ - name: Bootstrap kanban labels
557
+ run: jfl kanban bootstrap --repo \${{ github.repository }}
558
+ env:
559
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
560
+ continue-on-error: true
561
+
562
+ - name: Sync
563
+ id: sync
564
+ run: |
565
+ # issues event → push that change to Linear immediately (github direction)
566
+ # schedule/dispatch → full bidirectional
567
+ if [ "\${{ github.event_name }}" = "issues" ]; then
568
+ DIRECTION="github"
569
+ else
570
+ DIRECTION="\${{ inputs.direction || 'both' }}"
571
+ fi
572
+
573
+ jfl linear sync --direction \$DIRECTION --json | tee /tmp/sync-result.json
574
+ echo "result=$(cat /tmp/sync-result.json)" >> \$GITHUB_OUTPUT
575
+ env:
576
+ LINEAR_API_KEY: \${{ secrets.LINEAR_API_KEY }}
577
+ LINEAR_TEAM_ID: \${{ vars.LINEAR_TEAM_ID }}
578
+ LINEAR_PROJECT_ID: \${{ vars.LINEAR_PROJECT_ID }}
579
+ LINEAR_SCOPE: \${{ vars.LINEAR_SCOPE }}
580
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
581
+
582
+ - name: Summary
583
+ if: always()
584
+ run: |
585
+ cat /tmp/sync-result.json 2>/dev/null | node -e "
586
+ const r = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
587
+ const created = r.changes.filter(c=>c.action==='created').length;
588
+ const updated = r.changes.filter(c=>c.action==='updated').length;
589
+ console.log('## Linear Sync');
590
+ console.log(\`- **Direction:** \${r.direction}\`);
591
+ console.log(\`- **Created:** \${created}\`);
592
+ console.log(\`- **Updated:** \${updated}\`);
593
+ console.log(\`- **Skipped:** \${r.skipped}\`);
594
+ if(r.errors.length) console.log(\`- **Errors:** \${r.errors.length}\`);
595
+ if(r.changes.length) {
596
+ console.log('');
597
+ console.log('### Changes');
598
+ r.changes.forEach(c => {
599
+ const loc = c.githubNumber ? \`GitHub #\${c.githubNumber}\` : \`Linear \${c.linearIdentifier}\`;
600
+ const dest = c.side === 'github' ? 'GitHub' : 'Linear';
601
+ console.log(\`- [\${c.action}] \${dest}: \${c.title} (\${loc})\`);
602
+ });
603
+ }
604
+ " >> \$GITHUB_STEP_SUMMARY || true
605
+ `;
606
+ export async function linearBootstrap(projectRoot) {
607
+ const linearConfig = requireLinearConfig(projectRoot);
608
+ const projectConf = readProjectConfig(projectRoot);
609
+ const gtmParent = linearConfig.kanbanRepo ? undefined : projectConf.gtm_parent;
610
+ const kanbanRoot = gtmParent && existsSync(gtmParent) ? gtmParent : projectRoot;
611
+ const repo = linearConfig.kanbanRepo ?? getGitHubRepo(kanbanRoot);
612
+ if (!repo) {
613
+ console.error(chalk.red("Cannot determine GitHub repo. Run from inside the project or set kanbanRepo."));
614
+ process.exit(1);
615
+ }
616
+ console.log(chalk.bold("\n Bootstrap Linear Sync — GitHub Actions\n"));
617
+ console.log(chalk.gray(` Repo: ${repo}`));
618
+ console.log(chalk.gray(` Project: ${linearConfig.projectName ?? linearConfig.projectId}`));
619
+ if (linearConfig.scope)
620
+ console.log(chalk.gray(` Scope: ${linearConfig.scope}`));
621
+ console.log();
622
+ // Write workflow file
623
+ const workflowDir = join(kanbanRoot, ".github", "workflows");
624
+ const workflowPath = join(workflowDir, "linear-sync.yml");
625
+ if (!existsSync(workflowDir)) {
626
+ const { mkdirSync } = await import("fs");
627
+ mkdirSync(workflowDir, { recursive: true });
628
+ }
629
+ if (existsSync(workflowPath)) {
630
+ const { overwrite } = await inquirer.prompt([{
631
+ type: "confirm", name: "overwrite",
632
+ message: "linear-sync.yml already exists — overwrite?",
633
+ default: false,
634
+ }]);
635
+ if (!overwrite) {
636
+ console.log(chalk.gray(" Skipped workflow file."));
637
+ }
638
+ else {
639
+ writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
640
+ console.log(chalk.green(` ✓ Written: .github/workflows/linear-sync.yml`));
641
+ }
642
+ }
643
+ else {
644
+ writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
645
+ console.log(chalk.green(` ✓ Written: .github/workflows/linear-sync.yml`));
646
+ }
647
+ // Set GitHub secrets + variables
648
+ console.log();
649
+ console.log(chalk.gray(" Setting GitHub secrets and variables..."));
650
+ const spinner = ora("Setting LINEAR_API_KEY secret...").start();
651
+ try {
652
+ gh(`secret set LINEAR_API_KEY --body "${linearConfig.apiKey}" --repo ${repo}`, kanbanRoot);
653
+ spinner.succeed(chalk.green(" ✓ SECRET: LINEAR_API_KEY"));
654
+ }
655
+ catch (err) {
656
+ spinner.fail(chalk.yellow(` ✗ SECRET: LINEAR_API_KEY (${err.message}) — set manually: gh secret set LINEAR_API_KEY --repo ${repo}`));
657
+ }
658
+ // Variables (non-sensitive)
659
+ for (const [name, value] of [
660
+ ["LINEAR_TEAM_ID", linearConfig.teamId],
661
+ ["LINEAR_PROJECT_ID", linearConfig.projectId],
662
+ ...(linearConfig.scope ? [["LINEAR_SCOPE", linearConfig.scope]] : []),
663
+ ]) {
664
+ try {
665
+ gh(`variable set ${name} --body "${value}" --repo ${repo}`, kanbanRoot);
666
+ console.log(chalk.green(` ✓ VAR: ${name}=${value}`));
667
+ }
668
+ catch (err) {
669
+ console.log(chalk.yellow(` ✗ VAR: ${name} — set manually: gh variable set ${name} --body "${value}" --repo ${repo}`));
670
+ }
671
+ }
672
+ // Commit the workflow file
673
+ console.log();
674
+ const { commit } = await inquirer.prompt([{
675
+ type: "confirm", name: "commit",
676
+ message: "Commit and push workflow file now?",
677
+ default: true,
678
+ }]);
679
+ if (commit) {
680
+ try {
681
+ const { execSync: exec } = await import("child_process");
682
+ exec(`git add .github/workflows/linear-sync.yml && git commit -m "ci: add Linear sync workflow" && git push`, {
683
+ cwd: kanbanRoot, encoding: "utf-8", stdio: "inherit",
684
+ });
685
+ console.log(chalk.green(" ✓ Pushed"));
686
+ }
687
+ catch (err) {
688
+ console.log(chalk.yellow(" Could not push automatically — commit manually."));
689
+ }
690
+ }
691
+ // Trigger initial run
692
+ console.log();
693
+ const { trigger } = await inquirer.prompt([{
694
+ type: "confirm", name: "trigger",
695
+ message: "Trigger an initial sync run now?",
696
+ default: true,
697
+ }]);
698
+ if (trigger) {
699
+ try {
700
+ gh(`workflow run linear-sync.yml --repo ${repo} --field direction=both`, kanbanRoot);
701
+ console.log(chalk.green(` ✓ Triggered — watch at: https://github.com/${repo}/actions`));
702
+ }
703
+ catch {
704
+ console.log(chalk.gray(` Run manually: gh workflow run linear-sync.yml --repo ${repo}`));
705
+ }
706
+ }
707
+ console.log();
708
+ console.log(chalk.bold(" Done! How sync works:"));
709
+ console.log(chalk.gray(" • GitHub issue label changed → Linear updates within seconds (issues trigger)"));
710
+ console.log(chalk.gray(" • Linear issue created/moved → GitHub syncs every 30 minutes (schedule)"));
711
+ console.log(chalk.gray(" • Manual: jfl linear sync → runs immediately from your machine"));
712
+ console.log(chalk.gray(` • Actions: https://github.com/${repo}/actions`));
713
+ console.log();
714
+ }
715
+ //# sourceMappingURL=linear.js.map