maxsimcli 4.6.0 → 4.7.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 (221) hide show
  1. package/dist/assets/CHANGELOG.md +16 -0
  2. package/dist/backend-server.cjs +2739 -41
  3. package/dist/backend-server.cjs.map +1 -1
  4. package/dist/cli.cjs +3 -3
  5. package/dist/{lifecycle-DxCru7rk.cjs → lifecycle-D8mcsEjy.cjs} +2 -2
  6. package/dist/{lifecycle-DxCru7rk.cjs.map → lifecycle-D8mcsEjy.cjs.map} +1 -1
  7. package/dist/mcp-server.cjs +2715 -16
  8. package/dist/mcp-server.cjs.map +1 -1
  9. package/dist/{server-By0TN-nC.cjs → server-BAHfh_vw.cjs} +2716 -17
  10. package/dist/server-BAHfh_vw.cjs.map +1 -0
  11. package/package.json +1 -1
  12. package/dist/.tsbuildinfo +0 -1
  13. package/dist/backend/index.d.ts +0 -4
  14. package/dist/backend/index.d.ts.map +0 -1
  15. package/dist/backend/index.js +0 -12
  16. package/dist/backend/index.js.map +0 -1
  17. package/dist/backend/lifecycle.d.ts +0 -13
  18. package/dist/backend/lifecycle.d.ts.map +0 -1
  19. package/dist/backend/lifecycle.js +0 -168
  20. package/dist/backend/lifecycle.js.map +0 -1
  21. package/dist/backend/server.d.ts +0 -13
  22. package/dist/backend/server.d.ts.map +0 -1
  23. package/dist/backend/server.js +0 -1013
  24. package/dist/backend/server.js.map +0 -1
  25. package/dist/backend/terminal.d.ts +0 -49
  26. package/dist/backend/terminal.d.ts.map +0 -1
  27. package/dist/backend/terminal.js +0 -209
  28. package/dist/backend/terminal.js.map +0 -1
  29. package/dist/backend/types.d.ts +0 -77
  30. package/dist/backend/types.d.ts.map +0 -1
  31. package/dist/backend/types.js +0 -6
  32. package/dist/backend/types.js.map +0 -1
  33. package/dist/backend-server.d.cts +0 -2
  34. package/dist/backend-server.d.ts +0 -11
  35. package/dist/backend-server.d.ts.map +0 -1
  36. package/dist/backend-server.js +0 -43
  37. package/dist/backend-server.js.map +0 -1
  38. package/dist/cli.d.cts +0 -2
  39. package/dist/cli.d.ts +0 -7
  40. package/dist/cli.d.ts.map +0 -1
  41. package/dist/cli.js +0 -510
  42. package/dist/cli.js.map +0 -1
  43. package/dist/core/artefakte.d.ts +0 -12
  44. package/dist/core/artefakte.d.ts.map +0 -1
  45. package/dist/core/artefakte.js +0 -152
  46. package/dist/core/artefakte.js.map +0 -1
  47. package/dist/core/commands.d.ts +0 -26
  48. package/dist/core/commands.d.ts.map +0 -1
  49. package/dist/core/commands.js +0 -550
  50. package/dist/core/commands.js.map +0 -1
  51. package/dist/core/config.d.ts +0 -10
  52. package/dist/core/config.d.ts.map +0 -1
  53. package/dist/core/config.js +0 -143
  54. package/dist/core/config.js.map +0 -1
  55. package/dist/core/context-loader.d.ts +0 -21
  56. package/dist/core/context-loader.d.ts.map +0 -1
  57. package/dist/core/context-loader.js +0 -212
  58. package/dist/core/context-loader.js.map +0 -1
  59. package/dist/core/core.d.ts +0 -91
  60. package/dist/core/core.d.ts.map +0 -1
  61. package/dist/core/core.js +0 -823
  62. package/dist/core/core.js.map +0 -1
  63. package/dist/core/dashboard-launcher.d.ts +0 -56
  64. package/dist/core/dashboard-launcher.d.ts.map +0 -1
  65. package/dist/core/dashboard-launcher.js +0 -246
  66. package/dist/core/dashboard-launcher.js.map +0 -1
  67. package/dist/core/drift.d.ts +0 -37
  68. package/dist/core/drift.d.ts.map +0 -1
  69. package/dist/core/drift.js +0 -213
  70. package/dist/core/drift.js.map +0 -1
  71. package/dist/core/frontmatter.d.ts +0 -33
  72. package/dist/core/frontmatter.d.ts.map +0 -1
  73. package/dist/core/frontmatter.js +0 -193
  74. package/dist/core/frontmatter.js.map +0 -1
  75. package/dist/core/index.d.ts +0 -28
  76. package/dist/core/index.d.ts.map +0 -1
  77. package/dist/core/index.js +0 -189
  78. package/dist/core/index.js.map +0 -1
  79. package/dist/core/init.d.ts +0 -287
  80. package/dist/core/init.d.ts.map +0 -1
  81. package/dist/core/init.js +0 -816
  82. package/dist/core/init.js.map +0 -1
  83. package/dist/core/milestone.d.ts +0 -9
  84. package/dist/core/milestone.d.ts.map +0 -1
  85. package/dist/core/milestone.js +0 -230
  86. package/dist/core/milestone.js.map +0 -1
  87. package/dist/core/phase.d.ts +0 -53
  88. package/dist/core/phase.d.ts.map +0 -1
  89. package/dist/core/phase.js +0 -891
  90. package/dist/core/phase.js.map +0 -1
  91. package/dist/core/roadmap.d.ts +0 -10
  92. package/dist/core/roadmap.d.ts.map +0 -1
  93. package/dist/core/roadmap.js +0 -165
  94. package/dist/core/roadmap.js.map +0 -1
  95. package/dist/core/skills.d.ts +0 -20
  96. package/dist/core/skills.d.ts.map +0 -1
  97. package/dist/core/skills.js +0 -144
  98. package/dist/core/skills.js.map +0 -1
  99. package/dist/core/start.d.ts +0 -15
  100. package/dist/core/start.d.ts.map +0 -1
  101. package/dist/core/start.js +0 -80
  102. package/dist/core/start.js.map +0 -1
  103. package/dist/core/state.d.ts +0 -32
  104. package/dist/core/state.d.ts.map +0 -1
  105. package/dist/core/state.js +0 -582
  106. package/dist/core/state.js.map +0 -1
  107. package/dist/core/template.d.ts +0 -30
  108. package/dist/core/template.d.ts.map +0 -1
  109. package/dist/core/template.js +0 -223
  110. package/dist/core/template.js.map +0 -1
  111. package/dist/core/types.d.ts +0 -519
  112. package/dist/core/types.d.ts.map +0 -1
  113. package/dist/core/types.js +0 -60
  114. package/dist/core/types.js.map +0 -1
  115. package/dist/core/verify.d.ts +0 -128
  116. package/dist/core/verify.d.ts.map +0 -1
  117. package/dist/core/verify.js +0 -754
  118. package/dist/core/verify.js.map +0 -1
  119. package/dist/hooks/index.d.ts +0 -11
  120. package/dist/hooks/index.d.ts.map +0 -1
  121. package/dist/hooks/index.js +0 -18
  122. package/dist/hooks/index.js.map +0 -1
  123. package/dist/hooks/maxsim-check-update.d.ts +0 -17
  124. package/dist/hooks/maxsim-check-update.d.ts.map +0 -1
  125. package/dist/hooks/maxsim-check-update.js +0 -101
  126. package/dist/hooks/maxsim-check-update.js.map +0 -1
  127. package/dist/hooks/maxsim-context-monitor.d.ts +0 -21
  128. package/dist/hooks/maxsim-context-monitor.d.ts.map +0 -1
  129. package/dist/hooks/maxsim-context-monitor.js +0 -131
  130. package/dist/hooks/maxsim-context-monitor.js.map +0 -1
  131. package/dist/hooks/maxsim-statusline.d.ts +0 -19
  132. package/dist/hooks/maxsim-statusline.d.ts.map +0 -1
  133. package/dist/hooks/maxsim-statusline.js +0 -146
  134. package/dist/hooks/maxsim-statusline.js.map +0 -1
  135. package/dist/hooks/shared.d.ts +0 -11
  136. package/dist/hooks/shared.d.ts.map +0 -1
  137. package/dist/hooks/shared.js +0 -29
  138. package/dist/hooks/shared.js.map +0 -1
  139. package/dist/index.d.ts +0 -2
  140. package/dist/index.d.ts.map +0 -1
  141. package/dist/index.js +0 -3
  142. package/dist/index.js.map +0 -1
  143. package/dist/install/adapters.d.ts +0 -6
  144. package/dist/install/adapters.d.ts.map +0 -1
  145. package/dist/install/adapters.js +0 -65
  146. package/dist/install/adapters.js.map +0 -1
  147. package/dist/install/copy.d.ts +0 -6
  148. package/dist/install/copy.d.ts.map +0 -1
  149. package/dist/install/copy.js +0 -71
  150. package/dist/install/copy.js.map +0 -1
  151. package/dist/install/dashboard.d.ts +0 -16
  152. package/dist/install/dashboard.d.ts.map +0 -1
  153. package/dist/install/dashboard.js +0 -273
  154. package/dist/install/dashboard.js.map +0 -1
  155. package/dist/install/hooks.d.ts +0 -31
  156. package/dist/install/hooks.d.ts.map +0 -1
  157. package/dist/install/hooks.js +0 -260
  158. package/dist/install/hooks.js.map +0 -1
  159. package/dist/install/index.d.ts +0 -2
  160. package/dist/install/index.d.ts.map +0 -1
  161. package/dist/install/index.js +0 -534
  162. package/dist/install/index.js.map +0 -1
  163. package/dist/install/manifest.d.ts +0 -23
  164. package/dist/install/manifest.d.ts.map +0 -1
  165. package/dist/install/manifest.js +0 -133
  166. package/dist/install/manifest.js.map +0 -1
  167. package/dist/install/patches.d.ts +0 -10
  168. package/dist/install/patches.d.ts.map +0 -1
  169. package/dist/install/patches.js +0 -124
  170. package/dist/install/patches.js.map +0 -1
  171. package/dist/install/shared.d.ts +0 -56
  172. package/dist/install/shared.d.ts.map +0 -1
  173. package/dist/install/shared.js +0 -181
  174. package/dist/install/shared.js.map +0 -1
  175. package/dist/install/uninstall.d.ts +0 -5
  176. package/dist/install/uninstall.d.ts.map +0 -1
  177. package/dist/install/uninstall.js +0 -222
  178. package/dist/install/uninstall.js.map +0 -1
  179. package/dist/install/utils.d.ts +0 -27
  180. package/dist/install/utils.d.ts.map +0 -1
  181. package/dist/install/utils.js +0 -99
  182. package/dist/install/utils.js.map +0 -1
  183. package/dist/install.d.cts +0 -2
  184. package/dist/mcp/config-tools.d.ts +0 -13
  185. package/dist/mcp/config-tools.d.ts.map +0 -1
  186. package/dist/mcp/config-tools.js +0 -66
  187. package/dist/mcp/config-tools.js.map +0 -1
  188. package/dist/mcp/context-tools.d.ts +0 -13
  189. package/dist/mcp/context-tools.d.ts.map +0 -1
  190. package/dist/mcp/context-tools.js +0 -176
  191. package/dist/mcp/context-tools.js.map +0 -1
  192. package/dist/mcp/index.d.ts +0 -11
  193. package/dist/mcp/index.d.ts.map +0 -1
  194. package/dist/mcp/index.js +0 -26
  195. package/dist/mcp/index.js.map +0 -1
  196. package/dist/mcp/phase-tools.d.ts +0 -13
  197. package/dist/mcp/phase-tools.d.ts.map +0 -1
  198. package/dist/mcp/phase-tools.js +0 -177
  199. package/dist/mcp/phase-tools.js.map +0 -1
  200. package/dist/mcp/roadmap-tools.d.ts +0 -13
  201. package/dist/mcp/roadmap-tools.d.ts.map +0 -1
  202. package/dist/mcp/roadmap-tools.js +0 -79
  203. package/dist/mcp/roadmap-tools.js.map +0 -1
  204. package/dist/mcp/state-tools.d.ts +0 -13
  205. package/dist/mcp/state-tools.d.ts.map +0 -1
  206. package/dist/mcp/state-tools.js +0 -185
  207. package/dist/mcp/state-tools.js.map +0 -1
  208. package/dist/mcp/todo-tools.d.ts +0 -13
  209. package/dist/mcp/todo-tools.d.ts.map +0 -1
  210. package/dist/mcp/todo-tools.js +0 -143
  211. package/dist/mcp/todo-tools.js.map +0 -1
  212. package/dist/mcp/utils.d.ts +0 -27
  213. package/dist/mcp/utils.d.ts.map +0 -1
  214. package/dist/mcp/utils.js +0 -82
  215. package/dist/mcp/utils.js.map +0 -1
  216. package/dist/mcp-server.d.cts +0 -2
  217. package/dist/mcp-server.d.ts +0 -12
  218. package/dist/mcp-server.d.ts.map +0 -1
  219. package/dist/mcp-server.js +0 -31
  220. package/dist/mcp-server.js.map +0 -1
  221. package/dist/server-By0TN-nC.cjs.map +0 -1
@@ -43,7 +43,8 @@ let node_net = require("node:net");
43
43
  let node_util = require("node:util");
44
44
  let node_fs_promises = require("node:fs/promises");
45
45
  node_fs_promises = __toESM(node_fs_promises);
46
- require("node:child_process");
46
+ let node_child_process = require("node:child_process");
47
+ node_child_process = __toESM(node_child_process);
47
48
  let node_buffer = require("node:buffer");
48
49
  let child_process = require("child_process");
49
50
  require("node:events");
@@ -32122,17 +32123,17 @@ var require_view = /* @__PURE__ */ __commonJSMin(((exports, module) => {
32122
32123
  * @private
32123
32124
  */
32124
32125
  var debug = require_src$3()("express:view");
32125
- var path$23 = require("path");
32126
- var fs$21 = require("fs");
32126
+ var path$25 = require("path");
32127
+ var fs$23 = require("fs");
32127
32128
  /**
32128
32129
  * Module variables.
32129
32130
  * @private
32130
32131
  */
32131
- var dirname = path$23.dirname;
32132
- var basename = path$23.basename;
32133
- var extname = path$23.extname;
32134
- var join = path$23.join;
32135
- var resolve = path$23.resolve;
32132
+ var dirname = path$25.dirname;
32133
+ var basename = path$25.basename;
32134
+ var extname = path$25.extname;
32135
+ var join = path$25.join;
32136
+ var resolve = path$25.resolve;
32136
32137
  /**
32137
32138
  * Module exports.
32138
32139
  * @public
@@ -32229,7 +32230,7 @@ var require_view = /* @__PURE__ */ __commonJSMin(((exports, module) => {
32229
32230
  function tryStat(path) {
32230
32231
  debug("stat \"%s\"", path);
32231
32232
  try {
32232
- return fs$21.statSync(path);
32233
+ return fs$23.statSync(path);
32233
32234
  } catch (e) {
32234
32235
  return;
32235
32236
  }
@@ -34450,7 +34451,7 @@ var require_types$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
34450
34451
  //#region ../../node_modules/send/node_modules/mime/mime.js
34451
34452
  var require_mime = /* @__PURE__ */ __commonJSMin(((exports, module) => {
34452
34453
  require("path");
34453
- var fs$20 = require("fs");
34454
+ var fs$22 = require("fs");
34454
34455
  function Mime() {
34455
34456
  this.types = Object.create(null);
34456
34457
  this.extensions = Object.create(null);
@@ -34485,7 +34486,7 @@ var require_mime = /* @__PURE__ */ __commonJSMin(((exports, module) => {
34485
34486
  Mime.prototype.load = function(file) {
34486
34487
  this._loading = file;
34487
34488
  var map = {};
34488
- fs$20.readFileSync(file, "ascii").split(/[\r\n]+/).forEach(function(line) {
34489
+ fs$22.readFileSync(file, "ascii").split(/[\r\n]+/).forEach(function(line) {
34489
34490
  var fields = line.replace(/\s*#.*|^\s*|\s*$/g, "").split(/\s+/);
34490
34491
  map[fields.shift()] = fields;
34491
34492
  });
@@ -34764,12 +34765,12 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
34764
34765
  var escapeHtml = require_escape_html();
34765
34766
  var etag = require_etag();
34766
34767
  var fresh = require_fresh();
34767
- var fs$19 = require("fs");
34768
+ var fs$21 = require("fs");
34768
34769
  var mime = require_mime();
34769
34770
  var ms = require_ms();
34770
34771
  var onFinished = require_on_finished();
34771
34772
  var parseRange = require_range_parser();
34772
- var path$22 = require("path");
34773
+ var path$24 = require("path");
34773
34774
  var statuses = require_statuses();
34774
34775
  var Stream = require("stream");
34775
34776
  var util$3 = require("util");
@@ -34777,11 +34778,11 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
34777
34778
  * Path function references.
34778
34779
  * @private
34779
34780
  */
34780
- var extname = path$22.extname;
34781
- var join = path$22.join;
34782
- var normalize = path$22.normalize;
34783
- var resolve = path$22.resolve;
34784
- var sep = path$22.sep;
34781
+ var extname = path$24.extname;
34782
+ var join = path$24.join;
34783
+ var normalize = path$24.normalize;
34784
+ var resolve = path$24.resolve;
34785
+ var sep = path$24.sep;
34785
34786
  /**
34786
34787
  * Regular expression for identifying a bytes Range header.
34787
34788
  * @private
@@ -35229,7 +35230,7 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
35229
35230
  var i = 0;
35230
35231
  var self = this;
35231
35232
  debug("stat \"%s\"", path);
35232
- fs$19.stat(path, function onstat(err, stat) {
35233
+ fs$21.stat(path, function onstat(err, stat) {
35233
35234
  if (err && err.code === "ENOENT" && !extname(path) && path[path.length - 1] !== sep) return next(err);
35234
35235
  if (err) return self.onStatError(err);
35235
35236
  if (stat.isDirectory()) return self.redirect(path);
@@ -35240,7 +35241,7 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
35240
35241
  if (self._extensions.length <= i) return err ? self.onStatError(err) : self.error(404);
35241
35242
  var p = path + "." + self._extensions[i++];
35242
35243
  debug("stat \"%s\"", p);
35243
- fs$19.stat(p, function(err, stat) {
35244
+ fs$21.stat(p, function(err, stat) {
35244
35245
  if (err) return next(err);
35245
35246
  if (stat.isDirectory()) return next();
35246
35247
  self.emit("file", p, stat);
@@ -35264,7 +35265,7 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
35264
35265
  }
35265
35266
  var p = join(path, self._index[i]);
35266
35267
  debug("stat \"%s\"", p);
35267
- fs$19.stat(p, function(err, stat) {
35268
+ fs$21.stat(p, function(err, stat) {
35268
35269
  if (err) return next(err);
35269
35270
  if (stat.isDirectory()) return next();
35270
35271
  self.emit("file", p, stat);
@@ -35283,7 +35284,7 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
35283
35284
  SendStream.prototype.stream = function stream(path, options) {
35284
35285
  var self = this;
35285
35286
  var res = this.res;
35286
- var stream$5 = fs$19.createReadStream(path, options);
35287
+ var stream$5 = fs$21.createReadStream(path, options);
35287
35288
  this.emit("stream", stream$5);
35288
35289
  stream$5.pipe(res);
35289
35290
  function cleanup() {
@@ -38817,7 +38818,7 @@ var require_response = /* @__PURE__ */ __commonJSMin(((exports, module) => {
38817
38818
  var http$3 = require("http");
38818
38819
  var isAbsolute = require_utils$1().isAbsolute;
38819
38820
  var onFinished = require_on_finished();
38820
- var path$21 = require("path");
38821
+ var path$23 = require("path");
38821
38822
  var statuses = require_statuses();
38822
38823
  var merge = require_utils_merge();
38823
38824
  var sign = require_cookie_signature().sign;
@@ -38826,9 +38827,9 @@ var require_response = /* @__PURE__ */ __commonJSMin(((exports, module) => {
38826
38827
  var setCharset = require_utils$1().setCharset;
38827
38828
  var cookie = require_cookie();
38828
38829
  var send = require_send();
38829
- var extname = path$21.extname;
38830
+ var extname = path$23.extname;
38830
38831
  var mime = send.mime;
38831
- var resolve = path$21.resolve;
38832
+ var resolve = path$23.resolve;
38832
38833
  var vary = require_vary();
38833
38834
  /**
38834
38835
  * Response prototype.
@@ -78911,6 +78912,1462 @@ const nullableType = ZodNullable.create;
78911
78912
  const preprocessType = ZodEffects.createWithPreprocess;
78912
78913
  const pipelineType = ZodPipeline.create;
78913
78914
 
78915
+ //#endregion
78916
+ //#region src/github/gh.ts
78917
+ /**
78918
+ * GitHub CLI Wrapper — Core gh CLI interaction layer
78919
+ *
78920
+ * Wraps the `gh` CLI using `child_process.execFile` (never `exec`) for security.
78921
+ * Provides typed results via GhResult<T> discriminated union.
78922
+ * Supports graceful degradation: detectGitHubMode() returns 'local-only'
78923
+ * when gh is not installed or not authenticated with required scopes.
78924
+ *
78925
+ * CRITICAL: Never import octokit or any npm GitHub SDK.
78926
+ * CRITICAL: Never call process.exit() — return GhResult instead.
78927
+ */
78928
+ const execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
78929
+ /**
78930
+ * Check if the `gh` CLI is installed and authenticated with required scopes.
78931
+ *
78932
+ * Parses the output of `gh auth status` (which writes to stderr, not stdout).
78933
+ * Returns structured AuthStatus with scope detection for 'project' scope.
78934
+ * Timeout: 10 seconds.
78935
+ */
78936
+ async function checkGhAuth() {
78937
+ try {
78938
+ const { stdout, stderr } = await execFileAsync("gh", ["auth", "status"], { timeout: 1e4 });
78939
+ const output = stderr || stdout;
78940
+ const authenticated = !output.includes("not logged in");
78941
+ const scopeMatch = output.match(/Token scopes?:\s*'([^']+(?:',\s*'[^']+)*)'/);
78942
+ const scopes = [];
78943
+ if (scopeMatch) {
78944
+ const allScopes = scopeMatch[0].matchAll(/'([^']+)'/g);
78945
+ for (const m of allScopes) scopes.push(m[1]);
78946
+ }
78947
+ const userMatch = output.match(/Logged in to [^\s]+ as ([^\s(]+)/);
78948
+ return {
78949
+ installed: true,
78950
+ authenticated,
78951
+ scopes,
78952
+ hasProjectScope: scopes.includes("project") || scopes.includes("read:project"),
78953
+ username: userMatch ? userMatch[1] : null
78954
+ };
78955
+ } catch (e) {
78956
+ const error = e;
78957
+ if (error.code === "ENOENT") return {
78958
+ installed: false,
78959
+ authenticated: false,
78960
+ scopes: [],
78961
+ hasProjectScope: false,
78962
+ username: null
78963
+ };
78964
+ error.stderr || error.message;
78965
+ return {
78966
+ installed: true,
78967
+ authenticated: false,
78968
+ scopes: [],
78969
+ hasProjectScope: false,
78970
+ username: null
78971
+ };
78972
+ }
78973
+ }
78974
+ /**
78975
+ * Detect the GitHub integration mode based on auth status.
78976
+ *
78977
+ * Returns 'full' only when gh is installed, authenticated, and has the
78978
+ * 'project' scope. Otherwise returns 'local-only' for graceful degradation.
78979
+ */
78980
+ async function detectGitHubMode() {
78981
+ const auth = await checkGhAuth();
78982
+ if (!auth.installed) return "local-only";
78983
+ if (!auth.authenticated) return "local-only";
78984
+ if (!auth.hasProjectScope) {
78985
+ console.error("[maxsim] GitHub Projects requires 'project' scope. Run: gh auth refresh -s project");
78986
+ return "local-only";
78987
+ }
78988
+ return "full";
78989
+ }
78990
+ /**
78991
+ * Execute a `gh` CLI command and return a typed GhResult.
78992
+ *
78993
+ * - Uses `execFile` (not `exec`) for security
78994
+ * - Default timeout: 30 seconds
78995
+ * - Auto-detects JSON output when args contain `--json` or `--format`
78996
+ * - Maps exit codes and stderr patterns to GhErrorCode
78997
+ * - For `gh issue create`: does NOT try to parse JSON (it returns a URL string)
78998
+ * - Always includes raw stderr in error messages for AI consumption
78999
+ */
79000
+ async function ghExec(args, options) {
79001
+ const timeout = options?.timeout ?? 3e4;
79002
+ const isIssueCreate = args[0] === "issue" && args[1] === "create";
79003
+ const hasJsonFlag = args.includes("--json") || args.some((a) => a.startsWith("--format"));
79004
+ const shouldParseJson = options?.parseJson ?? (hasJsonFlag && !isIssueCreate);
79005
+ try {
79006
+ const { stdout, stderr } = await execFileAsync("gh", args, {
79007
+ cwd: options?.cwd,
79008
+ timeout,
79009
+ maxBuffer: 10 * 1024 * 1024
79010
+ });
79011
+ if (shouldParseJson) try {
79012
+ return {
79013
+ ok: true,
79014
+ data: JSON.parse(stdout)
79015
+ };
79016
+ } catch {
79017
+ return {
79018
+ ok: false,
79019
+ error: `Failed to parse gh output as JSON: ${stdout.slice(0, 500)}`,
79020
+ code: "UNKNOWN"
79021
+ };
79022
+ }
79023
+ return {
79024
+ ok: true,
79025
+ data: stdout.trim()
79026
+ };
79027
+ } catch (e) {
79028
+ return mapExecError(e);
79029
+ }
79030
+ }
79031
+ /**
79032
+ * Execute a GraphQL query via `gh api graphql`.
79033
+ *
79034
+ * - String variables use `-f key=value`
79035
+ * - Non-string variables (numbers, booleans) use `-F key=value`
79036
+ * - Parses JSON response and checks for GraphQL `errors` array
79037
+ */
79038
+ async function ghGraphQL(query, variables) {
79039
+ const args = [
79040
+ "api",
79041
+ "graphql",
79042
+ "-f",
79043
+ `query=${query}`
79044
+ ];
79045
+ if (variables) for (const [key, value] of Object.entries(variables)) if (typeof value === "string") args.push("-f", `${key}=${value}`);
79046
+ else args.push("-F", `${key}=${String(value)}`);
79047
+ const result = await ghExec(args, { parseJson: true });
79048
+ if (!result.ok) return result;
79049
+ if (result.data.errors && result.data.errors.length > 0) {
79050
+ const messages = result.data.errors.map((e) => e.message).join("; ");
79051
+ const code = mapGraphQLErrorCode(messages);
79052
+ return {
79053
+ ok: false,
79054
+ error: `GraphQL error: ${messages}`,
79055
+ code
79056
+ };
79057
+ }
79058
+ if (result.data.data === void 0) return {
79059
+ ok: false,
79060
+ error: "GraphQL response missing data field",
79061
+ code: "UNKNOWN"
79062
+ };
79063
+ return {
79064
+ ok: true,
79065
+ data: result.data.data
79066
+ };
79067
+ }
79068
+ /**
79069
+ * Map an execFile error to a GhResult with appropriate GhErrorCode.
79070
+ */
79071
+ function mapExecError(e) {
79072
+ const error = e;
79073
+ if (error.code === "ENOENT") return {
79074
+ ok: false,
79075
+ error: "gh CLI is not installed. Install from https://cli.github.com/",
79076
+ code: "NOT_INSTALLED"
79077
+ };
79078
+ const stderr = error.stderr || error.message || "";
79079
+ if (error.status === 4) return {
79080
+ ok: false,
79081
+ error: `Not found: ${stderr}`,
79082
+ code: "NOT_FOUND"
79083
+ };
79084
+ if (stderr.includes("not logged in") || stderr.includes("authentication") || stderr.includes("auth login") || stderr.includes("401")) return {
79085
+ ok: false,
79086
+ error: `Authentication required: ${stderr}`,
79087
+ code: "NOT_AUTHENTICATED"
79088
+ };
79089
+ if (stderr.includes("403") || stderr.includes("permission") || stderr.includes("denied")) return {
79090
+ ok: false,
79091
+ error: `Permission denied: ${stderr}`,
79092
+ code: "PERMISSION_DENIED"
79093
+ };
79094
+ if (stderr.includes("rate limit") || stderr.includes("429") || stderr.includes("API rate")) return {
79095
+ ok: false,
79096
+ error: `Rate limited: ${stderr}`,
79097
+ code: "RATE_LIMITED"
79098
+ };
79099
+ if (stderr.includes("scope") || stderr.includes("insufficient")) return {
79100
+ ok: false,
79101
+ error: `Missing scope: ${stderr}`,
79102
+ code: "SCOPE_MISSING"
79103
+ };
79104
+ if (stderr.includes("not found") || stderr.includes("404") || stderr.includes("Could not resolve")) return {
79105
+ ok: false,
79106
+ error: `Not found: ${stderr}`,
79107
+ code: "NOT_FOUND"
79108
+ };
79109
+ return {
79110
+ ok: false,
79111
+ error: `gh command failed: ${stderr}`,
79112
+ code: "UNKNOWN"
79113
+ };
79114
+ }
79115
+ /**
79116
+ * Map GraphQL error messages to GhErrorCode.
79117
+ */
79118
+ function mapGraphQLErrorCode(message) {
79119
+ const lower = message.toLowerCase();
79120
+ if (lower.includes("not found") || lower.includes("could not resolve")) return "NOT_FOUND";
79121
+ if (lower.includes("insufficient") || lower.includes("scope")) return "SCOPE_MISSING";
79122
+ if (lower.includes("forbidden") || lower.includes("permission")) return "PERMISSION_DENIED";
79123
+ if (lower.includes("rate") || lower.includes("throttl")) return "RATE_LIMITED";
79124
+ return "UNKNOWN";
79125
+ }
79126
+
79127
+ //#endregion
79128
+ //#region src/github/mapping.ts
79129
+ /**
79130
+ * GitHub Issues Mapping — Persistence layer for github-issues.json
79131
+ *
79132
+ * Manages the `.planning/github-issues.json` file that maps MAXSIM tasks/todos
79133
+ * to their corresponding GitHub issue numbers, node IDs, and project item IDs.
79134
+ *
79135
+ * All file operations use synchronous fs (matching the pattern in existing core modules).
79136
+ * Uses planningPath() from core to construct file paths.
79137
+ *
79138
+ * CRITICAL: Never call process.exit() — throw or return null instead.
79139
+ */
79140
+ const MAPPING_FILENAME = "github-issues.json";
79141
+ /**
79142
+ * Get the absolute path to `.planning/github-issues.json` for a given cwd.
79143
+ */
79144
+ function mappingFilePath(cwd) {
79145
+ return planningPath(cwd, MAPPING_FILENAME);
79146
+ }
79147
+ /**
79148
+ * Load and parse the mapping file.
79149
+ *
79150
+ * Returns null if the file does not exist.
79151
+ * Throws on malformed JSON or invalid structure (missing required fields).
79152
+ */
79153
+ function loadMapping(cwd) {
79154
+ const filePath = mappingFilePath(cwd);
79155
+ try {
79156
+ node_fs.default.statSync(filePath);
79157
+ } catch {
79158
+ return null;
79159
+ }
79160
+ const raw = node_fs.default.readFileSync(filePath, "utf-8");
79161
+ const parsed = JSON.parse(raw);
79162
+ if (typeof parsed.project_number !== "number" || typeof parsed.repo !== "string") throw new Error(`Invalid github-issues.json: missing required fields 'project_number' (number) and 'repo' (string)`);
79163
+ return parsed;
79164
+ }
79165
+ /**
79166
+ * Write the mapping file to `.planning/github-issues.json`.
79167
+ *
79168
+ * Creates the `.planning/` directory if it does not exist.
79169
+ * Writes with 2-space indent for readability and diff-friendliness.
79170
+ */
79171
+ function saveMapping(cwd, mapping) {
79172
+ const filePath = mappingFilePath(cwd);
79173
+ const dir = node_path.default.dirname(filePath);
79174
+ node_fs.default.mkdirSync(dir, { recursive: true });
79175
+ node_fs.default.writeFileSync(filePath, JSON.stringify(mapping, null, 2) + "\n", "utf-8");
79176
+ }
79177
+ /**
79178
+ * Update a specific task's issue mapping within a phase.
79179
+ *
79180
+ * Load-modify-save pattern. Creates phase entry if it does not exist.
79181
+ * Merges partial data with existing entry (if any).
79182
+ *
79183
+ * @throws If mapping file does not exist (must be initialized first via saveMapping)
79184
+ */
79185
+ function updateTaskMapping(cwd, phaseNum, taskId, data) {
79186
+ const mapping = loadMapping(cwd);
79187
+ if (!mapping) throw new Error("github-issues.json does not exist. Run project setup first.");
79188
+ if (!mapping.phases[phaseNum]) mapping.phases[phaseNum] = {
79189
+ tracking_issue: {
79190
+ number: 0,
79191
+ node_id: "",
79192
+ item_id: "",
79193
+ status: "To Do"
79194
+ },
79195
+ plan: "",
79196
+ tasks: {}
79197
+ };
79198
+ const existing = mapping.phases[phaseNum].tasks[taskId];
79199
+ const defaults = {
79200
+ number: 0,
79201
+ node_id: "",
79202
+ item_id: "",
79203
+ status: "To Do"
79204
+ };
79205
+ mapping.phases[phaseNum].tasks[taskId] = Object.assign(defaults, existing, data);
79206
+ saveMapping(cwd, mapping);
79207
+ }
79208
+ /**
79209
+ * Update a specific todo's issue mapping.
79210
+ *
79211
+ * Load-modify-save pattern. Creates `todos` section if missing.
79212
+ * Merges partial data with existing entry (if any).
79213
+ *
79214
+ * @throws If mapping file does not exist (must be initialized first via saveMapping)
79215
+ */
79216
+ function updateTodoMapping(cwd, todoId, data) {
79217
+ const mapping = loadMapping(cwd);
79218
+ if (!mapping) throw new Error("github-issues.json does not exist. Run project setup first.");
79219
+ if (!mapping.todos) mapping.todos = {};
79220
+ const existing = mapping.todos[todoId];
79221
+ const defaults = {
79222
+ number: 0,
79223
+ node_id: "",
79224
+ item_id: "",
79225
+ status: "To Do"
79226
+ };
79227
+ mapping.todos[todoId] = Object.assign(defaults, existing, data);
79228
+ saveMapping(cwd, mapping);
79229
+ }
79230
+ /**
79231
+ * Create a properly typed empty mapping object with sensible defaults.
79232
+ *
79233
+ * Used during initial project setup to create the mapping file.
79234
+ */
79235
+ function createEmptyMapping(repo) {
79236
+ return {
79237
+ project_number: 0,
79238
+ project_id: "",
79239
+ repo,
79240
+ status_field_id: "",
79241
+ status_options: {},
79242
+ estimate_field_id: "",
79243
+ milestone_id: 0,
79244
+ milestone_title: "",
79245
+ labels: {},
79246
+ phases: {},
79247
+ todos: {}
79248
+ };
79249
+ }
79250
+
79251
+ //#endregion
79252
+ //#region src/github/issues.ts
79253
+ /**
79254
+ * Parse an issue number from a `gh issue create` stdout URL.
79255
+ *
79256
+ * `gh issue create` outputs a URL like:
79257
+ * https://github.com/owner/repo/issues/42\n
79258
+ *
79259
+ * We trim whitespace and extract the last path segment as the issue number.
79260
+ */
79261
+ function parseIssueNumberFromUrl(stdout) {
79262
+ const lastSegment = stdout.trim().split("/").pop();
79263
+ if (!lastSegment) return null;
79264
+ const num = parseInt(lastSegment, 10);
79265
+ return Number.isNaN(num) ? null : num;
79266
+ }
79267
+ /**
79268
+ * After creating an issue (parsed number from URL), fetch its node_id
79269
+ * via `gh issue view {number} --json nodeId,number,url`.
79270
+ */
79271
+ async function fetchIssueDetails(issueNumber) {
79272
+ const result = await ghExec([
79273
+ "issue",
79274
+ "view",
79275
+ String(issueNumber),
79276
+ "--json",
79277
+ "nodeId,number,url"
79278
+ ], { parseJson: true });
79279
+ if (!result.ok) return {
79280
+ ok: false,
79281
+ error: result.error,
79282
+ code: result.code
79283
+ };
79284
+ return {
79285
+ ok: true,
79286
+ data: {
79287
+ number: result.data.number,
79288
+ url: result.data.url,
79289
+ node_id: result.data.nodeId
79290
+ }
79291
+ };
79292
+ }
79293
+ /**
79294
+ * Create a task issue with full specification body in collapsible details section.
79295
+ *
79296
+ * Title format: `[P{phaseNum}] {title}`
79297
+ * Body includes summary, actions, acceptance criteria, dependencies in `<details>`.
79298
+ * Labels: maxsim, phase-task.
79299
+ *
79300
+ * Returns the issue number, URL, and node_id.
79301
+ */
79302
+ async function createTaskIssue(opts) {
79303
+ const issueTitle = `[P${opts.phaseNum}] ${opts.title}`;
79304
+ const depsSection = opts.dependencies && opts.dependencies.length > 0 ? `\n### Dependencies\nDepends on: ${opts.dependencies.map((d) => `#${d}`).join(", ")}\n` : "";
79305
+ const estimateSection = opts.estimate !== void 0 ? `\n### Estimate\n${opts.estimate} points\n` : "";
79306
+ const args = [
79307
+ "issue",
79308
+ "create",
79309
+ "--title",
79310
+ issueTitle,
79311
+ "--body",
79312
+ `## Summary
79313
+ ${opts.summary}
79314
+
79315
+ <details>
79316
+ <summary>Full Specification</summary>
79317
+
79318
+ ### Actions
79319
+ ${opts.actions.map((a) => `- ${a}`).join("\n")}
79320
+
79321
+ ### Acceptance Criteria
79322
+ ${opts.acceptanceCriteria.map((c) => `- [ ] ${c}`).join("\n")}
79323
+ ${depsSection}${estimateSection}
79324
+ </details>
79325
+
79326
+ ---
79327
+ *Phase: ${opts.phaseNum} | Plan: ${opts.planNum} | Task: ${opts.taskId}*
79328
+ *Generated by MAXSIM*`
79329
+ ];
79330
+ args.push("--label", "maxsim");
79331
+ args.push("--label", "phase-task");
79332
+ if (opts.labels) for (const label of opts.labels) args.push("--label", label);
79333
+ if (opts.milestone) args.push("--milestone", opts.milestone);
79334
+ if (opts.projectTitle) args.push("--project", opts.projectTitle);
79335
+ const createResult = await ghExec(args);
79336
+ if (!createResult.ok) return {
79337
+ ok: false,
79338
+ error: createResult.error,
79339
+ code: createResult.code
79340
+ };
79341
+ const issueNumber = parseIssueNumberFromUrl(createResult.data);
79342
+ if (issueNumber === null) return {
79343
+ ok: false,
79344
+ error: `Failed to parse issue number from gh output: ${createResult.data}`,
79345
+ code: "UNKNOWN"
79346
+ };
79347
+ return fetchIssueDetails(issueNumber);
79348
+ }
79349
+ /**
79350
+ * Create a parent tracking issue for a phase with a live checkbox task list.
79351
+ *
79352
+ * Title format: `[Phase {phaseNum}] {phaseName}`
79353
+ * Body includes task list with checkbox links: `- [ ] #{childNumber}`
79354
+ * Labels: maxsim, phase-task.
79355
+ */
79356
+ async function createParentTrackingIssue(opts) {
79357
+ const issueTitle = `[Phase ${opts.phaseNum}] ${opts.phaseName}`;
79358
+ const taskList = opts.childIssueNumbers.map((n) => `- [ ] #${n}`).join("\n");
79359
+ const args = [
79360
+ "issue",
79361
+ "create",
79362
+ "--title",
79363
+ issueTitle,
79364
+ "--body",
79365
+ `## Phase ${opts.phaseNum}: ${opts.phaseName}
79366
+
79367
+ ### Tasks
79368
+ ${taskList}
79369
+
79370
+ ---
79371
+ *Phase tracking issue -- Generated by MAXSIM*`
79372
+ ];
79373
+ args.push("--label", "maxsim");
79374
+ args.push("--label", "phase-task");
79375
+ if (opts.milestone) args.push("--milestone", opts.milestone);
79376
+ if (opts.projectTitle) args.push("--project", opts.projectTitle);
79377
+ const createResult = await ghExec(args);
79378
+ if (!createResult.ok) return {
79379
+ ok: false,
79380
+ error: createResult.error,
79381
+ code: createResult.code
79382
+ };
79383
+ const issueNumber = parseIssueNumberFromUrl(createResult.data);
79384
+ if (issueNumber === null) return {
79385
+ ok: false,
79386
+ error: `Failed to parse issue number from gh output: ${createResult.data}`,
79387
+ code: "UNKNOWN"
79388
+ };
79389
+ return fetchIssueDetails(issueNumber);
79390
+ }
79391
+ /**
79392
+ * Create a todo issue with a lighter body (no collapsible details section).
79393
+ *
79394
+ * Labels: maxsim, todo.
79395
+ */
79396
+ async function createTodoIssue(opts) {
79397
+ let body = "";
79398
+ if (opts.description) body += `${opts.description}\n`;
79399
+ if (opts.acceptanceCriteria && opts.acceptanceCriteria.length > 0) {
79400
+ body += `\n### Acceptance Criteria\n`;
79401
+ body += opts.acceptanceCriteria.map((c) => `- [ ] ${c}`).join("\n");
79402
+ body += "\n";
79403
+ }
79404
+ body += `\n---\n*Generated by MAXSIM*`;
79405
+ const args = [
79406
+ "issue",
79407
+ "create",
79408
+ "--title",
79409
+ opts.title,
79410
+ "--body",
79411
+ body
79412
+ ];
79413
+ args.push("--label", "maxsim");
79414
+ args.push("--label", "todo");
79415
+ if (opts.milestone) args.push("--milestone", opts.milestone);
79416
+ if (opts.projectTitle) args.push("--project", opts.projectTitle);
79417
+ const createResult = await ghExec(args);
79418
+ if (!createResult.ok) return {
79419
+ ok: false,
79420
+ error: createResult.error,
79421
+ code: createResult.code
79422
+ };
79423
+ const issueNumber = parseIssueNumberFromUrl(createResult.data);
79424
+ if (issueNumber === null) return {
79425
+ ok: false,
79426
+ error: `Failed to parse issue number from gh output: ${createResult.data}`,
79427
+ code: "UNKNOWN"
79428
+ };
79429
+ return fetchIssueDetails(issueNumber);
79430
+ }
79431
+ /**
79432
+ * Build a PR description body with `Closes #{N}` lines for auto-close on merge (AC-08).
79433
+ *
79434
+ * This function is called by `mcp_create_pr` in Plan 04's github-tools.ts.
79435
+ */
79436
+ function buildPrBody(closesIssues, additionalContent) {
79437
+ return `${closesIssues.map((n) => `Closes #${n}`).join("\n")}${additionalContent ? `\n\n${additionalContent}` : ""}`;
79438
+ }
79439
+ /**
79440
+ * Close an issue with an optional reason.
79441
+ *
79442
+ * Reason defaults to 'completed' if not specified.
79443
+ */
79444
+ async function closeIssue(issueNumber, reason) {
79445
+ const args = [
79446
+ "issue",
79447
+ "close",
79448
+ String(issueNumber)
79449
+ ];
79450
+ if (reason) args.push("--reason", reason);
79451
+ const result = await ghExec(args);
79452
+ if (!result.ok) return {
79453
+ ok: false,
79454
+ error: result.error,
79455
+ code: result.code
79456
+ };
79457
+ return {
79458
+ ok: true,
79459
+ data: void 0
79460
+ };
79461
+ }
79462
+ /**
79463
+ * Close an issue as superseded by a newer issue.
79464
+ *
79465
+ * 1. Posts "Superseded by #{newIssueNumber}" comment on old issue
79466
+ * 2. Adds 'superseded' label to old issue
79467
+ * 3. Closes old issue as completed
79468
+ * 4. Posts "Replaces #{oldIssueNumber}" comment on new issue
79469
+ *
79470
+ * Creates bidirectional cross-references.
79471
+ */
79472
+ async function closeIssueAsSuperseded(oldIssueNumber, newIssueNumber) {
79473
+ const commentResult = await postComment(oldIssueNumber, `Superseded by #${newIssueNumber}`);
79474
+ if (!commentResult.ok) return {
79475
+ ok: false,
79476
+ error: commentResult.error,
79477
+ code: commentResult.code
79478
+ };
79479
+ const labelResult = await ghExec([
79480
+ "issue",
79481
+ "edit",
79482
+ String(oldIssueNumber),
79483
+ "--add-label",
79484
+ "superseded"
79485
+ ]);
79486
+ if (!labelResult.ok) return {
79487
+ ok: false,
79488
+ error: labelResult.error,
79489
+ code: labelResult.code
79490
+ };
79491
+ const closeResult = await closeIssue(oldIssueNumber, "completed");
79492
+ if (!closeResult.ok) return closeResult;
79493
+ const replaceCommentResult = await postComment(newIssueNumber, `Replaces #${oldIssueNumber}`);
79494
+ if (!replaceCommentResult.ok) return {
79495
+ ok: false,
79496
+ error: replaceCommentResult.error,
79497
+ code: replaceCommentResult.code
79498
+ };
79499
+ return {
79500
+ ok: true,
79501
+ data: void 0
79502
+ };
79503
+ }
79504
+ /**
79505
+ * Post a comment on an issue.
79506
+ */
79507
+ async function postComment(issueNumber, body) {
79508
+ const result = await ghExec([
79509
+ "issue",
79510
+ "comment",
79511
+ String(issueNumber),
79512
+ "--body",
79513
+ body
79514
+ ]);
79515
+ if (!result.ok) return {
79516
+ ok: false,
79517
+ error: result.error,
79518
+ code: result.code
79519
+ };
79520
+ return {
79521
+ ok: true,
79522
+ data: void 0
79523
+ };
79524
+ }
79525
+ /**
79526
+ * Import an existing external GitHub issue into MAXSIM tracking.
79527
+ *
79528
+ * Reads the issue details and adds 'maxsim' and 'imported' labels.
79529
+ * Returns the issue details for the AI to decide placement.
79530
+ */
79531
+ async function importExternalIssue(issueNumber) {
79532
+ const viewResult = await ghExec([
79533
+ "issue",
79534
+ "view",
79535
+ String(issueNumber),
79536
+ "--json",
79537
+ "title,labels,body,state"
79538
+ ], { parseJson: true });
79539
+ if (!viewResult.ok) return {
79540
+ ok: false,
79541
+ error: viewResult.error,
79542
+ code: viewResult.code
79543
+ };
79544
+ const labelResult = await ghExec([
79545
+ "issue",
79546
+ "edit",
79547
+ String(issueNumber),
79548
+ "--add-label",
79549
+ "maxsim,imported"
79550
+ ]);
79551
+ if (!labelResult.ok) return {
79552
+ ok: false,
79553
+ error: labelResult.error,
79554
+ code: labelResult.code
79555
+ };
79556
+ const existingLabels = viewResult.data.labels.map((l) => l.name);
79557
+ const allLabels = Array.from(new Set([
79558
+ ...existingLabels,
79559
+ "maxsim",
79560
+ "imported"
79561
+ ]));
79562
+ return {
79563
+ ok: true,
79564
+ data: {
79565
+ number: issueNumber,
79566
+ title: viewResult.data.title,
79567
+ labels: allLabels
79568
+ }
79569
+ };
79570
+ }
79571
+ /**
79572
+ * Update the parent tracking issue's task list checkbox.
79573
+ *
79574
+ * Reads the parent issue body, finds `- [ ] #{childNumber}` or `- [x] #{childNumber}`,
79575
+ * toggles the checkbox, and updates the issue body via `gh issue edit`.
79576
+ */
79577
+ async function updateParentTaskList(parentIssueNumber, childIssueNumber, checked) {
79578
+ const viewResult = await ghExec([
79579
+ "issue",
79580
+ "view",
79581
+ String(parentIssueNumber),
79582
+ "--json",
79583
+ "body"
79584
+ ], { parseJson: true });
79585
+ if (!viewResult.ok) return {
79586
+ ok: false,
79587
+ error: viewResult.error,
79588
+ code: viewResult.code
79589
+ };
79590
+ const currentBody = viewResult.data.body;
79591
+ const checkboxPattern = new RegExp(`- \\[([ x])\\] #${childIssueNumber}\\b`, "g");
79592
+ const newCheckState = checked ? "x" : " ";
79593
+ const updatedBody = currentBody.replace(checkboxPattern, `- [${newCheckState}] #${childIssueNumber}`);
79594
+ if (updatedBody === currentBody) return {
79595
+ ok: true,
79596
+ data: void 0
79597
+ };
79598
+ const editResult = await ghExec([
79599
+ "issue",
79600
+ "edit",
79601
+ String(parentIssueNumber),
79602
+ "--body",
79603
+ updatedBody
79604
+ ]);
79605
+ if (!editResult.ok) return {
79606
+ ok: false,
79607
+ error: editResult.error,
79608
+ code: editResult.code
79609
+ };
79610
+ return {
79611
+ ok: true,
79612
+ data: void 0
79613
+ };
79614
+ }
79615
+ /**
79616
+ * Create all issues for a plan at once (eager creation on plan finalization).
79617
+ *
79618
+ * 1. Creates all task issues with concurrency limit of 5 (rate limit safety).
79619
+ * 2. After all task issues created, creates parent tracking issue.
79620
+ * 3. Updates mapping file for all created issues.
79621
+ * 4. Returns parent issue number and all task issue numbers.
79622
+ *
79623
+ * Handles partial failures: continues batch, reports which failed.
79624
+ */
79625
+ async function createAllPlanIssues(opts) {
79626
+ const BATCH_SIZE = 5;
79627
+ const results = [];
79628
+ const failures = [];
79629
+ for (let i = 0; i < opts.tasks.length; i += BATCH_SIZE) {
79630
+ const batchPromises = opts.tasks.slice(i, i + BATCH_SIZE).map(async (task) => {
79631
+ let depIssueNumbers;
79632
+ if (task.dependencies && task.dependencies.length > 0) depIssueNumbers = task.dependencies.map((depId) => {
79633
+ const found = results.find((r) => r.taskId === depId);
79634
+ return found ? found.issueNumber : 0;
79635
+ }).filter((n) => n > 0);
79636
+ const result = await createTaskIssue({
79637
+ title: task.title,
79638
+ phaseNum: opts.phaseNum,
79639
+ planNum: opts.planNum,
79640
+ taskId: task.taskId,
79641
+ summary: task.summary,
79642
+ actions: task.actions,
79643
+ acceptanceCriteria: task.acceptanceCriteria,
79644
+ dependencies: depIssueNumbers,
79645
+ milestone: opts.milestone,
79646
+ projectTitle: opts.projectTitle,
79647
+ estimate: task.estimate
79648
+ });
79649
+ return {
79650
+ taskId: task.taskId,
79651
+ result
79652
+ };
79653
+ });
79654
+ const batchResults = await Promise.all(batchPromises);
79655
+ for (const { taskId, result } of batchResults) if (result.ok) results.push({
79656
+ taskId,
79657
+ issueNumber: result.data.number,
79658
+ nodeId: result.data.node_id
79659
+ });
79660
+ else failures.push({
79661
+ taskId,
79662
+ error: result.error
79663
+ });
79664
+ }
79665
+ if (results.length === 0) return {
79666
+ ok: false,
79667
+ error: `All task issue creations failed: ${failures.map((f) => `${f.taskId}: ${f.error}`).join("; ")}`,
79668
+ code: "UNKNOWN"
79669
+ };
79670
+ const childNumbers = results.map((r) => r.issueNumber);
79671
+ const parentResult = await createParentTrackingIssue({
79672
+ phaseNum: opts.phaseNum,
79673
+ phaseName: opts.phaseName,
79674
+ childIssueNumbers: childNumbers,
79675
+ milestone: opts.milestone,
79676
+ projectTitle: opts.projectTitle
79677
+ });
79678
+ if (!parentResult.ok) return {
79679
+ ok: false,
79680
+ error: `Task issues created but parent tracking issue failed: ${parentResult.error}`,
79681
+ code: parentResult.code
79682
+ };
79683
+ const mapping = loadMapping(opts.cwd);
79684
+ if (mapping) {
79685
+ if (!mapping.phases[opts.phaseNum]) mapping.phases[opts.phaseNum] = {
79686
+ tracking_issue: {
79687
+ number: 0,
79688
+ node_id: "",
79689
+ item_id: "",
79690
+ status: "To Do"
79691
+ },
79692
+ plan: "",
79693
+ tasks: {}
79694
+ };
79695
+ mapping.phases[opts.phaseNum].tracking_issue = {
79696
+ number: parentResult.data.number,
79697
+ node_id: parentResult.data.node_id,
79698
+ item_id: "",
79699
+ status: "To Do"
79700
+ };
79701
+ mapping.phases[opts.phaseNum].plan = `${opts.phaseNum}-${opts.planNum}`;
79702
+ for (const r of results) mapping.phases[opts.phaseNum].tasks[r.taskId] = {
79703
+ number: r.issueNumber,
79704
+ node_id: r.nodeId,
79705
+ item_id: "",
79706
+ status: "To Do"
79707
+ };
79708
+ saveMapping(opts.cwd, mapping);
79709
+ }
79710
+ const taskIssues = results.map((r) => ({
79711
+ taskId: r.taskId,
79712
+ issueNumber: r.issueNumber
79713
+ }));
79714
+ return {
79715
+ ok: true,
79716
+ data: {
79717
+ parentIssue: parentResult.data.number,
79718
+ taskIssues
79719
+ }
79720
+ };
79721
+ }
79722
+ /**
79723
+ * Supersede old plan's issues when a plan is re-planned (fresh issues per plan).
79724
+ *
79725
+ * 1. Load mapping to find old plan's issue numbers.
79726
+ * 2. For each old issue, close as superseded with cross-reference to new issue.
79727
+ * 3. Close old parent tracking issue as superseded.
79728
+ * 4. Update mapping: mark old entries, add new entries.
79729
+ */
79730
+ async function supersedePlanIssues(opts) {
79731
+ const mapping = loadMapping(opts.cwd);
79732
+ if (!mapping) return {
79733
+ ok: false,
79734
+ error: "github-issues.json does not exist. Run project setup first.",
79735
+ code: "NOT_FOUND"
79736
+ };
79737
+ const phase = mapping.phases[opts.phaseNum];
79738
+ if (!phase) return {
79739
+ ok: false,
79740
+ error: `No phase ${opts.phaseNum} found in mapping file`,
79741
+ code: "NOT_FOUND"
79742
+ };
79743
+ const currentPlan = phase.plan;
79744
+ const expectedOldPlan = `${opts.phaseNum}-${opts.oldPlanNum}`;
79745
+ if (currentPlan !== expectedOldPlan) return {
79746
+ ok: false,
79747
+ error: `Phase ${opts.phaseNum} is on plan '${currentPlan}', expected '${expectedOldPlan}'`,
79748
+ code: "UNKNOWN"
79749
+ };
79750
+ const failures = [];
79751
+ const oldTasks = Object.entries(phase.tasks);
79752
+ for (const [taskId, oldTask] of oldTasks) {
79753
+ const newIssue = opts.newIssueNumbers.find((n) => n.taskId === taskId);
79754
+ if (!newIssue) {
79755
+ const closeResult = await closeIssue(oldTask.number, "completed");
79756
+ if (!closeResult.ok) failures.push(`close task ${taskId} (#${oldTask.number}): ${closeResult.error}`);
79757
+ continue;
79758
+ }
79759
+ const supersedeResult = await closeIssueAsSuperseded(oldTask.number, newIssue.issueNumber);
79760
+ if (!supersedeResult.ok) failures.push(`supersede task ${taskId} (#${oldTask.number} -> #${newIssue.issueNumber}): ${supersedeResult.error}`);
79761
+ }
79762
+ if (phase.tracking_issue.number > 0) {
79763
+ const closeResult = await closeIssue(phase.tracking_issue.number, "completed");
79764
+ if (!closeResult.ok) failures.push(`close parent tracking issue #${phase.tracking_issue.number}: ${closeResult.error}`);
79765
+ }
79766
+ phase.plan = `${opts.phaseNum}-${opts.newPlanNum}`;
79767
+ phase.tasks = {};
79768
+ for (const newIssue of opts.newIssueNumbers) phase.tasks[newIssue.taskId] = {
79769
+ number: newIssue.issueNumber,
79770
+ node_id: "",
79771
+ item_id: "",
79772
+ status: "To Do"
79773
+ };
79774
+ saveMapping(opts.cwd, mapping);
79775
+ if (failures.length > 0) return {
79776
+ ok: false,
79777
+ error: `Partial failure during supersession: ${failures.join("; ")}`,
79778
+ code: "UNKNOWN"
79779
+ };
79780
+ return {
79781
+ ok: true,
79782
+ data: void 0
79783
+ };
79784
+ }
79785
+
79786
+ //#endregion
79787
+ //#region src/github/projects.ts
79788
+ /**
79789
+ * Extract error info from a failed GhResult and re-wrap it for a different
79790
+ * generic type. This avoids TypeScript narrowing issues with discriminated
79791
+ * union property access.
79792
+ */
79793
+ function fail$2(result) {
79794
+ return {
79795
+ ok: false,
79796
+ error: result.error,
79797
+ code: result.code
79798
+ };
79799
+ }
79800
+ /**
79801
+ * Create a new GitHub Projects v2 board.
79802
+ *
79803
+ * Runs `gh project create --owner @me --title "{title}" --format json`.
79804
+ * Returns the project number and node ID.
79805
+ */
79806
+ async function createProjectBoard(title) {
79807
+ const result = await ghExec([
79808
+ "project",
79809
+ "create",
79810
+ "--owner",
79811
+ "@me",
79812
+ "--title",
79813
+ title,
79814
+ "--format",
79815
+ "json"
79816
+ ], { parseJson: true });
79817
+ if (!result.ok) return result;
79818
+ return {
79819
+ ok: true,
79820
+ data: {
79821
+ number: result.data.number,
79822
+ id: result.data.id
79823
+ }
79824
+ };
79825
+ }
79826
+ /**
79827
+ * Ensure a project board exists, creating it if needed.
79828
+ *
79829
+ * Checks the mapping file for an existing project. If found, verifies it
79830
+ * still exists via `gh project view`. If not found, creates a new board
79831
+ * and sets up fields and status options.
79832
+ *
79833
+ * Returns the project number, ID, and whether it was newly created.
79834
+ */
79835
+ async function ensureProjectBoard(title, cwd) {
79836
+ const mapping = loadMapping(cwd);
79837
+ if (mapping && mapping.project_number > 0 && mapping.project_id) {
79838
+ if ((await ghExec([
79839
+ "project",
79840
+ "view",
79841
+ String(mapping.project_number),
79842
+ "--owner",
79843
+ "@me",
79844
+ "--format",
79845
+ "json"
79846
+ ], { parseJson: true })).ok) return {
79847
+ ok: true,
79848
+ data: {
79849
+ number: mapping.project_number,
79850
+ id: mapping.project_id,
79851
+ created: false
79852
+ }
79853
+ };
79854
+ }
79855
+ const createResult = await createProjectBoard(title);
79856
+ if (!createResult.ok) return fail$2(createResult);
79857
+ const { number, id } = createResult.data;
79858
+ const setupResult = await setupProjectFields(number, id, cwd);
79859
+ if (!setupResult.ok) return fail$2(setupResult);
79860
+ return {
79861
+ ok: true,
79862
+ data: {
79863
+ number,
79864
+ id,
79865
+ created: true
79866
+ }
79867
+ };
79868
+ }
79869
+ /**
79870
+ * Get all fields for a project board.
79871
+ *
79872
+ * Runs `gh project field-list {num} --owner @me --format json`.
79873
+ * Returns field list with IDs, names, types, and options (for single-select).
79874
+ */
79875
+ async function getProjectFields(projectNum) {
79876
+ const result = await ghExec([
79877
+ "project",
79878
+ "field-list",
79879
+ String(projectNum),
79880
+ "--owner",
79881
+ "@me",
79882
+ "--format",
79883
+ "json"
79884
+ ], { parseJson: true });
79885
+ if (!result.ok) return fail$2(result);
79886
+ return {
79887
+ ok: true,
79888
+ data: result.data.fields ?? result.data
79889
+ };
79890
+ }
79891
+ /**
79892
+ * Add a new single-select option to a project field via GraphQL.
79893
+ *
79894
+ * The `updateProjectV2Field` mutation REPLACES all options, so all existing
79895
+ * options must be included alongside the new one.
79896
+ *
79897
+ * Returns the new option's ID.
79898
+ */
79899
+ async function addStatusOption(projectId, statusFieldId, optionName, existingOptions) {
79900
+ const result = await ghGraphQL(`
79901
+ mutation {
79902
+ updateProjectV2Field(input: {
79903
+ projectId: "${projectId}"
79904
+ fieldId: "${statusFieldId}"
79905
+ singleSelectOptions: [${[...existingOptions.map((o) => `{name: "${o.name}", description: "", color: GRAY}`), `{name: "${optionName}", description: "", color: BLUE}`].join(", ")}]
79906
+ }) {
79907
+ projectV2Field {
79908
+ ... on ProjectV2SingleSelectField {
79909
+ options { id name }
79910
+ }
79911
+ }
79912
+ }
79913
+ }
79914
+ `);
79915
+ if (!result.ok) return fail$2(result);
79916
+ const newOption = result.data.updateProjectV2Field.projectV2Field.options.find((o) => o.name === optionName);
79917
+ if (!newOption) return {
79918
+ ok: false,
79919
+ error: `Option "${optionName}" was not found after mutation — it may have been renamed or rejected`,
79920
+ code: "UNKNOWN"
79921
+ };
79922
+ return {
79923
+ ok: true,
79924
+ data: newOption.id
79925
+ };
79926
+ }
79927
+ /**
79928
+ * Set up project fields: Status options and Estimate number field.
79929
+ *
79930
+ * Orchestrates the full field setup:
79931
+ * (a) Get existing fields
79932
+ * (b) Find Status field, verify "In Review" option exists or add it
79933
+ * (c) Create Estimate NUMBER field via `gh project field-create`
79934
+ * (d) Store all field/option IDs in the mapping file
79935
+ */
79936
+ async function setupProjectFields(projectNum, projectId, cwd) {
79937
+ const fieldsResult = await getProjectFields(projectNum);
79938
+ if (!fieldsResult.ok) return fail$2(fieldsResult);
79939
+ const fields = fieldsResult.data;
79940
+ const statusField = fields.find((f) => f.name === "Status" && (f.type === "SINGLE_SELECT" || f.type === "ProjectV2SingleSelectField"));
79941
+ if (!statusField) return {
79942
+ ok: false,
79943
+ error: "Status field not found on project board. This is unexpected for a Projects v2 board.",
79944
+ code: "NOT_FOUND"
79945
+ };
79946
+ const statusOptions = statusField.options ?? [];
79947
+ const statusOptionsMap = {};
79948
+ for (const opt of statusOptions) statusOptionsMap[opt.name] = opt.id;
79949
+ if (!statusOptionsMap["In Review"]) {
79950
+ const addResult = await addStatusOption(projectId, statusField.id, "In Review", statusOptions);
79951
+ if (!addResult.ok) return fail$2(addResult);
79952
+ statusOptionsMap["In Review"] = addResult.data;
79953
+ }
79954
+ if (statusOptionsMap["Todo"] && !statusOptionsMap["To Do"]) statusOptionsMap["To Do"] = statusOptionsMap["Todo"];
79955
+ const estimateField = fields.find((f) => f.name === "Estimate");
79956
+ let estimateFieldId = estimateField?.id ?? "";
79957
+ if (!estimateField) {
79958
+ const createFieldResult = await ghExec([
79959
+ "project",
79960
+ "field-create",
79961
+ String(projectNum),
79962
+ "--owner",
79963
+ "@me",
79964
+ "--name",
79965
+ "Estimate",
79966
+ "--data-type",
79967
+ "NUMBER"
79968
+ ]);
79969
+ if (!createFieldResult.ok) return fail$2(createFieldResult);
79970
+ const refetch = await getProjectFields(projectNum);
79971
+ if (refetch.ok) {
79972
+ const est = refetch.data.find((f) => f.name === "Estimate");
79973
+ if (est) estimateFieldId = est.id;
79974
+ }
79975
+ }
79976
+ const repoResult = await ghExec([
79977
+ "repo",
79978
+ "view",
79979
+ "--json",
79980
+ "nameWithOwner",
79981
+ "--jq",
79982
+ ".nameWithOwner"
79983
+ ]);
79984
+ const repo = repoResult.ok ? repoResult.data.trim() : "";
79985
+ const mapping = loadMapping(cwd) ?? createEmptyMapping(repo);
79986
+ mapping.project_number = projectNum;
79987
+ mapping.project_id = projectId;
79988
+ mapping.status_field_id = statusField.id;
79989
+ mapping.status_options = statusOptionsMap;
79990
+ mapping.estimate_field_id = estimateFieldId;
79991
+ if (repo && !mapping.repo) mapping.repo = repo;
79992
+ saveMapping(cwd, mapping);
79993
+ return {
79994
+ ok: true,
79995
+ data: void 0
79996
+ };
79997
+ }
79998
+ /**
79999
+ * Add an issue to a project board.
80000
+ *
80001
+ * Runs `gh project item-add {num} --owner @me --url {issueUrl} --format json`.
80002
+ * Returns the project item ID (different from the issue ID).
80003
+ */
80004
+ async function addItemToProject(projectNum, issueUrl) {
80005
+ const result = await ghExec([
80006
+ "project",
80007
+ "item-add",
80008
+ String(projectNum),
80009
+ "--owner",
80010
+ "@me",
80011
+ "--url",
80012
+ issueUrl,
80013
+ "--format",
80014
+ "json"
80015
+ ], { parseJson: true });
80016
+ if (!result.ok) return fail$2(result);
80017
+ return {
80018
+ ok: true,
80019
+ data: { item_id: result.data.id }
80020
+ };
80021
+ }
80022
+ /**
80023
+ * Move a project item to a specific status column.
80024
+ *
80025
+ * Runs `gh project item-edit` with single-select-option-id for the
80026
+ * Status field.
80027
+ */
80028
+ async function moveItemToStatus(projectId, itemId, statusFieldId, statusOptionId) {
80029
+ const result = await ghExec([
80030
+ "project",
80031
+ "item-edit",
80032
+ "--project-id",
80033
+ projectId,
80034
+ "--id",
80035
+ itemId,
80036
+ "--field-id",
80037
+ statusFieldId,
80038
+ "--single-select-option-id",
80039
+ statusOptionId
80040
+ ]);
80041
+ if (!result.ok) return fail$2(result);
80042
+ return {
80043
+ ok: true,
80044
+ data: void 0
80045
+ };
80046
+ }
80047
+ /**
80048
+ * Set the Estimate (story points) field on a project item.
80049
+ *
80050
+ * Runs `gh project item-edit` with --number flag for the Estimate field.
80051
+ */
80052
+ async function setEstimate(projectId, itemId, estimateFieldId, points) {
80053
+ const result = await ghExec([
80054
+ "project",
80055
+ "item-edit",
80056
+ "--project-id",
80057
+ projectId,
80058
+ "--id",
80059
+ itemId,
80060
+ "--field-id",
80061
+ estimateFieldId,
80062
+ "--number",
80063
+ String(points)
80064
+ ]);
80065
+ if (!result.ok) return fail$2(result);
80066
+ return {
80067
+ ok: true,
80068
+ data: void 0
80069
+ };
80070
+ }
80071
+
80072
+ //#endregion
80073
+ //#region src/github/milestones.ts
80074
+ /**
80075
+ * Re-wrap a failed GhResult for a different generic type.
80076
+ */
80077
+ function fail$1(result) {
80078
+ return {
80079
+ ok: false,
80080
+ error: result.error,
80081
+ code: result.code
80082
+ };
80083
+ }
80084
+ /**
80085
+ * Create a new GitHub milestone.
80086
+ *
80087
+ * Uses the REST API: `POST /repos/{owner}/{repo}/milestones`.
80088
+ * The `{owner}` and `{repo}` placeholders are auto-resolved by `gh api`.
80089
+ *
80090
+ * Returns the milestone number and internal ID.
80091
+ */
80092
+ async function createMilestone(title, description) {
80093
+ const args = [
80094
+ "api",
80095
+ "repos/{owner}/{repo}/milestones",
80096
+ "-X",
80097
+ "POST",
80098
+ "-f",
80099
+ `title=${title}`,
80100
+ "-f",
80101
+ "state=open"
80102
+ ];
80103
+ if (description) args.push("-f", `description=${description}`);
80104
+ const result = await ghExec(args, { parseJson: true });
80105
+ if (!result.ok) return result;
80106
+ return {
80107
+ ok: true,
80108
+ data: {
80109
+ number: result.data.number,
80110
+ id: result.data.id
80111
+ }
80112
+ };
80113
+ }
80114
+ /**
80115
+ * Find an existing milestone by title.
80116
+ *
80117
+ * Uses the REST API: `GET /repos/{owner}/{repo}/milestones`.
80118
+ * Fetches all open milestones and filters by exact title match.
80119
+ *
80120
+ * Returns null if no milestone with the given title exists.
80121
+ */
80122
+ async function findMilestone(title) {
80123
+ const result = await ghExec([
80124
+ "api",
80125
+ "repos/{owner}/{repo}/milestones",
80126
+ "--paginate"
80127
+ ], { parseJson: true });
80128
+ if (!result.ok) return fail$1(result);
80129
+ const match = result.data.find((m) => m.title === title);
80130
+ if (!match) return {
80131
+ ok: true,
80132
+ data: null
80133
+ };
80134
+ return {
80135
+ ok: true,
80136
+ data: {
80137
+ number: match.number,
80138
+ id: match.id
80139
+ }
80140
+ };
80141
+ }
80142
+ /**
80143
+ * Ensure a milestone exists, creating it if needed. Idempotent.
80144
+ *
80145
+ * First attempts to find an existing milestone with the given title.
80146
+ * If not found, creates a new one. Returns whether it was newly created.
80147
+ */
80148
+ async function ensureMilestone(title, description) {
80149
+ const findResult = await findMilestone(title);
80150
+ if (!findResult.ok) return fail$1(findResult);
80151
+ if (findResult.data) return {
80152
+ ok: true,
80153
+ data: {
80154
+ ...findResult.data,
80155
+ created: false
80156
+ }
80157
+ };
80158
+ const createResult = await createMilestone(title, description);
80159
+ if (!createResult.ok) return fail$1(createResult);
80160
+ return {
80161
+ ok: true,
80162
+ data: {
80163
+ ...createResult.data,
80164
+ created: true
80165
+ }
80166
+ };
80167
+ }
80168
+ /**
80169
+ * Close a milestone if all its issues are closed.
80170
+ *
80171
+ * Fetches milestone details via REST API to check `open_issues` count.
80172
+ * If open_issues === 0, patches the milestone state to "closed".
80173
+ *
80174
+ * This implements AC-12: milestones auto-close when all issues are closed.
80175
+ */
80176
+ async function closeMilestoneIfComplete(milestoneNumber) {
80177
+ const detailResult = await ghExec(["api", `repos/{owner}/{repo}/milestones/${milestoneNumber}`], { parseJson: true });
80178
+ if (!detailResult.ok) return fail$1(detailResult);
80179
+ const milestone = detailResult.data;
80180
+ if (milestone.state === "closed") return {
80181
+ ok: true,
80182
+ data: { closed: true }
80183
+ };
80184
+ if (milestone.open_issues > 0) return {
80185
+ ok: true,
80186
+ data: { closed: false }
80187
+ };
80188
+ const closeResult = await ghExec([
80189
+ "api",
80190
+ `repos/{owner}/{repo}/milestones/${milestoneNumber}`,
80191
+ "-X",
80192
+ "PATCH",
80193
+ "-f",
80194
+ "state=closed"
80195
+ ]);
80196
+ if (!closeResult.ok) return fail$1(closeResult);
80197
+ return {
80198
+ ok: true,
80199
+ data: { closed: true }
80200
+ };
80201
+ }
80202
+
80203
+ //#endregion
80204
+ //#region src/github/sync.ts
80205
+ /**
80206
+ * Re-wrap a failed GhResult for a different generic type.
80207
+ */
80208
+ function fail(result) {
80209
+ return {
80210
+ ok: false,
80211
+ error: result.error,
80212
+ code: result.code
80213
+ };
80214
+ }
80215
+ /**
80216
+ * Batch-fetch issue details via a single GraphQL query.
80217
+ *
80218
+ * Fetches up to 100 issues per query using node ID lookups.
80219
+ * Falls back to sequential `gh issue view` if GraphQL fails.
80220
+ */
80221
+ async function batchFetchIssues(repo, issueNumbers) {
80222
+ if (issueNumbers.length === 0) return {
80223
+ ok: true,
80224
+ data: /* @__PURE__ */ new Map()
80225
+ };
80226
+ const [owner, name] = repo.split("/");
80227
+ if (!owner || !name) return {
80228
+ ok: false,
80229
+ error: `Invalid repo format: ${repo}. Expected "owner/repo".`,
80230
+ code: "UNKNOWN"
80231
+ };
80232
+ const BATCH_SIZE = 100;
80233
+ const resultMap = /* @__PURE__ */ new Map();
80234
+ for (let i = 0; i < issueNumbers.length; i += BATCH_SIZE) {
80235
+ const batch = issueNumbers.slice(i, i + BATCH_SIZE);
80236
+ const result = await ghGraphQL(`
80237
+ query {
80238
+ repository(owner: "${owner}", name: "${name}") {
80239
+ ${batch.map((num, idx) => `issue_${idx}: issue(number: ${num}) { number state title labels(first: 20) { nodes { name } } }`).join("\n ")}
80240
+ }
80241
+ }
80242
+ `);
80243
+ if (!result.ok) return batchFetchIssuesSequential(issueNumbers);
80244
+ const repoData = result.data.repository;
80245
+ for (let idx = 0; idx < batch.length; idx++) {
80246
+ const issueData = repoData[`issue_${idx}`];
80247
+ if (issueData) resultMap.set(issueData.number, {
80248
+ state: issueData.state.toLowerCase(),
80249
+ title: issueData.title,
80250
+ labels: issueData.labels.nodes.map((l) => l.name)
80251
+ });
80252
+ }
80253
+ }
80254
+ return {
80255
+ ok: true,
80256
+ data: resultMap
80257
+ };
80258
+ }
80259
+ /**
80260
+ * Sequential fallback: fetch issues one at a time via `gh issue view`.
80261
+ */
80262
+ async function batchFetchIssuesSequential(issueNumbers) {
80263
+ const resultMap = /* @__PURE__ */ new Map();
80264
+ for (const num of issueNumbers) {
80265
+ const result = await ghExec([
80266
+ "issue",
80267
+ "view",
80268
+ String(num),
80269
+ "--json",
80270
+ "state,title,labels"
80271
+ ], { parseJson: true });
80272
+ if (result.ok) resultMap.set(num, {
80273
+ state: result.data.state.toLowerCase(),
80274
+ title: result.data.title,
80275
+ labels: result.data.labels.map((l) => l.name)
80276
+ });
80277
+ }
80278
+ return {
80279
+ ok: true,
80280
+ data: resultMap
80281
+ };
80282
+ }
80283
+ /**
80284
+ * Compare local mapping file against GitHub reality.
80285
+ *
80286
+ * For each tracked issue (phases + todos), fetches current GitHub state
80287
+ * and compares against the local mapping. Reports discrepancies in
80288
+ * state, title, and labels.
80289
+ *
80290
+ * Uses batched GraphQL for efficiency (single query for up to 100 issues).
80291
+ */
80292
+ async function syncCheck(cwd) {
80293
+ const mapping = loadMapping(cwd);
80294
+ if (!mapping) return {
80295
+ ok: false,
80296
+ error: "github-issues.json does not exist. Run project setup first.",
80297
+ code: "NOT_FOUND"
80298
+ };
80299
+ if (!mapping.repo) return {
80300
+ ok: false,
80301
+ error: "No repo configured in github-issues.json.",
80302
+ code: "NOT_FOUND"
80303
+ };
80304
+ const trackedIssues = [];
80305
+ for (const [phaseNum, phase] of Object.entries(mapping.phases)) {
80306
+ if (phase.tracking_issue.number > 0) trackedIssues.push({
80307
+ issueNumber: phase.tracking_issue.number,
80308
+ localStatus: phase.tracking_issue.status,
80309
+ source: `phase ${phaseNum} tracking`
80310
+ });
80311
+ for (const [taskId, task] of Object.entries(phase.tasks)) if (task.number > 0) trackedIssues.push({
80312
+ issueNumber: task.number,
80313
+ localStatus: task.status,
80314
+ source: `phase ${phaseNum}, task ${taskId}`
80315
+ });
80316
+ }
80317
+ if (mapping.todos) {
80318
+ for (const [todoId, todo] of Object.entries(mapping.todos)) if (todo.number > 0) trackedIssues.push({
80319
+ issueNumber: todo.number,
80320
+ localStatus: todo.status,
80321
+ source: `todo ${todoId}`
80322
+ });
80323
+ }
80324
+ if (trackedIssues.length === 0) return {
80325
+ ok: true,
80326
+ data: {
80327
+ inSync: true,
80328
+ changes: []
80329
+ }
80330
+ };
80331
+ const issueNumbers = trackedIssues.map((t) => t.issueNumber);
80332
+ const fetchResult = await batchFetchIssues(mapping.repo, issueNumbers);
80333
+ if (!fetchResult.ok) return fail(fetchResult);
80334
+ const remoteStates = fetchResult.data;
80335
+ const changes = [];
80336
+ for (const tracked of trackedIssues) {
80337
+ const remote = remoteStates.get(tracked.issueNumber);
80338
+ if (!remote) {
80339
+ changes.push({
80340
+ issueNumber: tracked.issueNumber,
80341
+ field: "existence",
80342
+ localValue: "exists",
80343
+ remoteValue: "not found"
80344
+ });
80345
+ continue;
80346
+ }
80347
+ const isRemoteClosed = remote.state === "closed";
80348
+ const isLocalDone = tracked.localStatus === "Done";
80349
+ if (isRemoteClosed && !isLocalDone) changes.push({
80350
+ issueNumber: tracked.issueNumber,
80351
+ field: "state",
80352
+ localValue: tracked.localStatus,
80353
+ remoteValue: "closed (Done)"
80354
+ });
80355
+ else if (!isRemoteClosed && isLocalDone) changes.push({
80356
+ issueNumber: tracked.issueNumber,
80357
+ field: "state",
80358
+ localValue: "Done",
80359
+ remoteValue: `open (${remote.state})`
80360
+ });
80361
+ }
80362
+ return {
80363
+ ok: true,
80364
+ data: {
80365
+ inSync: changes.length === 0,
80366
+ changes
80367
+ }
80368
+ };
80369
+ }
80370
+
78914
80371
  //#endregion
78915
80372
  //#region src/mcp/utils.ts
78916
80373
  /**
@@ -78980,6 +80437,10 @@ function mcpError(error, summary) {
78980
80437
  /**
78981
80438
  * Phase CRUD MCP Tools — Phase operations exposed as MCP tools
78982
80439
  *
80440
+ * Integrates with GitHub: phase completion triggers sync check, issue close,
80441
+ * board move to Done, and milestone completion check. Find/list enrich
80442
+ * responses with GitHub issue data when available.
80443
+ *
78983
80444
  * CRITICAL: Never import output() or error() from core — they call process.exit().
78984
80445
  * CRITICAL: Never write to stdout — it is reserved for MCP JSON-RPC protocol.
78985
80446
  * CRITICAL: Never call process.exit() — the server must stay alive after every tool call.
@@ -78994,6 +80455,31 @@ function registerPhaseTools(server) {
78994
80455
  if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
78995
80456
  const result = findPhaseInternal(cwd, phase);
78996
80457
  if (!result) return mcpError(`Phase ${phase} not found`, "Phase not found");
80458
+ let githubTracking = null;
80459
+ let githubTaskIssues = null;
80460
+ let githubWarning;
80461
+ try {
80462
+ const mapping = loadMapping(cwd);
80463
+ if (mapping && result.phase_number) {
80464
+ const phaseMapping = mapping.phases[result.phase_number];
80465
+ if (phaseMapping) {
80466
+ if (phaseMapping.tracking_issue.number > 0) githubTracking = {
80467
+ number: phaseMapping.tracking_issue.number,
80468
+ status: phaseMapping.tracking_issue.status
80469
+ };
80470
+ const taskEntries = Object.entries(phaseMapping.tasks);
80471
+ if (taskEntries.length > 0) {
80472
+ githubTaskIssues = {};
80473
+ for (const [taskId, task] of taskEntries) if (task.number > 0) githubTaskIssues[taskId] = {
80474
+ number: task.number,
80475
+ status: task.status
80476
+ };
80477
+ }
80478
+ }
80479
+ }
80480
+ } catch (e) {
80481
+ githubWarning = `GitHub data enrichment failed: ${e.message}`;
80482
+ }
78997
80483
  return mcpSuccess({
78998
80484
  found: result.found,
78999
80485
  directory: result.directory,
@@ -79006,7 +80492,10 @@ function registerPhaseTools(server) {
79006
80492
  has_research: result.has_research,
79007
80493
  has_context: result.has_context,
79008
80494
  has_verification: result.has_verification,
79009
- archived: result.archived ?? null
80495
+ archived: result.archived ?? null,
80496
+ github_tracking_issue: githubTracking,
80497
+ github_task_issues: githubTaskIssues,
80498
+ ...githubWarning ? { github_warning: githubWarning } : {}
79010
80499
  }, `Found phase ${result.phase_number}: ${result.phase_name ?? "unnamed"}`);
79011
80500
  } catch (e) {
79012
80501
  return mcpError(e.message, "Operation failed");
@@ -79038,13 +80527,37 @@ function registerPhaseTools(server) {
79038
80527
  const total_count = dirs.length;
79039
80528
  const paginated = dirs.slice(offset, offset + limit);
79040
80529
  const has_more = offset + limit < total_count;
80530
+ let githubIssueCounts = null;
80531
+ let githubWarning;
80532
+ try {
80533
+ if (await detectGitHubMode() === "full") {
80534
+ const mapping = loadMapping(cwd);
80535
+ if (mapping && Object.keys(mapping.phases).length > 0) {
80536
+ githubIssueCounts = {};
80537
+ for (const [phaseNum, phaseData] of Object.entries(mapping.phases)) {
80538
+ let open = 0;
80539
+ let closed = 0;
80540
+ for (const task of Object.values(phaseData.tasks)) if (task.number > 0) if (task.status === "Done") closed++;
80541
+ else open++;
80542
+ githubIssueCounts[phaseNum] = {
80543
+ open,
80544
+ closed
80545
+ };
80546
+ }
80547
+ }
80548
+ }
80549
+ } catch (e) {
80550
+ githubWarning = `GitHub enrichment failed: ${e.message}`;
80551
+ }
79041
80552
  return mcpSuccess({
79042
80553
  directories: paginated,
79043
80554
  count: paginated.length,
79044
80555
  total_count,
79045
80556
  offset,
79046
80557
  limit,
79047
- has_more
80558
+ has_more,
80559
+ github_issue_counts: githubIssueCounts,
80560
+ ...githubWarning ? { github_warning: githubWarning } : {}
79048
80561
  }, `Showing ${paginated.length} of ${total_count} phase(s)`);
79049
80562
  } catch (e) {
79050
80563
  return mcpError(e.message, "Operation failed");
@@ -79091,7 +80604,50 @@ function registerPhaseTools(server) {
79091
80604
  try {
79092
80605
  const cwd = detectProjectRoot();
79093
80606
  if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
80607
+ let syncDiscrepancies = [];
80608
+ let githubWarning;
80609
+ try {
80610
+ if (await detectGitHubMode() === "full") {
80611
+ const syncResult = await syncCheck(cwd);
80612
+ if (syncResult.ok && !syncResult.data.inSync) syncDiscrepancies = syncResult.data.changes;
80613
+ }
80614
+ } catch (e) {
80615
+ githubWarning = `Sync check failed: ${e.message}`;
80616
+ }
79094
80617
  const result = await phaseCompleteCore(cwd, phase);
80618
+ let githubClosed = false;
80619
+ let milestoneClosed = false;
80620
+ try {
80621
+ if (await detectGitHubMode() === "full") {
80622
+ const mapping = loadMapping(cwd);
80623
+ if (mapping) {
80624
+ const phaseMapping = mapping.phases[phase];
80625
+ if (phaseMapping) {
80626
+ if (phaseMapping.tracking_issue.number > 0) {
80627
+ if ((await closeIssue(phaseMapping.tracking_issue.number, "completed")).ok) {
80628
+ githubClosed = true;
80629
+ phaseMapping.tracking_issue.status = "Done";
80630
+ if (phaseMapping.tracking_issue.item_id && mapping.status_field_id && mapping.status_options["Done"]) await moveItemToStatus(mapping.project_id, phaseMapping.tracking_issue.item_id, mapping.status_field_id, mapping.status_options["Done"]);
80631
+ }
80632
+ }
80633
+ for (const [_taskId, task] of Object.entries(phaseMapping.tasks)) if (task.number > 0 && task.status !== "Done") {
80634
+ if ((await closeIssue(task.number, "completed")).ok) {
80635
+ task.status = "Done";
80636
+ if (task.item_id && mapping.status_field_id && mapping.status_options["Done"]) await moveItemToStatus(mapping.project_id, task.item_id, mapping.status_field_id, mapping.status_options["Done"]);
80637
+ if (phaseMapping.tracking_issue.number > 0) await updateParentTaskList(phaseMapping.tracking_issue.number, task.number, true);
80638
+ }
80639
+ }
80640
+ saveMapping(cwd, mapping);
80641
+ if (mapping.milestone_id > 0) {
80642
+ const msResult = await closeMilestoneIfComplete(mapping.milestone_id);
80643
+ if (msResult.ok) milestoneClosed = msResult.data.closed;
80644
+ }
80645
+ }
80646
+ }
80647
+ }
80648
+ } catch (e) {
80649
+ githubWarning = (githubWarning ? githubWarning + "; " : "") + `GitHub completion operations failed: ${e.message}`;
80650
+ }
79095
80651
  return mcpSuccess({
79096
80652
  completed_phase: result.completed_phase,
79097
80653
  phase_name: result.phase_name,
@@ -79101,12 +80657,118 @@ function registerPhaseTools(server) {
79101
80657
  is_last_phase: result.is_last_phase,
79102
80658
  date: result.date,
79103
80659
  roadmap_updated: result.roadmap_updated,
79104
- state_updated: result.state_updated
80660
+ state_updated: result.state_updated,
80661
+ sync_discrepancies: syncDiscrepancies.length > 0 ? syncDiscrepancies : null,
80662
+ github_closed: githubClosed,
80663
+ milestone_closed: milestoneClosed,
80664
+ ...githubWarning ? { github_warning: githubWarning } : {}
79105
80665
  }, `Phase ${phase} marked as complete${result.next_phase ? `, next: Phase ${result.next_phase}` : ""}`);
79106
80666
  } catch (e) {
79107
80667
  return mcpError(e.message, "Operation failed");
79108
80668
  }
79109
80669
  });
80670
+ server.tool("mcp_bounce_issue", "Bounce a task back from In Review to In Progress with a detailed comment explaining what failed. Implements reviewer feedback loop (AC-05).", {
80671
+ issue_number: numberType().describe("GitHub issue number to bounce back"),
80672
+ reason: stringType().describe("Detailed reason why the task is being bounced back (reviewer feedback)")
80673
+ }, async ({ issue_number, reason }) => {
80674
+ try {
80675
+ const cwd = detectProjectRoot();
80676
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
80677
+ if (await detectGitHubMode() === "local-only") {
80678
+ const mapping = loadMapping(cwd);
80679
+ if (mapping) {
80680
+ if (updateLocalMappingStatus$1(mapping, issue_number, "In Progress")) {
80681
+ saveMapping(cwd, mapping);
80682
+ return mcpSuccess({
80683
+ mode: "local-only",
80684
+ issue_number,
80685
+ status: "In Progress",
80686
+ local_updated: true,
80687
+ reason
80688
+ }, `Local-only: issue #${issue_number} bounced to In Progress (reason recorded locally)`);
80689
+ }
80690
+ }
80691
+ return mcpSuccess({
80692
+ mode: "local-only",
80693
+ issue_number,
80694
+ reason,
80695
+ note: "Bounce recorded locally. GitHub operations skipped."
80696
+ }, `Local-only: bounce for issue #${issue_number} recorded`);
80697
+ }
80698
+ const mapping = loadMapping(cwd);
80699
+ let githubWarning;
80700
+ let moved = false;
80701
+ let commented = false;
80702
+ try {
80703
+ const commentResult = await postComment(issue_number, `## Bounced Back to In Progress\n\n**Reason:** ${reason}\n\n---\n*Review feedback posted by MAXSIM*`);
80704
+ commented = commentResult.ok;
80705
+ if (!commentResult.ok) githubWarning = `Comment failed: ${commentResult.error}`;
80706
+ } catch (e) {
80707
+ githubWarning = `Comment failed: ${e.message}`;
80708
+ }
80709
+ try {
80710
+ if (mapping) {
80711
+ const issueEntry = findIssueInMapping$2(mapping, issue_number);
80712
+ if (issueEntry?.item_id && mapping.status_field_id && mapping.status_options["In Progress"]) {
80713
+ const moveResult = await moveItemToStatus(mapping.project_id, issueEntry.item_id, mapping.status_field_id, mapping.status_options["In Progress"]);
80714
+ moved = moveResult.ok;
80715
+ if (!moveResult.ok) githubWarning = (githubWarning ? githubWarning + "; " : "") + `Board move failed: ${moveResult.error}`;
80716
+ }
80717
+ updateLocalMappingStatus$1(mapping, issue_number, "In Progress");
80718
+ saveMapping(cwd, mapping);
80719
+ }
80720
+ } catch (e) {
80721
+ githubWarning = (githubWarning ? githubWarning + "; " : "") + `Board move failed: ${e.message}`;
80722
+ }
80723
+ return mcpSuccess({
80724
+ mode: "full",
80725
+ issue_number,
80726
+ status: "In Progress",
80727
+ commented,
80728
+ moved,
80729
+ reason,
80730
+ ...githubWarning ? { github_warning: githubWarning } : {}
80731
+ }, `Issue #${issue_number} bounced to In Progress${commented ? " with feedback comment" : ""}`);
80732
+ } catch (e) {
80733
+ return mcpError(e.message, "Operation failed");
80734
+ }
80735
+ });
80736
+ }
80737
+ /**
80738
+ * Find an issue entry in the mapping file (searches phases and todos).
80739
+ */
80740
+ function findIssueInMapping$2(mapping, issueNumber) {
80741
+ for (const phase of Object.values(mapping.phases)) {
80742
+ if (phase.tracking_issue.number === issueNumber) return phase.tracking_issue;
80743
+ for (const task of Object.values(phase.tasks)) if (task.number === issueNumber) return task;
80744
+ }
80745
+ if (mapping.todos) {
80746
+ for (const todo of Object.values(mapping.todos)) if (todo.number === issueNumber) return todo;
80747
+ }
80748
+ return null;
80749
+ }
80750
+ /**
80751
+ * Update local mapping status for an issue (mutates mapping in-place).
80752
+ * Returns true if the issue was found and updated.
80753
+ */
80754
+ function updateLocalMappingStatus$1(mapping, issueNumber, status) {
80755
+ for (const phase of Object.values(mapping.phases)) {
80756
+ if (phase.tracking_issue.number === issueNumber) {
80757
+ phase.tracking_issue.status = status;
80758
+ return true;
80759
+ }
80760
+ for (const task of Object.values(phase.tasks)) if (task.number === issueNumber) {
80761
+ task.status = status;
80762
+ return true;
80763
+ }
80764
+ }
80765
+ if (mapping.todos) {
80766
+ for (const todo of Object.values(mapping.todos)) if (todo.number === issueNumber) {
80767
+ todo.status = status;
80768
+ return true;
80769
+ }
80770
+ }
80771
+ return false;
79110
80772
  }
79111
80773
 
79112
80774
  //#endregion
@@ -79114,6 +80776,10 @@ function registerPhaseTools(server) {
79114
80776
  /**
79115
80777
  * Todo CRUD MCP Tools — Todo operations exposed as MCP tools
79116
80778
  *
80779
+ * Integrates with GitHub: todo add creates GitHub issue in 'full' mode,
80780
+ * todo complete closes GitHub issue and moves to Done on board,
80781
+ * todo list enriches with GitHub issue data when available.
80782
+ *
79117
80783
  * CRITICAL: Never import output() or error() from core — they call process.exit().
79118
80784
  * CRITICAL: Never write to stdout — it is reserved for MCP JSON-RPC protocol.
79119
80785
  * CRITICAL: Never call process.exit() — the server must stay alive after every tool call.
@@ -79139,12 +80805,55 @@ function registerTodoTools(server) {
79139
80805
  const filePath = node_path.default.join(pendingDir, filename);
79140
80806
  const content = `---\ncreated: ${today}\ntitle: ${title}\narea: ${area || "general"}\nphase: ${phase || "unassigned"}\n---\n${description || ""}\n`;
79141
80807
  node_fs.default.writeFileSync(filePath, content, "utf-8");
80808
+ let githubIssueNumber = null;
80809
+ let githubIssueUrl = null;
80810
+ let githubWarning;
80811
+ try {
80812
+ if (await detectGitHubMode() === "full") {
80813
+ const mapping = loadMapping(cwd);
80814
+ const issueResult = await createTodoIssue({
80815
+ title,
80816
+ description: description || void 0,
80817
+ milestone: mapping?.milestone_title || void 0
80818
+ });
80819
+ if (issueResult.ok) {
80820
+ githubIssueNumber = issueResult.data.number;
80821
+ githubIssueUrl = issueResult.data.url;
80822
+ if (mapping && mapping.project_number > 0) {
80823
+ const issueUrl = `https://github.com/${mapping.repo}/issues/${issueResult.data.number}`;
80824
+ const addResult = await addItemToProject(mapping.project_number, issueUrl);
80825
+ if (addResult.ok) {
80826
+ updateTodoMapping(cwd, filename, {
80827
+ number: issueResult.data.number,
80828
+ node_id: issueResult.data.node_id,
80829
+ item_id: addResult.data.item_id,
80830
+ status: "To Do"
80831
+ });
80832
+ if (mapping.status_field_id && mapping.status_options["To Do"]) await moveItemToStatus(mapping.project_id, addResult.data.item_id, mapping.status_field_id, mapping.status_options["To Do"]);
80833
+ } else {
80834
+ updateTodoMapping(cwd, filename, {
80835
+ number: issueResult.data.number,
80836
+ node_id: issueResult.data.node_id,
80837
+ item_id: "",
80838
+ status: "To Do"
80839
+ });
80840
+ githubWarning = `Issue created but board add failed: ${addResult.error}`;
80841
+ }
80842
+ } else githubWarning = "Issue created but no project board configured for board tracking.";
80843
+ } else githubWarning = `GitHub issue creation failed: ${issueResult.error}`;
80844
+ }
80845
+ } catch (e) {
80846
+ githubWarning = `GitHub operation failed: ${e.message}`;
80847
+ }
79142
80848
  return mcpSuccess({
79143
80849
  file: filename,
79144
80850
  path: `.planning/todos/pending/${filename}`,
79145
80851
  title,
79146
- area: area || "general"
79147
- }, `Todo created: ${title}`);
80852
+ area: area || "general",
80853
+ github_issue_number: githubIssueNumber,
80854
+ github_issue_url: githubIssueUrl,
80855
+ ...githubWarning ? { github_warning: githubWarning } : {}
80856
+ }, `Todo created: ${title}${githubIssueNumber ? ` (GitHub #${githubIssueNumber})` : ""}`);
79148
80857
  } catch (e) {
79149
80858
  return mcpError(e.message, "Operation failed");
79150
80859
  }
@@ -79163,11 +80872,33 @@ function registerTodoTools(server) {
79163
80872
  content = `completed: ${today}\n` + content;
79164
80873
  node_fs.default.writeFileSync(node_path.default.join(completedDir, todo_id), content, "utf-8");
79165
80874
  node_fs.default.unlinkSync(sourcePath);
80875
+ let githubClosed = false;
80876
+ let githubWarning;
80877
+ try {
80878
+ if (await detectGitHubMode() === "full") {
80879
+ const mapping = loadMapping(cwd);
80880
+ if (mapping?.todos?.[todo_id]) {
80881
+ const todoMapping = mapping.todos[todo_id];
80882
+ if (todoMapping.number > 0) {
80883
+ const closeResult = await closeIssue(todoMapping.number, "completed");
80884
+ githubClosed = closeResult.ok;
80885
+ if (!closeResult.ok) githubWarning = `GitHub issue close failed: ${closeResult.error}`;
80886
+ if (todoMapping.item_id && mapping.status_field_id && mapping.status_options["Done"]) await moveItemToStatus(mapping.project_id, todoMapping.item_id, mapping.status_field_id, mapping.status_options["Done"]);
80887
+ todoMapping.status = "Done";
80888
+ saveMapping(cwd, mapping);
80889
+ }
80890
+ }
80891
+ }
80892
+ } catch (e) {
80893
+ githubWarning = `GitHub operation failed: ${e.message}`;
80894
+ }
79166
80895
  return mcpSuccess({
79167
80896
  completed: true,
79168
80897
  file: todo_id,
79169
- date: today
79170
- }, `Todo completed: ${todo_id}`);
80898
+ date: today,
80899
+ github_closed: githubClosed,
80900
+ ...githubWarning ? { github_warning: githubWarning } : {}
80901
+ }, `Todo completed: ${todo_id}${githubClosed ? " (GitHub issue closed)" : ""}`);
79171
80902
  } catch (e) {
79172
80903
  return mcpError(e.message, "Operation failed");
79173
80904
  }
@@ -79188,6 +80919,22 @@ function registerTodoTools(server) {
79188
80919
  if (status === "pending" || status === "all") dirs.push(node_path.default.join(todosBase, "pending"));
79189
80920
  if (status === "completed" || status === "all") dirs.push(node_path.default.join(todosBase, "completed"));
79190
80921
  const todos = [];
80922
+ let todoMappings = null;
80923
+ let githubWarning;
80924
+ try {
80925
+ if (await detectGitHubMode() === "full") {
80926
+ const mapping = loadMapping(cwd);
80927
+ if (mapping?.todos) {
80928
+ todoMappings = {};
80929
+ for (const [todoId, data] of Object.entries(mapping.todos)) if (data.number > 0) todoMappings[todoId] = {
80930
+ number: data.number,
80931
+ status: data.status
80932
+ };
80933
+ }
80934
+ }
80935
+ } catch (e) {
80936
+ githubWarning = `GitHub enrichment failed: ${e.message}`;
80937
+ }
79191
80938
  for (const dir of dirs) {
79192
80939
  const dirStatus = dir.endsWith("pending") ? "pending" : "completed";
79193
80940
  let files = [];
@@ -79199,19 +80946,25 @@ function registerTodoTools(server) {
79199
80946
  for (const file of files) try {
79200
80947
  const fm = parseTodoFrontmatter(node_fs.default.readFileSync(node_path.default.join(dir, file), "utf-8"));
79201
80948
  if (area && fm.area !== area) continue;
79202
- todos.push({
80949
+ const todoEntry = {
79203
80950
  file,
79204
80951
  created: fm.created,
79205
80952
  title: fm.title,
79206
80953
  area: fm.area,
79207
80954
  status: dirStatus,
79208
80955
  path: `.planning/todos/${dirStatus}/${file}`
79209
- });
80956
+ };
80957
+ if (todoMappings?.[file]) {
80958
+ todoEntry.github_issue_number = todoMappings[file].number;
80959
+ todoEntry.github_status = todoMappings[file].status;
80960
+ }
80961
+ todos.push(todoEntry);
79210
80962
  } catch {}
79211
80963
  }
79212
80964
  return mcpSuccess({
79213
80965
  count: todos.length,
79214
- todos
80966
+ todos,
80967
+ ...githubWarning ? { github_warning: githubWarning } : {}
79215
80968
  }, `${todos.length} todos found`);
79216
80969
  } catch (e) {
79217
80970
  return mcpError(e.message, "Operation failed");
@@ -79224,11 +80977,29 @@ function registerTodoTools(server) {
79224
80977
  /**
79225
80978
  * State Management MCP Tools — STATE.md operations exposed as MCP tools
79226
80979
  *
80980
+ * Integrates with GitHub: blocker add/resolve uses best-effort GitHub
80981
+ * issue linking when blocker text references issue numbers.
80982
+ *
79227
80983
  * CRITICAL: Never import output() or error() from core — they call process.exit().
79228
80984
  * CRITICAL: Never write to stdout — it is reserved for MCP JSON-RPC protocol.
79229
80985
  * CRITICAL: Never call process.exit() — the server must stay alive after every tool call.
79230
80986
  */
79231
80987
  /**
80988
+ * Extract GitHub issue numbers from text.
80989
+ *
80990
+ * Matches patterns like "#42", "issue 42", "issue #42", "blocked by #42".
80991
+ * Returns unique issue numbers found.
80992
+ */
80993
+ function extractIssueNumbers(text) {
80994
+ const matches = text.matchAll(/#(\d+)|issue\s+#?(\d+)/gi);
80995
+ const numbers = /* @__PURE__ */ new Set();
80996
+ for (const match of matches) {
80997
+ const num = parseInt(match[1] || match[2], 10);
80998
+ if (!Number.isNaN(num) && num > 0) numbers.add(num);
80999
+ }
81000
+ return Array.from(numbers);
81001
+ }
81002
+ /**
79232
81003
  * Register all state management tools on the MCP server.
79233
81004
  */
79234
81005
  function registerStateTools(server) {
@@ -79310,10 +81081,24 @@ function registerStateTools(server) {
79310
81081
  const updated = appendToStateSection(node_fs.default.readFileSync(stPath, "utf-8"), /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i, `- ${text}`, [/None\.?\s*\n?/gi, /None yet\.?\s*\n?/gi]);
79311
81082
  if (!updated) return mcpError("Blockers section not found in STATE.md", "Section not found");
79312
81083
  node_fs.default.writeFileSync(stPath, updated, "utf-8");
81084
+ let githubLinked = [];
81085
+ let githubWarning;
81086
+ try {
81087
+ if (await detectGitHubMode() === "full") {
81088
+ const issueNumbers = extractIssueNumbers(text);
81089
+ if (issueNumbers.length > 0) {
81090
+ for (const issueNum of issueNumbers) if ((await postComment(issueNum, `**Blocker added in MAXSIM:**\n\n${text}\n\n---\n*Posted by MAXSIM blocker tracking*`)).ok) githubLinked.push(issueNum);
81091
+ }
81092
+ }
81093
+ } catch (e) {
81094
+ githubWarning = `GitHub linking failed: ${e.message}`;
81095
+ }
79313
81096
  return mcpSuccess({
79314
81097
  added: true,
79315
- blocker: text
79316
- }, "Blocker added");
81098
+ blocker: text,
81099
+ github_linked_issues: githubLinked.length > 0 ? githubLinked : null,
81100
+ ...githubWarning ? { github_warning: githubWarning } : {}
81101
+ }, `Blocker added${githubLinked.length > 0 ? ` (linked to ${githubLinked.map((n) => `#${n}`).join(", ")})` : ""}`);
79317
81102
  } catch (e) {
79318
81103
  return mcpError(e.message, "Operation failed");
79319
81104
  }
@@ -79328,17 +81113,37 @@ function registerStateTools(server) {
79328
81113
  const sectionPattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
79329
81114
  const match = content.match(sectionPattern);
79330
81115
  if (!match) return mcpError("Blockers section not found in STATE.md", "Section not found");
79331
- let newBody = match[2].split("\n").filter((line) => {
81116
+ const lines = match[2].split("\n");
81117
+ const matchingLines = [];
81118
+ let newBody = lines.filter((line) => {
79332
81119
  if (!line.startsWith("- ")) return true;
79333
- return !line.toLowerCase().includes(text.toLowerCase());
81120
+ if (line.toLowerCase().includes(text.toLowerCase())) {
81121
+ matchingLines.push(line);
81122
+ return false;
81123
+ }
81124
+ return true;
79334
81125
  }).join("\n");
79335
81126
  if (!newBody.trim() || !newBody.includes("- ")) newBody = "None\n";
79336
81127
  content = content.replace(sectionPattern, (_match, header) => `${header}${newBody}`);
79337
81128
  node_fs.default.writeFileSync(stPath, content, "utf-8");
81129
+ let githubCommented = [];
81130
+ let githubWarning;
81131
+ try {
81132
+ if (await detectGitHubMode() === "full") {
81133
+ const issueNumbers = extractIssueNumbers(matchingLines.join(" ") + " " + text);
81134
+ if (issueNumbers.length > 0) {
81135
+ for (const issueNum of issueNumbers) if ((await postComment(issueNum, `**Blocker resolved in MAXSIM:**\n\nResolved blocker matching: "${text}"\n\n---\n*Posted by MAXSIM blocker tracking*`)).ok) githubCommented.push(issueNum);
81136
+ }
81137
+ }
81138
+ } catch (e) {
81139
+ githubWarning = `GitHub comment failed: ${e.message}`;
81140
+ }
79338
81141
  return mcpSuccess({
79339
81142
  resolved: true,
79340
- blocker: text
79341
- }, "Blocker resolved");
81143
+ blocker: text,
81144
+ github_commented_issues: githubCommented.length > 0 ? githubCommented : null,
81145
+ ...githubWarning ? { github_warning: githubWarning } : {}
81146
+ }, `Blocker resolved${githubCommented.length > 0 ? ` (commented on ${githubCommented.map((n) => `#${n}`).join(", ")})` : ""}`);
79342
81147
  } catch (e) {
79343
81148
  return mcpError(e.message, "Operation failed");
79344
81149
  }
@@ -79566,6 +81371,897 @@ function registerConfigTools(server) {
79566
81371
  });
79567
81372
  }
79568
81373
 
81374
+ //#endregion
81375
+ //#region src/github/types.ts
81376
+ const MAXSIM_LABELS = [
81377
+ {
81378
+ name: "maxsim",
81379
+ color: "6f42c1",
81380
+ description: "MAXSIM managed issue"
81381
+ },
81382
+ {
81383
+ name: "phase-task",
81384
+ color: "0075ca",
81385
+ description: "MAXSIM phase task"
81386
+ },
81387
+ {
81388
+ name: "todo",
81389
+ color: "fbca04",
81390
+ description: "MAXSIM todo item"
81391
+ },
81392
+ {
81393
+ name: "imported",
81394
+ color: "e4e669",
81395
+ description: "Imported into MAXSIM tracking"
81396
+ },
81397
+ {
81398
+ name: "superseded",
81399
+ color: "d73a4a",
81400
+ description: "Superseded by newer plan"
81401
+ }
81402
+ ];
81403
+ const FIBONACCI_POINTS = [
81404
+ 1,
81405
+ 2,
81406
+ 3,
81407
+ 5,
81408
+ 8,
81409
+ 13,
81410
+ 21,
81411
+ 34
81412
+ ];
81413
+
81414
+ //#endregion
81415
+ //#region src/github/labels.ts
81416
+ /**
81417
+ * Ensure all MAXSIM labels exist on the repository.
81418
+ *
81419
+ * Iterates over MAXSIM_LABELS and runs `gh label create` with `--force`
81420
+ * for each label. The `--force` flag updates existing labels with the
81421
+ * specified color and description.
81422
+ *
81423
+ * Continues on individual label failures (logs to stderr).
81424
+ * Only fails if ALL labels fail to create.
81425
+ */
81426
+ async function ensureLabels() {
81427
+ let successCount = 0;
81428
+ const errors = [];
81429
+ for (const label of MAXSIM_LABELS) {
81430
+ const result = await ghExec([
81431
+ "label",
81432
+ "create",
81433
+ label.name,
81434
+ "--color",
81435
+ label.color,
81436
+ "--description",
81437
+ label.description,
81438
+ "--force"
81439
+ ]);
81440
+ if (result.ok) successCount++;
81441
+ else {
81442
+ const errMsg = result.error;
81443
+ console.error(`[maxsim] Failed to create label "${label.name}": ${errMsg}`);
81444
+ errors.push(`${label.name}: ${errMsg}`);
81445
+ }
81446
+ }
81447
+ if (successCount === 0 && errors.length > 0) return {
81448
+ ok: false,
81449
+ error: `All labels failed to create: ${errors.join("; ")}`,
81450
+ code: "UNKNOWN"
81451
+ };
81452
+ return {
81453
+ ok: true,
81454
+ data: void 0
81455
+ };
81456
+ }
81457
+
81458
+ //#endregion
81459
+ //#region src/github/templates.ts
81460
+ /**
81461
+ * GitHub Issue Templates — Template file generation
81462
+ *
81463
+ * Installs GitHub Issue Form YAML templates into `.github/ISSUE_TEMPLATE/`
81464
+ * for the MAXSIM-managed issue types: phase tasks and todos.
81465
+ *
81466
+ * These are file-system operations only (no gh CLI needed).
81467
+ * Uses synchronous fs to match existing core module patterns.
81468
+ *
81469
+ * CRITICAL: Never call process.exit().
81470
+ */
81471
+ /**
81472
+ * Phase task issue template (GitHub Issue Forms YAML format).
81473
+ *
81474
+ * Used for issues created from MAXSIM phase plans.
81475
+ * Labels: maxsim, phase-task
81476
+ */
81477
+ const PHASE_TASK_TEMPLATE = `name: "MAXSIM Phase Task"
81478
+ description: "Task generated by MAXSIM phase planning"
81479
+ labels: ["maxsim", "phase-task"]
81480
+ body:
81481
+ - type: markdown
81482
+ attributes:
81483
+ value: |
81484
+ This issue was auto-generated by MAXSIM.
81485
+
81486
+ - type: textarea
81487
+ id: summary
81488
+ attributes:
81489
+ label: Summary
81490
+ description: Task summary
81491
+ validations:
81492
+ required: true
81493
+
81494
+ - type: textarea
81495
+ id: spec
81496
+ attributes:
81497
+ label: Full Specification
81498
+ description: Detailed task specification including actions, criteria, and dependencies
81499
+ `;
81500
+ /**
81501
+ * Todo issue template (GitHub Issue Forms YAML format).
81502
+ *
81503
+ * Used for issues created from MAXSIM todo items.
81504
+ * Labels: maxsim, todo
81505
+ */
81506
+ const TODO_TEMPLATE = `name: "MAXSIM Todo"
81507
+ description: "Todo item tracked by MAXSIM"
81508
+ labels: ["maxsim", "todo"]
81509
+ body:
81510
+ - type: textarea
81511
+ id: description
81512
+ attributes:
81513
+ label: Description
81514
+ description: Brief description of the todo item
81515
+ validations:
81516
+ required: true
81517
+
81518
+ - type: textarea
81519
+ id: acceptance
81520
+ attributes:
81521
+ label: Acceptance Criteria
81522
+ description: What defines "done" for this todo?
81523
+ `;
81524
+ /**
81525
+ * Install MAXSIM issue templates into the project's `.github/ISSUE_TEMPLATE/` directory.
81526
+ *
81527
+ * Creates the directory recursively if it does not exist.
81528
+ * Writes two YAML files:
81529
+ * - phase-task.yml (for phase plan tasks)
81530
+ * - todo.yml (for todo items)
81531
+ *
81532
+ * Overwrites existing templates if present (to ensure latest version).
81533
+ * This is a synchronous file write operation (no gh CLI needed).
81534
+ */
81535
+ function installIssueTemplates(cwd) {
81536
+ const templateDir = node_path.default.join(cwd, ".github", "ISSUE_TEMPLATE");
81537
+ node_fs.default.mkdirSync(templateDir, { recursive: true });
81538
+ node_fs.default.writeFileSync(node_path.default.join(templateDir, "phase-task.yml"), PHASE_TASK_TEMPLATE, "utf-8");
81539
+ node_fs.default.writeFileSync(node_path.default.join(templateDir, "todo.yml"), TODO_TEMPLATE, "utf-8");
81540
+ }
81541
+
81542
+ //#endregion
81543
+ //#region src/mcp/github-tools.ts
81544
+ /**
81545
+ * GitHub Issue Lifecycle MCP Tools — GitHub operations exposed as MCP tools
81546
+ *
81547
+ * Provides MCP tools for issue CRUD, PR creation with auto-close linking (AC-08),
81548
+ * sync checking (AC-09), and issue import. Every tool checks detectGitHubMode()
81549
+ * and degrades gracefully to local-only behavior when GitHub is not configured.
81550
+ *
81551
+ * CRITICAL: Never import output() or error() from core — they call process.exit().
81552
+ * CRITICAL: Never write to stdout — it is reserved for MCP JSON-RPC protocol.
81553
+ * CRITICAL: Never call process.exit() — the server must stay alive after every tool call.
81554
+ */
81555
+ /**
81556
+ * Register all GitHub issue lifecycle tools on the MCP server.
81557
+ */
81558
+ function registerGitHubTools(server) {
81559
+ server.tool("mcp_github_setup", "Set up GitHub integration: create project board, labels, milestone, and issue templates.", { milestone_title: stringType().optional().describe("Milestone title (defaults to current milestone from STATE.md)") }, async ({ milestone_title }) => {
81560
+ try {
81561
+ const cwd = detectProjectRoot();
81562
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
81563
+ if (await detectGitHubMode() === "local-only") {
81564
+ installIssueTemplates(cwd);
81565
+ return mcpSuccess({
81566
+ mode: "local-only",
81567
+ templates_installed: true,
81568
+ board_created: false,
81569
+ labels_created: false,
81570
+ milestone_created: false
81571
+ }, "Local-only mode: installed issue templates only. Run `gh auth login` with project scope for full GitHub integration.");
81572
+ }
81573
+ const boardResult = await ensureProjectBoard("MAXSIM Task Board", cwd);
81574
+ if (!boardResult.ok) return mcpError(`Board setup failed: ${boardResult.error}`, "Setup failed");
81575
+ const labelsResult = await ensureLabels();
81576
+ if (!labelsResult.ok) return mcpError(`Label setup failed: ${labelsResult.error}`, "Setup failed");
81577
+ let milestoneData = null;
81578
+ if (milestone_title) {
81579
+ const msResult = await ensureMilestone(milestone_title);
81580
+ if (msResult.ok) {
81581
+ milestoneData = msResult.data;
81582
+ const mapping = loadMapping(cwd);
81583
+ if (mapping) {
81584
+ mapping.milestone_id = msResult.data.number;
81585
+ mapping.milestone_title = milestone_title;
81586
+ saveMapping(cwd, mapping);
81587
+ }
81588
+ }
81589
+ }
81590
+ installIssueTemplates(cwd);
81591
+ return mcpSuccess({
81592
+ mode: "full",
81593
+ board: {
81594
+ number: boardResult.data.number,
81595
+ created: boardResult.data.created
81596
+ },
81597
+ labels_created: true,
81598
+ milestone: milestoneData ? {
81599
+ number: milestoneData.number,
81600
+ title: milestone_title,
81601
+ created: milestoneData.created
81602
+ } : null,
81603
+ templates_installed: true
81604
+ }, `GitHub integration set up: board #${boardResult.data.number}, labels, ${milestoneData ? `milestone "${milestone_title}"` : "no milestone"}, templates`);
81605
+ } catch (e) {
81606
+ return mcpError(e.message, "Operation failed");
81607
+ }
81608
+ });
81609
+ server.tool("mcp_create_plan_issues", "Create GitHub issues for all tasks in a finalized plan. Creates task issues and parent tracking issue.", {
81610
+ phase: stringType().describe("Phase number (e.g. \"01\")"),
81611
+ plan: stringType().describe("Plan number (e.g. \"01\")"),
81612
+ phase_name: stringType().describe("Phase description for the tracking issue title"),
81613
+ tasks: arrayType(objectType({
81614
+ taskId: stringType(),
81615
+ title: stringType(),
81616
+ summary: stringType(),
81617
+ actions: arrayType(stringType()),
81618
+ acceptanceCriteria: arrayType(stringType()),
81619
+ dependencies: arrayType(stringType()).optional(),
81620
+ estimate: numberType().optional()
81621
+ })).describe("Array of task objects to create issues for"),
81622
+ milestone: stringType().optional().describe("Milestone title to assign")
81623
+ }, async ({ phase, plan, phase_name, tasks, milestone }) => {
81624
+ try {
81625
+ const cwd = detectProjectRoot();
81626
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
81627
+ if (await detectGitHubMode() === "local-only") return mcpSuccess({
81628
+ mode: "local-only",
81629
+ warning: "GitHub not configured, issues not created",
81630
+ tasks_count: tasks.length
81631
+ }, "Local-only mode: GitHub issues not created. Run `gh auth login` for full integration.");
81632
+ const mapping = loadMapping(cwd);
81633
+ const result = await createAllPlanIssues({
81634
+ phaseNum: phase,
81635
+ planNum: plan,
81636
+ phaseName: phase_name,
81637
+ tasks,
81638
+ milestone,
81639
+ projectTitle: mapping?.project_number ? void 0 : void 0,
81640
+ cwd
81641
+ });
81642
+ if (!result.ok) return mcpError(`Issue creation failed: ${result.error}`, "Creation failed");
81643
+ if (mapping && mapping.project_number > 0) {
81644
+ const repo = mapping.repo;
81645
+ const allIssueNumbers = [result.data.parentIssue, ...result.data.taskIssues.map((t) => t.issueNumber)];
81646
+ for (const issueNum of allIssueNumbers) {
81647
+ const issueUrl = `https://github.com/${repo}/issues/${issueNum}`;
81648
+ const addResult = await addItemToProject(mapping.project_number, issueUrl);
81649
+ if (addResult.ok) {
81650
+ const taskEntry = result.data.taskIssues.find((t) => t.issueNumber === issueNum);
81651
+ if (taskEntry) updateTaskMapping(cwd, phase, taskEntry.taskId, { item_id: addResult.data.item_id });
81652
+ if (mapping.status_options["To Do"] && mapping.status_field_id) await moveItemToStatus(mapping.project_id, addResult.data.item_id, mapping.status_field_id, mapping.status_options["To Do"]);
81653
+ if (taskEntry && mapping.estimate_field_id) {
81654
+ const taskDef = tasks.find((t) => t.taskId === taskEntry.taskId);
81655
+ if (taskDef?.estimate) await setEstimate(mapping.project_id, addResult.data.item_id, mapping.estimate_field_id, taskDef.estimate);
81656
+ }
81657
+ }
81658
+ }
81659
+ }
81660
+ return mcpSuccess({
81661
+ mode: "full",
81662
+ parent_issue: result.data.parentIssue,
81663
+ task_issues: result.data.taskIssues,
81664
+ total_created: result.data.taskIssues.length + 1
81665
+ }, `Created ${result.data.taskIssues.length} task issues + parent tracking issue #${result.data.parentIssue}`);
81666
+ } catch (e) {
81667
+ return mcpError(e.message, "Operation failed");
81668
+ }
81669
+ });
81670
+ server.tool("mcp_create_todo_issue", "Create a GitHub issue for a todo item.", {
81671
+ title: stringType().describe("Todo title"),
81672
+ description: stringType().optional().describe("Todo description"),
81673
+ acceptance_criteria: arrayType(stringType()).optional().describe("Acceptance criteria list")
81674
+ }, async ({ title, description, acceptance_criteria }) => {
81675
+ try {
81676
+ const cwd = detectProjectRoot();
81677
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
81678
+ if (await detectGitHubMode() === "local-only") return mcpSuccess({
81679
+ mode: "local-only",
81680
+ warning: "GitHub not configured. Use mcp_add_todo for local todo tracking.",
81681
+ title
81682
+ }, "Local-only mode: GitHub todo issue not created.");
81683
+ const mapping = loadMapping(cwd);
81684
+ const result = await createTodoIssue({
81685
+ title,
81686
+ description,
81687
+ acceptanceCriteria: acceptance_criteria,
81688
+ milestone: mapping?.milestone_title || void 0
81689
+ });
81690
+ if (!result.ok) return mcpError(`Todo issue creation failed: ${result.error}`, "Creation failed");
81691
+ if (mapping && mapping.project_number > 0) {
81692
+ const issueUrl = `https://github.com/${mapping.repo}/issues/${result.data.number}`;
81693
+ const addResult = await addItemToProject(mapping.project_number, issueUrl);
81694
+ if (addResult.ok && mapping) {
81695
+ if (!mapping.todos) mapping.todos = {};
81696
+ mapping.todos[`todo-${result.data.number}`] = {
81697
+ number: result.data.number,
81698
+ node_id: result.data.node_id,
81699
+ item_id: addResult.data.item_id,
81700
+ status: "To Do"
81701
+ };
81702
+ saveMapping(cwd, mapping);
81703
+ }
81704
+ }
81705
+ return mcpSuccess({
81706
+ mode: "full",
81707
+ issue_number: result.data.number,
81708
+ url: result.data.url
81709
+ }, `Created todo issue #${result.data.number}: ${title}`);
81710
+ } catch (e) {
81711
+ return mcpError(e.message, "Operation failed");
81712
+ }
81713
+ });
81714
+ server.tool("mcp_move_issue", "Move a GitHub issue to a new status column (To Do, In Progress, In Review, Done).", {
81715
+ issue_number: numberType().describe("GitHub issue number"),
81716
+ status: enumType([
81717
+ "To Do",
81718
+ "In Progress",
81719
+ "In Review",
81720
+ "Done"
81721
+ ]).describe("Target status column")
81722
+ }, async ({ issue_number, status }) => {
81723
+ try {
81724
+ const cwd = detectProjectRoot();
81725
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
81726
+ const mode = await detectGitHubMode();
81727
+ const mapping = loadMapping(cwd);
81728
+ if (mode === "local-only") {
81729
+ if (mapping) {
81730
+ if (updateLocalMappingStatus(mapping, issue_number, status)) {
81731
+ saveMapping(cwd, mapping);
81732
+ return mcpSuccess({
81733
+ mode: "local-only",
81734
+ issue_number,
81735
+ status,
81736
+ local_updated: true
81737
+ }, `Local mapping updated: issue #${issue_number} -> ${status}`);
81738
+ }
81739
+ }
81740
+ return mcpError(`Issue #${issue_number} not found in local mapping`, "Issue not tracked");
81741
+ }
81742
+ if (!mapping) return mcpError("github-issues.json not found. Run mcp_github_setup first.", "Setup required");
81743
+ const issueEntry = findIssueInMapping$1(mapping, issue_number);
81744
+ if (!issueEntry) return mcpError(`Issue #${issue_number} not found in local mapping`, "Issue not tracked");
81745
+ if (!issueEntry.item_id) return mcpError(`Issue #${issue_number} has no project item_id. It may not have been added to the board.`, "Not on board");
81746
+ const statusOptionId = mapping.status_options[status];
81747
+ if (!statusOptionId) return mcpError(`Status "${status}" not found in project board options`, "Invalid status");
81748
+ const moveResult = await moveItemToStatus(mapping.project_id, issueEntry.item_id, mapping.status_field_id, statusOptionId);
81749
+ if (!moveResult.ok) return mcpError(`Move failed: ${moveResult.error}`, "Move failed");
81750
+ updateLocalMappingStatus(mapping, issue_number, status);
81751
+ saveMapping(cwd, mapping);
81752
+ return mcpSuccess({
81753
+ mode: "full",
81754
+ issue_number,
81755
+ status,
81756
+ moved: true
81757
+ }, `Issue #${issue_number} moved to "${status}"`);
81758
+ } catch (e) {
81759
+ return mcpError(e.message, "Operation failed");
81760
+ }
81761
+ });
81762
+ server.tool("mcp_close_issue", "Close a GitHub issue as completed or not planned.", {
81763
+ issue_number: numberType().describe("GitHub issue number"),
81764
+ reason: enumType(["completed", "not_planned"]).optional().default("completed").describe("Close reason")
81765
+ }, async ({ issue_number, reason }) => {
81766
+ try {
81767
+ const cwd = detectProjectRoot();
81768
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
81769
+ if (await detectGitHubMode() === "local-only") return mcpSuccess({
81770
+ mode: "local-only",
81771
+ warning: "GitHub not configured. Cannot close remote issue.",
81772
+ issue_number
81773
+ }, "Local-only mode: cannot close GitHub issue.");
81774
+ const result = await closeIssue(issue_number, reason);
81775
+ if (!result.ok) return mcpError(`Close failed: ${result.error}`, "Close failed");
81776
+ const mapping = loadMapping(cwd);
81777
+ if (mapping) {
81778
+ const issueEntry = findIssueInMapping$1(mapping, issue_number);
81779
+ if (issueEntry?.item_id && mapping.status_options["Done"] && mapping.status_field_id) await moveItemToStatus(mapping.project_id, issueEntry.item_id, mapping.status_field_id, mapping.status_options["Done"]);
81780
+ updateLocalMappingStatus(mapping, issue_number, "Done");
81781
+ saveMapping(cwd, mapping);
81782
+ }
81783
+ return mcpSuccess({
81784
+ mode: "full",
81785
+ issue_number,
81786
+ reason,
81787
+ closed: true
81788
+ }, `Issue #${issue_number} closed (${reason})`);
81789
+ } catch (e) {
81790
+ return mcpError(e.message, "Operation failed");
81791
+ }
81792
+ });
81793
+ server.tool("mcp_post_comment", "Post a progress comment on a GitHub issue.", {
81794
+ issue_number: numberType().describe("GitHub issue number"),
81795
+ body: stringType().describe("Comment body (markdown supported)")
81796
+ }, async ({ issue_number, body }) => {
81797
+ try {
81798
+ if (await detectGitHubMode() === "local-only") return mcpSuccess({
81799
+ mode: "local-only",
81800
+ warning: "GitHub not configured. Cannot post comment.",
81801
+ issue_number
81802
+ }, "Local-only mode: cannot post comment on GitHub issue.");
81803
+ const result = await postComment(issue_number, body);
81804
+ if (!result.ok) return mcpError(`Comment failed: ${result.error}`, "Comment failed");
81805
+ return mcpSuccess({
81806
+ mode: "full",
81807
+ issue_number,
81808
+ commented: true
81809
+ }, `Comment posted on issue #${issue_number}`);
81810
+ } catch (e) {
81811
+ return mcpError(e.message, "Operation failed");
81812
+ }
81813
+ });
81814
+ server.tool("mcp_import_issue", "Import an external GitHub issue into MAXSIM tracking.", { issue_number: numberType().describe("GitHub issue number to import") }, async ({ issue_number }) => {
81815
+ try {
81816
+ if (await detectGitHubMode() === "local-only") return mcpSuccess({
81817
+ mode: "local-only",
81818
+ warning: "GitHub not configured. Cannot import issue.",
81819
+ issue_number
81820
+ }, "Local-only mode: cannot import GitHub issue.");
81821
+ const result = await importExternalIssue(issue_number);
81822
+ if (!result.ok) return mcpError(`Import failed: ${result.error}`, "Import failed");
81823
+ return mcpSuccess({
81824
+ mode: "full",
81825
+ issue_number: result.data.number,
81826
+ title: result.data.title,
81827
+ labels: result.data.labels,
81828
+ imported: true
81829
+ }, `Imported issue #${result.data.number}: "${result.data.title}". Assign to a phase or todo for tracking.`);
81830
+ } catch (e) {
81831
+ return mcpError(e.message, "Operation failed");
81832
+ }
81833
+ });
81834
+ server.tool("mcp_sync_check", "Check for external changes to tracked GitHub issues.", {}, async () => {
81835
+ try {
81836
+ const cwd = detectProjectRoot();
81837
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
81838
+ if (await detectGitHubMode() === "local-only") return mcpSuccess({
81839
+ mode: "local-only",
81840
+ warning: "GitHub not configured. Sync check not available.",
81841
+ in_sync: true,
81842
+ changes: []
81843
+ }, "Local-only mode: sync check skipped.");
81844
+ const result = await syncCheck(cwd);
81845
+ if (!result.ok) return mcpError(`Sync check failed: ${result.error}`, "Sync failed");
81846
+ return mcpSuccess({
81847
+ mode: "full",
81848
+ in_sync: result.data.inSync,
81849
+ changes: result.data.changes,
81850
+ change_count: result.data.changes.length
81851
+ }, result.data.inSync ? "All tracked issues are in sync with GitHub." : `${result.data.changes.length} discrepancies found between local mapping and GitHub.`);
81852
+ } catch (e) {
81853
+ return mcpError(e.message, "Operation failed");
81854
+ }
81855
+ });
81856
+ server.tool("mcp_supersede_plan", "Close old plan issues and link to new plan issues.", {
81857
+ phase: stringType().describe("Phase number"),
81858
+ old_plan: stringType().describe("Old plan number to supersede"),
81859
+ new_plan: stringType().describe("New plan number that replaces it")
81860
+ }, async ({ phase, old_plan, new_plan }) => {
81861
+ try {
81862
+ const cwd = detectProjectRoot();
81863
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
81864
+ if (await detectGitHubMode() === "local-only") return mcpSuccess({
81865
+ mode: "local-only",
81866
+ warning: "GitHub not configured. Cannot supersede plan issues.",
81867
+ phase,
81868
+ old_plan,
81869
+ new_plan
81870
+ }, "Local-only mode: plan supersession skipped.");
81871
+ const mapping = loadMapping(cwd);
81872
+ if (!mapping) return mcpError("github-issues.json not found. Run mcp_github_setup first.", "Setup required");
81873
+ const newPhaseMapping = mapping.phases[phase];
81874
+ if (!newPhaseMapping) return mcpError(`Phase ${phase} not found in mapping. Create new plan issues first.`, "Phase not found");
81875
+ const result = await supersedePlanIssues({
81876
+ phaseNum: phase,
81877
+ oldPlanNum: old_plan,
81878
+ newPlanNum: new_plan,
81879
+ newIssueNumbers: Object.entries(newPhaseMapping.tasks).map(([taskId, task]) => ({
81880
+ taskId,
81881
+ issueNumber: task.number
81882
+ })),
81883
+ cwd
81884
+ });
81885
+ if (!result.ok) return mcpError(`Supersession failed: ${result.error}`, "Supersession failed");
81886
+ return mcpSuccess({
81887
+ mode: "full",
81888
+ phase,
81889
+ old_plan,
81890
+ new_plan,
81891
+ superseded: true
81892
+ }, `Plan ${phase}-${old_plan} superseded by ${phase}-${new_plan}`);
81893
+ } catch (e) {
81894
+ return mcpError(e.message, "Operation failed");
81895
+ }
81896
+ });
81897
+ server.tool("mcp_create_pr", "Create a pull request with auto-close linking for tracked GitHub issues. Generates PR description with Closes #N lines (AC-08).", {
81898
+ issue_numbers: arrayType(numberType()).describe("Issue numbers to auto-close when PR merges"),
81899
+ branch: stringType().describe("Source branch name for the PR"),
81900
+ title: stringType().describe("PR title"),
81901
+ base: stringType().optional().default("main").describe("Base branch (default: main)"),
81902
+ additional_context: stringType().optional().describe("Additional context to include in PR body"),
81903
+ draft: booleanType().optional().default(false).describe("Create as draft PR")
81904
+ }, async ({ issue_numbers, branch, title, base, additional_context, draft }) => {
81905
+ try {
81906
+ const mode = await detectGitHubMode();
81907
+ const prBody = buildPrBody(issue_numbers, additional_context);
81908
+ if (mode === "local-only") return mcpSuccess({
81909
+ mode: "local-only",
81910
+ warning: "GitHub not configured. PR not created. Use the body below to create manually.",
81911
+ pr_body: prBody,
81912
+ issues_linked: issue_numbers
81913
+ }, "Local-only mode: PR body generated but PR not created.");
81914
+ const args = [
81915
+ "pr",
81916
+ "create",
81917
+ "--title",
81918
+ title,
81919
+ "--body",
81920
+ prBody,
81921
+ "--head",
81922
+ branch
81923
+ ];
81924
+ if (base) args.push("--base", base);
81925
+ if (draft) args.push("--draft");
81926
+ const createResult = await ghExec(args);
81927
+ if (!createResult.ok) return mcpError(`PR creation failed: ${createResult.error}`, "PR creation failed");
81928
+ const prUrl = createResult.data.trim();
81929
+ const prNumberMatch = prUrl.match(/\/pull\/(\d+)/);
81930
+ return mcpSuccess({
81931
+ mode: "full",
81932
+ pr_number: prNumberMatch ? parseInt(prNumberMatch[1], 10) : null,
81933
+ pr_url: prUrl,
81934
+ issues_linked: issue_numbers,
81935
+ draft
81936
+ }, `PR${draft ? " (draft)" : ""} created: ${prUrl} — auto-closes ${issue_numbers.map((n) => `#${n}`).join(", ")}`);
81937
+ } catch (e) {
81938
+ return mcpError(e.message, "Operation failed");
81939
+ }
81940
+ });
81941
+ }
81942
+ /**
81943
+ * Find an issue entry in the mapping file (searches phases and todos).
81944
+ */
81945
+ function findIssueInMapping$1(mapping, issueNumber) {
81946
+ for (const phase of Object.values(mapping.phases)) {
81947
+ if (phase.tracking_issue.number === issueNumber) return phase.tracking_issue;
81948
+ for (const task of Object.values(phase.tasks)) if (task.number === issueNumber) return task;
81949
+ }
81950
+ if (mapping.todos) {
81951
+ for (const todo of Object.values(mapping.todos)) if (todo.number === issueNumber) return todo;
81952
+ }
81953
+ return null;
81954
+ }
81955
+ /**
81956
+ * Update local mapping status for an issue (mutates mapping in-place).
81957
+ * Returns true if the issue was found and updated.
81958
+ */
81959
+ function updateLocalMappingStatus(mapping, issueNumber, status) {
81960
+ for (const phase of Object.values(mapping.phases)) {
81961
+ if (phase.tracking_issue.number === issueNumber) {
81962
+ phase.tracking_issue.status = status;
81963
+ return true;
81964
+ }
81965
+ for (const task of Object.values(phase.tasks)) if (task.number === issueNumber) {
81966
+ task.status = status;
81967
+ return true;
81968
+ }
81969
+ }
81970
+ if (mapping.todos) {
81971
+ for (const todo of Object.values(mapping.todos)) if (todo.number === issueNumber) {
81972
+ todo.status = status;
81973
+ return true;
81974
+ }
81975
+ }
81976
+ return false;
81977
+ }
81978
+
81979
+ //#endregion
81980
+ //#region src/mcp/board-tools.ts
81981
+ /**
81982
+ * Board Query MCP Tools — Project board operations exposed as MCP tools
81983
+ *
81984
+ * Provides MCP tools for querying the GitHub project board, searching issues,
81985
+ * getting issue details, and setting estimates. Every tool checks detectGitHubMode()
81986
+ * and degrades gracefully to local-only behavior when GitHub is not configured.
81987
+ *
81988
+ * CRITICAL: Never import output() or error() from core — they call process.exit().
81989
+ * CRITICAL: Never write to stdout — it is reserved for MCP JSON-RPC protocol.
81990
+ * CRITICAL: Never call process.exit() — the server must stay alive after every tool call.
81991
+ */
81992
+ /**
81993
+ * Register all board query tools on the MCP server.
81994
+ */
81995
+ function registerBoardTools(server) {
81996
+ server.tool("mcp_query_board", "Query the GitHub project board. Returns all items with their status, estimates, and issue details.", {
81997
+ status: enumType([
81998
+ "To Do",
81999
+ "In Progress",
82000
+ "In Review",
82001
+ "Done"
82002
+ ]).optional().describe("Filter by status column"),
82003
+ phase: stringType().optional().describe("Filter by phase number (matches issue title prefix)")
82004
+ }, async ({ status, phase }) => {
82005
+ try {
82006
+ const cwd = detectProjectRoot();
82007
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
82008
+ const mode = await detectGitHubMode();
82009
+ const mapping = loadMapping(cwd);
82010
+ if (mode === "local-only") {
82011
+ if (!mapping) return mcpSuccess({
82012
+ mode: "local-only",
82013
+ items: [],
82014
+ count: 0
82015
+ }, "Local-only mode: no mapping file found.");
82016
+ const items = buildLocalBoardItems(mapping, status, phase);
82017
+ return mcpSuccess({
82018
+ mode: "local-only",
82019
+ items,
82020
+ count: items.length
82021
+ }, `Local-only mode: ${items.length} items from local mapping.`);
82022
+ }
82023
+ if (!mapping || !mapping.project_number) return mcpError("No project board configured. Run mcp_github_setup first.", "Setup required");
82024
+ const result = await ghExec([
82025
+ "project",
82026
+ "item-list",
82027
+ String(mapping.project_number),
82028
+ "--owner",
82029
+ "@me",
82030
+ "--format",
82031
+ "json"
82032
+ ], { parseJson: true });
82033
+ if (!result.ok) return mcpError(`Board query failed: ${result.error}`, "Query failed");
82034
+ let items = result.data.items || [];
82035
+ if (status) items = items.filter((item) => item.status === status);
82036
+ if (phase) {
82037
+ const phasePrefix = `[P${phase}]`;
82038
+ const phasePrefixAlt = `[Phase ${phase}]`;
82039
+ items = items.filter((item) => item.title?.includes(phasePrefix) || item.title?.includes(phasePrefixAlt) || item.content?.title?.includes(phasePrefix) || item.content?.title?.includes(phasePrefixAlt));
82040
+ }
82041
+ const formatted = items.map((item) => ({
82042
+ item_id: item.id,
82043
+ title: item.content?.title ?? item.title,
82044
+ issue_number: item.content?.number ?? null,
82045
+ status: item.status ?? "No Status",
82046
+ url: item.content?.url ?? null
82047
+ }));
82048
+ return mcpSuccess({
82049
+ mode: "full",
82050
+ items: formatted,
82051
+ count: formatted.length
82052
+ }, `Board query: ${formatted.length} items${status ? ` in "${status}"` : ""}${phase ? ` for phase ${phase}` : ""}`);
82053
+ } catch (e) {
82054
+ return mcpError(e.message, "Operation failed");
82055
+ }
82056
+ });
82057
+ server.tool("mcp_search_issues", "Search GitHub issues by label, milestone, state, or text query.", {
82058
+ labels: arrayType(stringType()).optional().describe("Filter by label names"),
82059
+ milestone: stringType().optional().describe("Filter by milestone title"),
82060
+ state: enumType([
82061
+ "open",
82062
+ "closed",
82063
+ "all"
82064
+ ]).optional().default("open").describe("Filter by issue state"),
82065
+ query: stringType().optional().describe("Text search query")
82066
+ }, async ({ labels, milestone, state, query }) => {
82067
+ try {
82068
+ const cwd = detectProjectRoot();
82069
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
82070
+ if (await detectGitHubMode() === "local-only") {
82071
+ const mapping = loadMapping(cwd);
82072
+ if (!mapping) return mcpSuccess({
82073
+ mode: "local-only",
82074
+ issues: [],
82075
+ count: 0
82076
+ }, "Local-only mode: no mapping file found.");
82077
+ const items = buildLocalSearchResults(mapping, state);
82078
+ return mcpSuccess({
82079
+ mode: "local-only",
82080
+ issues: items,
82081
+ count: items.length
82082
+ }, `Local-only mode: ${items.length} items from local mapping.`);
82083
+ }
82084
+ const args = [
82085
+ "issue",
82086
+ "list",
82087
+ "--json",
82088
+ "number,title,state,labels,milestone",
82089
+ "--limit",
82090
+ "100"
82091
+ ];
82092
+ if (state && state !== "all") args.push("--state", state);
82093
+ else if (state === "all") args.push("--state", "all");
82094
+ if (labels && labels.length > 0) for (const label of labels) args.push("--label", label);
82095
+ if (milestone) args.push("--milestone", milestone);
82096
+ if (query) args.push("--search", query);
82097
+ const result = await ghExec(args, { parseJson: true });
82098
+ if (!result.ok) return mcpError(`Search failed: ${result.error}`, "Search failed");
82099
+ const issues = result.data.map((issue) => ({
82100
+ number: issue.number,
82101
+ title: issue.title,
82102
+ state: issue.state,
82103
+ labels: issue.labels.map((l) => l.name),
82104
+ milestone: issue.milestone?.title ?? null
82105
+ }));
82106
+ return mcpSuccess({
82107
+ mode: "full",
82108
+ issues,
82109
+ count: issues.length
82110
+ }, `Found ${issues.length} issues`);
82111
+ } catch (e) {
82112
+ return mcpError(e.message, "Operation failed");
82113
+ }
82114
+ });
82115
+ server.tool("mcp_get_issue_detail", "Get full details of a specific GitHub issue including comments.", { issue_number: numberType().describe("GitHub issue number") }, async ({ issue_number }) => {
82116
+ try {
82117
+ if (await detectGitHubMode() === "local-only") return mcpSuccess({
82118
+ mode: "local-only",
82119
+ warning: "GitHub not configured. Cannot fetch issue details.",
82120
+ issue_number
82121
+ }, "Local-only mode: cannot fetch issue details.");
82122
+ const result = await ghExec([
82123
+ "issue",
82124
+ "view",
82125
+ String(issue_number),
82126
+ "--json",
82127
+ "number,title,body,state,labels,comments,assignees"
82128
+ ], { parseJson: true });
82129
+ if (!result.ok) return mcpError(`Fetch failed: ${result.error}`, "Fetch failed");
82130
+ const issue = result.data;
82131
+ return mcpSuccess({
82132
+ mode: "full",
82133
+ number: issue.number,
82134
+ title: issue.title,
82135
+ body: issue.body,
82136
+ state: issue.state,
82137
+ labels: issue.labels.map((l) => l.name),
82138
+ assignees: issue.assignees.map((a) => a.login),
82139
+ comments: issue.comments.map((c) => ({
82140
+ author: c.author.login,
82141
+ body: c.body,
82142
+ created_at: c.createdAt
82143
+ })),
82144
+ comment_count: issue.comments.length
82145
+ }, `Issue #${issue.number}: ${issue.title} (${issue.state})`);
82146
+ } catch (e) {
82147
+ return mcpError(e.message, "Operation failed");
82148
+ }
82149
+ });
82150
+ server.tool("mcp_set_estimate", "Set Fibonacci story points on a GitHub issue.", {
82151
+ issue_number: numberType().describe("GitHub issue number"),
82152
+ points: numberType().describe("Fibonacci story points (1, 2, 3, 5, 8, 13, 21, 34)")
82153
+ }, async ({ issue_number, points }) => {
82154
+ try {
82155
+ const cwd = detectProjectRoot();
82156
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
82157
+ if (!FIBONACCI_POINTS.includes(points)) return mcpError(`Invalid points: ${points}. Must be one of: ${FIBONACCI_POINTS.join(", ")}`, "Validation failed");
82158
+ if (await detectGitHubMode() === "local-only") return mcpSuccess({
82159
+ mode: "local-only",
82160
+ warning: "GitHub not configured. Cannot set estimate.",
82161
+ issue_number,
82162
+ points
82163
+ }, "Local-only mode: cannot set estimate on GitHub project.");
82164
+ const mapping = loadMapping(cwd);
82165
+ if (!mapping) return mcpError("github-issues.json not found. Run mcp_github_setup first.", "Setup required");
82166
+ if (!mapping.estimate_field_id) return mcpError("Estimate field not configured. Re-run mcp_github_setup.", "Setup required");
82167
+ const issueEntry = findIssueInMapping(mapping, issue_number);
82168
+ if (!issueEntry) return mcpError(`Issue #${issue_number} not found in local mapping`, "Issue not tracked");
82169
+ if (!issueEntry.item_id) return mcpError(`Issue #${issue_number} has no project item_id. It may not have been added to the board.`, "Not on board");
82170
+ const result = await setEstimate(mapping.project_id, issueEntry.item_id, mapping.estimate_field_id, points);
82171
+ if (!result.ok) return mcpError(`Set estimate failed: ${result.error}`, "Estimate failed");
82172
+ return mcpSuccess({
82173
+ mode: "full",
82174
+ issue_number,
82175
+ points,
82176
+ set: true
82177
+ }, `Estimate set: issue #${issue_number} = ${points} points`);
82178
+ } catch (e) {
82179
+ return mcpError(e.message, "Operation failed");
82180
+ }
82181
+ });
82182
+ }
82183
+ /**
82184
+ * Find an issue entry in the mapping file (searches phases and todos).
82185
+ */
82186
+ function findIssueInMapping(mapping, issueNumber) {
82187
+ for (const phase of Object.values(mapping.phases)) {
82188
+ if (phase.tracking_issue.number === issueNumber) return phase.tracking_issue;
82189
+ for (const task of Object.values(phase.tasks)) if (task.number === issueNumber) return task;
82190
+ }
82191
+ if (mapping.todos) {
82192
+ for (const todo of Object.values(mapping.todos)) if (todo.number === issueNumber) return todo;
82193
+ }
82194
+ return null;
82195
+ }
82196
+ /**
82197
+ * Build local board items from the mapping file (for local-only mode).
82198
+ */
82199
+ function buildLocalBoardItems(mapping, statusFilter, phaseFilter) {
82200
+ const items = [];
82201
+ for (const [phaseNum, phase] of Object.entries(mapping.phases)) {
82202
+ if (phaseFilter && phaseNum !== phaseFilter) continue;
82203
+ if (phase.tracking_issue.number > 0) {
82204
+ const entry = {
82205
+ issue_number: phase.tracking_issue.number,
82206
+ title: `[Phase ${phaseNum}] Tracking`,
82207
+ status: phase.tracking_issue.status,
82208
+ source: `phase ${phaseNum}`
82209
+ };
82210
+ if (!statusFilter || entry.status === statusFilter) items.push(entry);
82211
+ }
82212
+ for (const [taskId, task] of Object.entries(phase.tasks)) if (task.number > 0) {
82213
+ const entry = {
82214
+ issue_number: task.number,
82215
+ title: `[P${phaseNum}] Task ${taskId}`,
82216
+ status: task.status,
82217
+ source: `phase ${phaseNum}, task ${taskId}`
82218
+ };
82219
+ if (!statusFilter || entry.status === statusFilter) items.push(entry);
82220
+ }
82221
+ }
82222
+ if (!phaseFilter && mapping.todos) {
82223
+ for (const [todoId, todo] of Object.entries(mapping.todos)) if (todo.number > 0) {
82224
+ const entry = {
82225
+ issue_number: todo.number,
82226
+ title: `Todo: ${todoId}`,
82227
+ status: todo.status,
82228
+ source: `todo ${todoId}`
82229
+ };
82230
+ if (!statusFilter || entry.status === statusFilter) items.push(entry);
82231
+ }
82232
+ }
82233
+ return items;
82234
+ }
82235
+ /**
82236
+ * Build local search results from the mapping file (for local-only mode).
82237
+ */
82238
+ function buildLocalSearchResults(mapping, stateFilter) {
82239
+ const items = [];
82240
+ for (const [phaseNum, phase] of Object.entries(mapping.phases)) for (const [taskId, task] of Object.entries(phase.tasks)) if (task.number > 0) {
82241
+ const state = task.status === "Done" ? "closed" : "open";
82242
+ if (stateFilter && stateFilter !== "all" && state !== stateFilter) continue;
82243
+ items.push({
82244
+ issue_number: task.number,
82245
+ title: `[P${phaseNum}] Task ${taskId}`,
82246
+ state,
82247
+ source: `phase ${phaseNum}`
82248
+ });
82249
+ }
82250
+ if (mapping.todos) {
82251
+ for (const [todoId, todo] of Object.entries(mapping.todos)) if (todo.number > 0) {
82252
+ const state = todo.status === "Done" ? "closed" : "open";
82253
+ if (stateFilter && stateFilter !== "all" && state !== stateFilter) continue;
82254
+ items.push({
82255
+ issue_number: todo.number,
82256
+ title: `Todo: ${todoId}`,
82257
+ state,
82258
+ source: "todo"
82259
+ });
82260
+ }
82261
+ }
82262
+ return items;
82263
+ }
82264
+
79569
82265
  //#endregion
79570
82266
  //#region src/mcp/index.ts
79571
82267
  /**
@@ -79578,6 +82274,8 @@ function registerAllTools(server) {
79578
82274
  registerContextTools(server);
79579
82275
  registerRoadmapTools(server);
79580
82276
  registerConfigTools(server);
82277
+ registerGitHubTools(server);
82278
+ registerBoardTools(server);
79581
82279
  }
79582
82280
 
79583
82281
  //#endregion