maxsimcli 4.9.0 → 4.11.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.
@@ -39,6 +39,7 @@ let child_process = require("child_process");
39
39
  require("node:events");
40
40
  let node_child_process = require("node:child_process");
41
41
  let node_util = require("node:util");
42
+ let node_crypto = require("node:crypto");
42
43
 
43
44
  //#region ../../node_modules/zod/v3/helpers/util.js
44
45
  var util$2;
@@ -38059,9 +38060,9 @@ async function postPlanComment(phaseIssueNumber, planNumber, planContent) {
38059
38060
  /**
38060
38061
  * Close an issue. Optionally post a reason comment before closing.
38061
38062
  *
38062
- * Uses `state_reason: 'completed'` by default.
38063
+ * @param stateReason - 'completed' (default) or 'not_planned' (rollback/cancel)
38063
38064
  */
38064
- async function closeIssue(issueNumber, reason) {
38065
+ async function closeIssue(issueNumber, reason, stateReason = "completed") {
38065
38066
  return withGhResult(async () => {
38066
38067
  const octokit = getOctokit();
38067
38068
  const { owner, repo } = await getRepoInfo();
@@ -38076,7 +38077,7 @@ async function closeIssue(issueNumber, reason) {
38076
38077
  repo,
38077
38078
  issue_number: issueNumber,
38078
38079
  state: "closed",
38079
- state_reason: "completed"
38080
+ state_reason: stateReason
38080
38081
  });
38081
38082
  });
38082
38083
  }
@@ -38098,7 +38099,7 @@ async function reopenIssue(issueNumber) {
38098
38099
  /**
38099
38100
  * Fetch a single issue by number.
38100
38101
  *
38101
- * Returns number, id, title, state, and body.
38102
+ * Returns number, id, title, state, body, updated_at, labels, and comments_url.
38102
38103
  */
38103
38104
  async function getPhaseIssue(phaseIssueNumber) {
38104
38105
  return withGhResult(async () => {
@@ -38109,12 +38110,16 @@ async function getPhaseIssue(phaseIssueNumber) {
38109
38110
  repo,
38110
38111
  issue_number: phaseIssueNumber
38111
38112
  });
38113
+ const labels = response.data.labels.map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean);
38112
38114
  return {
38113
38115
  number: response.data.number,
38114
38116
  id: response.data.id,
38115
38117
  title: response.data.title,
38116
38118
  state: response.data.state,
38117
- body: response.data.body ?? ""
38119
+ body: response.data.body ?? "",
38120
+ updated_at: response.data.updated_at,
38121
+ labels,
38122
+ comments_url: response.data.comments_url
38118
38123
  };
38119
38124
  });
38120
38125
  }
@@ -38122,7 +38127,7 @@ async function getPhaseIssue(phaseIssueNumber) {
38122
38127
  * List all sub-issues of a phase Issue.
38123
38128
  *
38124
38129
  * Uses GitHub's native sub-issues API.
38125
- * Returns each sub-issue's number, id, title, and state.
38130
+ * Returns each sub-issue's number, id, title, state, and updated_at.
38126
38131
  */
38127
38132
  async function listPhaseSubIssues(phaseIssueNumber) {
38128
38133
  return withGhResult(async () => {
@@ -38136,7 +38141,8 @@ async function listPhaseSubIssues(phaseIssueNumber) {
38136
38141
  number: issue.number,
38137
38142
  id: issue.id,
38138
38143
  title: issue.title,
38139
- state: issue.state
38144
+ state: issue.state,
38145
+ updated_at: issue.updated_at ?? ""
38140
38146
  }));
38141
38147
  });
38142
38148
  }
@@ -38258,6 +38264,13 @@ function mappingFilePath(cwd) {
38258
38264
  return planningPath(cwd, MAPPING_FILENAME);
38259
38265
  }
38260
38266
  /**
38267
+ * Compute a SHA-256 hash of an issue body string.
38268
+ * Used for external edit detection (WIRE-06).
38269
+ */
38270
+ function hashBody(body) {
38271
+ return (0, node_crypto.createHash)("sha256").update(body).digest("hex");
38272
+ }
38273
+ /**
38261
38274
  * Load and parse the mapping file (local cache).
38262
38275
  *
38263
38276
  * Returns null if the file does not exist.
@@ -38287,6 +38300,39 @@ function saveMapping(cwd, mapping) {
38287
38300
  node_fs.default.mkdirSync(dir, { recursive: true });
38288
38301
  node_fs.default.writeFileSync(filePath, JSON.stringify(mapping, null, 2) + "\n", "utf-8");
38289
38302
  }
38303
+ /**
38304
+ * Update a specific task's issue mapping within a phase.
38305
+ *
38306
+ * Load-modify-save pattern. Creates phase entry if it does not exist.
38307
+ * Merges partial data with existing entry (if any).
38308
+ *
38309
+ * @throws If mapping file does not exist (must be initialized first via saveMapping)
38310
+ */
38311
+ function updateTaskMapping(cwd, phaseNum, taskId, data) {
38312
+ const mapping = loadMapping(cwd);
38313
+ if (!mapping) throw new Error("github-issues.json does not exist. Run project setup first.");
38314
+ if (!mapping.phases[phaseNum]) mapping.phases[phaseNum] = {
38315
+ tracking_issue: {
38316
+ number: 0,
38317
+ id: 0,
38318
+ node_id: "",
38319
+ item_id: "",
38320
+ status: "To Do"
38321
+ },
38322
+ plan: "",
38323
+ tasks: {}
38324
+ };
38325
+ const existing = mapping.phases[phaseNum].tasks[taskId];
38326
+ const defaults = {
38327
+ number: 0,
38328
+ id: 0,
38329
+ node_id: "",
38330
+ item_id: "",
38331
+ status: "To Do"
38332
+ };
38333
+ mapping.phases[phaseNum].tasks[taskId] = Object.assign(defaults, existing, data);
38334
+ saveMapping(cwd, mapping);
38335
+ }
38290
38336
 
38291
38337
  //#endregion
38292
38338
  //#region src/mcp/utils.ts
@@ -40313,10 +40359,24 @@ function registerGitHubTools(server) {
40313
40359
  }
40314
40360
  const result = await createPhaseIssue(phase_number, phase_name, goal, requirements, success_criteria);
40315
40361
  if (!result.ok) return mcpError(`Phase issue creation failed: ${result.error}`, "Creation failed");
40316
- return mcpSuccess({
40362
+ const responseData = {
40317
40363
  issue_number: result.data.number,
40318
40364
  issue_id: result.data.id
40319
- }, `Created phase issue #${result.data.number}: [Phase ${phase_number}] ${phase_name}`);
40365
+ };
40366
+ const cwd = detectProjectRoot();
40367
+ if (cwd) {
40368
+ const mapping = loadMapping(cwd);
40369
+ if (mapping && mapping.project_number) {
40370
+ const addResult = await addItemToProject(mapping.project_number, result.data.number);
40371
+ if (addResult.ok) {
40372
+ responseData.item_id = addResult.data.itemId;
40373
+ responseData.project_number = mapping.project_number;
40374
+ const moveResult = await moveItemToStatus(mapping.project_number, addResult.data.itemId, "To Do");
40375
+ if (!moveResult.ok) responseData.board_warning = `Added to board but could not set status: ${moveResult.error}`;
40376
+ } else responseData.board_warning = `Issue created but could not add to board: ${addResult.error}`;
40377
+ }
40378
+ }
40379
+ return mcpSuccess(responseData, `Created phase issue #${result.data.number}: [Phase ${phase_number}] ${phase_name}`);
40320
40380
  } catch (e) {
40321
40381
  return mcpError(e.message, "Operation failed");
40322
40382
  }
@@ -40337,11 +40397,24 @@ function registerGitHubTools(server) {
40337
40397
  }
40338
40398
  const result = await createTaskSubIssue(phase_number, task_id, title, body, parent_issue_number);
40339
40399
  if (!result.ok) return mcpError(`Task issue creation failed: ${result.error}`, "Creation failed");
40340
- return mcpSuccess({
40400
+ const responseData = {
40341
40401
  issue_number: result.data.number,
40342
40402
  issue_id: result.data.id,
40343
40403
  parent_issue_number
40344
- }, `Created task sub-issue #${result.data.number}: ${title}`);
40404
+ };
40405
+ const cwd = detectProjectRoot();
40406
+ if (cwd) try {
40407
+ updateTaskMapping(cwd, phase_number, task_id, {
40408
+ number: result.data.number,
40409
+ id: result.data.id,
40410
+ node_id: "",
40411
+ item_id: "",
40412
+ status: "To Do"
40413
+ });
40414
+ } catch (mappingErr) {
40415
+ responseData.mapping_warning = `Issue created but mapping update failed: ${mappingErr.message}`;
40416
+ }
40417
+ return mcpSuccess(responseData, `Created task sub-issue #${result.data.number}: ${title}`);
40345
40418
  } catch (e) {
40346
40419
  return mcpError(e.message, "Operation failed");
40347
40420
  }
@@ -40369,10 +40442,11 @@ function registerGitHubTools(server) {
40369
40442
  return mcpError(e.message, "Operation failed");
40370
40443
  }
40371
40444
  });
40372
- server.tool("mcp_close_issue", "Close a GitHub issue as completed.", {
40445
+ server.tool("mcp_close_issue", "Close a GitHub issue as completed or not planned.", {
40373
40446
  issue_number: numberType().describe("GitHub issue number"),
40374
- reason: stringType().optional().describe("Optional reason comment to post before closing")
40375
- }, async ({ issue_number, reason }) => {
40447
+ reason: stringType().optional().describe("Optional reason comment to post before closing"),
40448
+ state_reason: enumType(["completed", "not_planned"]).optional().default("completed").describe("Close reason: completed (default) or not_planned (rollback)")
40449
+ }, async ({ issue_number, reason, state_reason }) => {
40376
40450
  try {
40377
40451
  try {
40378
40452
  requireAuth();
@@ -40380,12 +40454,13 @@ function registerGitHubTools(server) {
40380
40454
  if (e instanceof AuthError) return mcpAuthError$1(e);
40381
40455
  throw e;
40382
40456
  }
40383
- const result = await closeIssue(issue_number, reason);
40457
+ const result = await closeIssue(issue_number, reason, state_reason);
40384
40458
  if (!result.ok) return mcpError(`Close failed: ${result.error}`, "Close failed");
40385
40459
  return mcpSuccess({
40386
40460
  issue_number,
40387
- closed: true
40388
- }, `Issue #${issue_number} closed`);
40461
+ closed: true,
40462
+ state_reason
40463
+ }, `Issue #${issue_number} closed (${state_reason})`);
40389
40464
  } catch (e) {
40390
40465
  return mcpError(e.message, "Operation failed");
40391
40466
  }
@@ -40424,7 +40499,10 @@ function registerGitHubTools(server) {
40424
40499
  id: issue.id,
40425
40500
  title: issue.title,
40426
40501
  state: issue.state,
40427
- body: issue.body
40502
+ body: issue.body,
40503
+ updated_at: issue.updated_at,
40504
+ labels: issue.labels,
40505
+ comments_url: issue.comments_url
40428
40506
  }, `Issue #${issue.number}: ${issue.title} (${issue.state})`);
40429
40507
  } catch (e) {
40430
40508
  return mcpError(e.message, "Operation failed");
@@ -40501,6 +40579,136 @@ function registerGitHubTools(server) {
40501
40579
  return mcpError(e.message, "Operation failed");
40502
40580
  }
40503
40581
  });
40582
+ server.tool("mcp_post_comment", "Post a comment on a GitHub issue.", {
40583
+ issue_number: numberType().describe("GitHub issue number"),
40584
+ body: stringType().describe("Comment body (markdown)"),
40585
+ type: enumType([
40586
+ "research",
40587
+ "context",
40588
+ "summary",
40589
+ "verification",
40590
+ "uat",
40591
+ "general"
40592
+ ]).optional().describe("Comment type for structured header")
40593
+ }, async ({ issue_number, body, type }) => {
40594
+ try {
40595
+ try {
40596
+ requireAuth();
40597
+ } catch (e) {
40598
+ if (e instanceof AuthError) return mcpAuthError$1(e);
40599
+ throw e;
40600
+ }
40601
+ const result = await postComment(issue_number, type ? `<!-- maxsim:type=${type} -->\n${body}` : body);
40602
+ if (!result.ok) return mcpError(`Comment failed: ${result.error}`, "Comment failed");
40603
+ return mcpSuccess({
40604
+ issue_number,
40605
+ comment_id: result.data.commentId,
40606
+ type: type ?? "general"
40607
+ }, `Comment posted on issue #${issue_number}`);
40608
+ } catch (e) {
40609
+ return mcpError(e.message, "Operation failed");
40610
+ }
40611
+ });
40612
+ server.tool("mcp_batch_create_tasks", "Create multiple task sub-issues for a phase with automatic rollback on failure.", {
40613
+ phase_number: stringType().describe("Phase number (e.g. \"01\")"),
40614
+ parent_issue_number: numberType().describe("Parent phase issue number"),
40615
+ tasks: arrayType(objectType({
40616
+ task_id: stringType().describe("Task ID within the phase"),
40617
+ title: stringType().describe("Task title"),
40618
+ body: stringType().describe("Task body (markdown)")
40619
+ })).describe("List of tasks to create")
40620
+ }, async ({ phase_number, parent_issue_number, tasks }) => {
40621
+ try {
40622
+ try {
40623
+ requireAuth();
40624
+ } catch (e) {
40625
+ if (e instanceof AuthError) return mcpAuthError$1(e);
40626
+ throw e;
40627
+ }
40628
+ const succeeded = [];
40629
+ const failed = [];
40630
+ const created = [];
40631
+ for (const task of tasks) {
40632
+ const result = await createTaskSubIssue(phase_number, task.task_id, task.title, task.body, parent_issue_number);
40633
+ if (result.ok) {
40634
+ created.push({
40635
+ issueNumber: result.data.number,
40636
+ taskId: task.task_id
40637
+ });
40638
+ succeeded.push({
40639
+ task_id: task.task_id,
40640
+ issue_number: result.data.number
40641
+ });
40642
+ const cwd = detectProjectRoot();
40643
+ if (cwd) try {
40644
+ updateTaskMapping(cwd, phase_number, task.task_id, {
40645
+ number: result.data.number,
40646
+ id: result.data.id,
40647
+ node_id: "",
40648
+ item_id: "",
40649
+ status: "To Do"
40650
+ });
40651
+ } catch {}
40652
+ } else {
40653
+ failed.push({
40654
+ task_id: task.task_id,
40655
+ error: result.error
40656
+ });
40657
+ const rolledBack = [];
40658
+ for (const c of [...created].reverse()) if ((await closeIssue(c.issueNumber, "[MAXSIM-ROLLBACK] Partial batch failure", "not_planned")).ok) rolledBack.push(c.issueNumber);
40659
+ return mcpSuccess({
40660
+ succeeded,
40661
+ failed,
40662
+ rolled_back: rolledBack,
40663
+ partial: true
40664
+ }, `Batch failed at task "${task.task_id}". Rolled back ${rolledBack.length} issue(s).`);
40665
+ }
40666
+ }
40667
+ return mcpSuccess({
40668
+ succeeded,
40669
+ failed,
40670
+ rolled_back: [],
40671
+ partial: false
40672
+ }, `Batch created ${succeeded.length} task issue(s) for phase ${phase_number}`);
40673
+ } catch (e) {
40674
+ return mcpError(e.message, "Operation failed");
40675
+ }
40676
+ });
40677
+ server.tool("mcp_detect_external_edits", "Check if a phase issue body was modified outside MAXSIM.", { phase_number: stringType().describe("Phase number (e.g. \"01\")") }, async ({ phase_number }) => {
40678
+ try {
40679
+ try {
40680
+ requireAuth();
40681
+ } catch (e) {
40682
+ if (e instanceof AuthError) return mcpAuthError$1(e);
40683
+ throw e;
40684
+ }
40685
+ const cwd = detectProjectRoot();
40686
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
40687
+ const mapping = loadMapping(cwd);
40688
+ if (!mapping) return mcpError("github-issues.json not found. Run project setup first.", "Mapping missing");
40689
+ const phaseMapping = mapping.phases[phase_number];
40690
+ if (!phaseMapping) return mcpError(`Phase ${phase_number} not found in mapping`, "Phase not found");
40691
+ const storedHash = phaseMapping.body_hash;
40692
+ const issueNumber = phaseMapping.tracking_issue.number;
40693
+ const issueResult = await getPhaseIssue(issueNumber);
40694
+ if (!issueResult.ok) return mcpError(`Failed to fetch issue #${issueNumber}: ${issueResult.error}`, "Fetch failed");
40695
+ const liveHash = hashBody(issueResult.data.body);
40696
+ if (!storedHash) return mcpSuccess({
40697
+ modified: false,
40698
+ phase_number,
40699
+ issue_number: issueNumber,
40700
+ note: "No stored hash — baseline not yet established"
40701
+ }, `Phase ${phase_number}: no baseline hash stored`);
40702
+ const modified = liveHash !== storedHash;
40703
+ return mcpSuccess({
40704
+ modified,
40705
+ phase_number,
40706
+ issue_number: issueNumber
40707
+ }, modified ? `Phase ${phase_number} issue #${issueNumber} has been externally modified` : `Phase ${phase_number} issue #${issueNumber} matches stored hash`);
40708
+ } catch (e) {
40709
+ return mcpError(e.message, "Operation failed");
40710
+ }
40711
+ });
40504
40712
  }
40505
40713
 
40506
40714
  //#endregion
@@ -40586,6 +40794,57 @@ function registerBoardTools(server) {
40586
40794
  return mcpError(e.message, "Operation failed");
40587
40795
  }
40588
40796
  });
40797
+ server.tool("mcp_sync_check", "Verify local github-issues.json mapping is in sync with live GitHub state.", {}, async () => {
40798
+ try {
40799
+ try {
40800
+ requireAuth();
40801
+ } catch (e) {
40802
+ if (e instanceof AuthError) return mcpAuthError(e);
40803
+ throw e;
40804
+ }
40805
+ const cwd = detectProjectRoot();
40806
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
40807
+ const mapping = loadMapping(cwd);
40808
+ if (!mapping) return mcpError("github-issues.json not found. Run project setup first.", "Mapping missing");
40809
+ const mismatches = [];
40810
+ const missing_remote = [];
40811
+ for (const [phaseNum, phaseMapping] of Object.entries(mapping.phases)) {
40812
+ const issueNumber = phaseMapping.tracking_issue.number;
40813
+ if (!issueNumber) continue;
40814
+ const issueResult = await getPhaseIssue(issueNumber);
40815
+ if (!issueResult.ok) {
40816
+ if (issueResult.error.includes("NOT_FOUND") || issueResult.error.includes("404")) missing_remote.push({
40817
+ phase_number: phaseNum,
40818
+ issue_number: issueNumber
40819
+ });
40820
+ continue;
40821
+ }
40822
+ const localStatus = phaseMapping.tracking_issue.status;
40823
+ const remoteState = issueResult.data.state;
40824
+ if (remoteState === "closed" && localStatus !== "Done") mismatches.push({
40825
+ phase_number: phaseNum,
40826
+ issue_number: issueNumber,
40827
+ local_state: localStatus,
40828
+ remote_state: remoteState
40829
+ });
40830
+ else if (remoteState === "open" && localStatus === "Done") mismatches.push({
40831
+ phase_number: phaseNum,
40832
+ issue_number: issueNumber,
40833
+ local_state: localStatus,
40834
+ remote_state: remoteState
40835
+ });
40836
+ }
40837
+ const in_sync = mismatches.length === 0 && missing_remote.length === 0;
40838
+ return mcpSuccess({
40839
+ in_sync,
40840
+ mismatches,
40841
+ missing_local: [],
40842
+ missing_remote
40843
+ }, in_sync ? "Local mapping is in sync with GitHub" : `Sync issues found: ${mismatches.length} mismatch(es), ${missing_remote.length} missing remote`);
40844
+ } catch (e) {
40845
+ return mcpError(e.message, "Operation failed");
40846
+ }
40847
+ });
40589
40848
  server.tool("mcp_search_issues", "Search GitHub issues by label, state, or text query.", {
40590
40849
  labels: arrayType(stringType()).optional().describe("Filter by label names"),
40591
40850
  state: enumType([