sequant 1.20.2 → 2.0.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 (137) hide show
  1. package/.claude-plugin/marketplace.json +2 -4
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +29 -9
  4. package/dist/bin/cli.js +25 -2
  5. package/dist/src/commands/doctor.js +42 -9
  6. package/dist/src/commands/init.d.ts +1 -0
  7. package/dist/src/commands/init.js +52 -0
  8. package/dist/src/commands/logs.d.ts +1 -0
  9. package/dist/src/commands/logs.js +18 -2
  10. package/dist/src/commands/run.d.ts +7 -0
  11. package/dist/src/commands/run.js +235 -68
  12. package/dist/src/commands/serve.d.ts +13 -0
  13. package/dist/src/commands/serve.js +131 -0
  14. package/dist/src/commands/stats.d.ts +1 -0
  15. package/dist/src/commands/stats.js +185 -26
  16. package/dist/src/commands/status.d.ts +2 -0
  17. package/dist/src/commands/status.js +99 -50
  18. package/dist/src/index.d.ts +2 -2
  19. package/dist/src/index.js +4 -1
  20. package/dist/src/lib/ac-parser.d.ts +2 -0
  21. package/dist/src/lib/ac-parser.js +12 -2
  22. package/dist/src/lib/assess-comment-parser.d.ts +137 -0
  23. package/dist/src/lib/assess-comment-parser.js +344 -0
  24. package/dist/src/lib/ci/config.d.ts +22 -0
  25. package/dist/src/lib/ci/config.js +134 -0
  26. package/dist/src/lib/ci/index.d.ts +12 -0
  27. package/dist/src/lib/ci/index.js +10 -0
  28. package/dist/src/lib/ci/inputs.d.ts +29 -0
  29. package/dist/src/lib/ci/inputs.js +103 -0
  30. package/dist/src/lib/ci/labels.d.ts +34 -0
  31. package/dist/src/lib/ci/labels.js +101 -0
  32. package/dist/src/lib/ci/outputs.d.ts +25 -0
  33. package/dist/src/lib/ci/outputs.js +84 -0
  34. package/dist/src/lib/ci/triggers.d.ts +9 -0
  35. package/dist/src/lib/ci/triggers.js +86 -0
  36. package/dist/src/lib/ci/types.d.ts +131 -0
  37. package/dist/src/lib/ci/types.js +47 -0
  38. package/dist/src/lib/mcp-config.d.ts +54 -0
  39. package/dist/src/lib/mcp-config.js +172 -0
  40. package/dist/src/lib/merge-check/index.js +6 -12
  41. package/dist/src/lib/merge-check/types.d.ts +20 -7
  42. package/dist/src/lib/merge-check/types.js +11 -0
  43. package/dist/src/lib/phase-signal.d.ts +3 -3
  44. package/dist/src/lib/phase-signal.js +5 -3
  45. package/dist/src/lib/settings.d.ts +52 -0
  46. package/dist/src/lib/settings.js +41 -0
  47. package/dist/src/lib/shutdown.d.ts +16 -5
  48. package/dist/src/lib/shutdown.js +32 -12
  49. package/dist/src/lib/solve-comment-parser.d.ts +9 -102
  50. package/dist/src/lib/solve-comment-parser.js +13 -248
  51. package/dist/src/lib/stacks.d.ts +8 -0
  52. package/dist/src/lib/stacks.js +34 -0
  53. package/dist/src/lib/system.js +3 -7
  54. package/dist/src/lib/test-tautology-detector.d.ts +10 -0
  55. package/dist/src/lib/test-tautology-detector.js +43 -4
  56. package/dist/src/lib/upstream/assessment.js +9 -59
  57. package/dist/src/lib/upstream/issues.js +12 -75
  58. package/dist/src/lib/version-check.d.ts +2 -2
  59. package/dist/src/lib/version-check.js +6 -3
  60. package/dist/src/lib/version.d.ts +4 -0
  61. package/dist/src/lib/version.js +25 -0
  62. package/dist/src/lib/workflow/batch-executor.d.ts +18 -86
  63. package/dist/src/lib/workflow/batch-executor.js +232 -55
  64. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +56 -0
  65. package/dist/src/lib/workflow/drivers/agent-driver.js +8 -0
  66. package/dist/src/lib/workflow/drivers/aider.d.ts +18 -0
  67. package/dist/src/lib/workflow/drivers/aider.js +160 -0
  68. package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -0
  69. package/dist/src/lib/workflow/drivers/claude-code.js +165 -0
  70. package/dist/src/lib/workflow/drivers/index.d.ts +20 -0
  71. package/dist/src/lib/workflow/drivers/index.js +27 -0
  72. package/dist/src/lib/workflow/error-classifier.d.ts +16 -0
  73. package/dist/src/lib/workflow/error-classifier.js +90 -0
  74. package/dist/src/lib/workflow/log-writer.d.ts +6 -3
  75. package/dist/src/lib/workflow/log-writer.js +57 -27
  76. package/dist/src/lib/workflow/metrics-schema.d.ts +9 -9
  77. package/dist/src/lib/workflow/phase-detection.d.ts +23 -0
  78. package/dist/src/lib/workflow/phase-detection.js +45 -29
  79. package/dist/src/lib/workflow/phase-executor.d.ts +42 -3
  80. package/dist/src/lib/workflow/phase-executor.js +345 -220
  81. package/dist/src/lib/workflow/phase-mapper.d.ts +1 -1
  82. package/dist/src/lib/workflow/phase-mapper.js +7 -7
  83. package/dist/src/lib/workflow/platforms/github.d.ts +157 -0
  84. package/dist/src/lib/workflow/platforms/github.js +466 -0
  85. package/dist/src/lib/workflow/platforms/index.d.ts +17 -0
  86. package/dist/src/lib/workflow/platforms/index.js +25 -0
  87. package/dist/src/lib/workflow/platforms/platform-provider.d.ts +67 -0
  88. package/dist/src/lib/workflow/platforms/platform-provider.js +8 -0
  89. package/dist/src/lib/workflow/pr-status.d.ts +2 -4
  90. package/dist/src/lib/workflow/pr-status.js +3 -16
  91. package/dist/src/lib/workflow/qa-cache.d.ts +58 -0
  92. package/dist/src/lib/workflow/qa-cache.js +88 -0
  93. package/dist/src/lib/workflow/reconcile.d.ts +69 -0
  94. package/dist/src/lib/workflow/reconcile.js +290 -0
  95. package/dist/src/lib/workflow/ring-buffer.d.ts +17 -0
  96. package/dist/src/lib/workflow/ring-buffer.js +37 -0
  97. package/dist/src/lib/workflow/run-log-schema.d.ts +115 -24
  98. package/dist/src/lib/workflow/run-log-schema.js +47 -12
  99. package/dist/src/lib/workflow/run-reflect.js +1 -1
  100. package/dist/src/lib/workflow/state-cleanup.js +21 -0
  101. package/dist/src/lib/workflow/state-manager.d.ts +34 -3
  102. package/dist/src/lib/workflow/state-manager.js +278 -126
  103. package/dist/src/lib/workflow/state-schema.d.ts +34 -30
  104. package/dist/src/lib/workflow/state-schema.js +35 -25
  105. package/dist/src/lib/workflow/state-utils.d.ts +3 -1
  106. package/dist/src/lib/workflow/state-utils.js +1 -0
  107. package/dist/src/lib/workflow/types.d.ts +208 -6
  108. package/dist/src/lib/workflow/types.js +20 -1
  109. package/dist/src/lib/workflow/worktree-discovery.d.ts +1 -1
  110. package/dist/src/lib/workflow/worktree-discovery.js +6 -14
  111. package/dist/src/lib/workflow/worktree-manager.js +33 -51
  112. package/dist/src/mcp/index.d.ts +4 -0
  113. package/dist/src/mcp/index.js +4 -0
  114. package/dist/src/mcp/resources.d.ts +7 -0
  115. package/dist/src/mcp/resources.js +111 -0
  116. package/dist/src/mcp/run-registry.d.ts +34 -0
  117. package/dist/src/mcp/run-registry.js +42 -0
  118. package/dist/src/mcp/server.d.ts +12 -0
  119. package/dist/src/mcp/server.js +50 -0
  120. package/dist/src/mcp/tools/logs.d.ts +7 -0
  121. package/dist/src/mcp/tools/logs.js +149 -0
  122. package/dist/src/mcp/tools/run.d.ts +121 -0
  123. package/dist/src/mcp/tools/run.js +591 -0
  124. package/dist/src/mcp/tools/status.d.ts +7 -0
  125. package/dist/src/mcp/tools/status.js +127 -0
  126. package/package.json +10 -1
  127. package/templates/hooks/post-tool.sh +19 -8
  128. package/templates/hooks/pre-tool.sh +36 -49
  129. package/templates/mcp.json +6 -0
  130. package/templates/skills/assess/SKILL.md +354 -352
  131. package/templates/skills/exec/SKILL.md +64 -1
  132. package/templates/skills/fullsolve/SKILL.md +35 -4
  133. package/templates/skills/qa/SKILL.md +486 -9
  134. package/templates/skills/qa/scripts/quality-checks.sh +1 -1
  135. package/templates/skills/setup/SKILL.md +386 -0
  136. package/templates/skills/solve/SKILL.md +38 -664
  137. package/templates/skills/spec/SKILL.md +90 -31
@@ -10,6 +10,7 @@ import { existsSync, readFileSync } from "fs";
10
10
  import path from "path";
11
11
  import { PM_CONFIG } from "../stacks.js";
12
12
  import { getResumablePhasesForIssue } from "./phase-detection.js";
13
+ import { GitHubProvider } from "./platforms/github.js";
13
14
  /**
14
15
  * Lockfile names for different package managers
15
16
  */
@@ -797,26 +798,19 @@ export function rebaseBeforePR(worktreePath, issueNumber, packageManager, verbos
797
798
  * @internal Exported for testing
798
799
  */
799
800
  export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose, labels) {
801
+ const github = new GitHubProvider();
800
802
  // Step 1: Check for existing PR on this branch
801
- const existingPR = spawnSync("gh", ["pr", "view", branch, "--json", "number,url"], { stdio: "pipe", cwd: worktreePath, timeout: 15000 });
802
- if (existingPR.status === 0 && existingPR.stdout) {
803
- try {
804
- const prInfo = JSON.parse(existingPR.stdout.toString());
805
- if (prInfo.number && prInfo.url) {
806
- if (verbose) {
807
- console.log(chalk.gray(` ℹ️ PR #${prInfo.number} already exists for branch ${branch}`));
808
- }
809
- return {
810
- attempted: true,
811
- success: true,
812
- prNumber: prInfo.number,
813
- prUrl: prInfo.url,
814
- };
815
- }
816
- }
817
- catch {
818
- // JSON parse failed — no existing PR, continue to create
803
+ const existingPRInfo = github.viewPRByBranchSync(branch, worktreePath);
804
+ if (existingPRInfo) {
805
+ if (verbose) {
806
+ console.log(chalk.gray(` ℹ️ PR #${existingPRInfo.number} already exists for branch ${branch}`));
819
807
  }
808
+ return {
809
+ attempted: true,
810
+ success: true,
811
+ prNumber: existingPRInfo.number,
812
+ prUrl: existingPRInfo.url,
813
+ };
820
814
  }
821
815
  // Step 2: Push branch to remote
822
816
  if (verbose) {
@@ -849,25 +843,19 @@ export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose,
849
843
  `---`,
850
844
  `🤖 Generated by \`sequant run\``,
851
845
  ].join("\n");
852
- const prResult = spawnSync("gh", ["pr", "create", "--title", prTitle, "--body", prBody, "--head", branch], { stdio: "pipe", cwd: worktreePath, timeout: 30000 });
853
- if (prResult.status !== 0) {
854
- const prError = prResult.stderr?.toString().trim() ?? "Unknown error";
846
+ const prResult = github.createPRCliSync(prTitle, prBody, branch, worktreePath);
847
+ if (prResult.exitCode !== 0) {
848
+ const prError = prResult.stderr.trim() || "Unknown error";
855
849
  // Check if PR already exists (race condition or push-before-PR scenarios)
856
850
  if (prError.includes("already exists")) {
857
- const retryView = spawnSync("gh", ["pr", "view", branch, "--json", "number,url"], { stdio: "pipe", cwd: worktreePath, timeout: 15000 });
858
- if (retryView.status === 0 && retryView.stdout) {
859
- try {
860
- const prInfo = JSON.parse(retryView.stdout.toString());
861
- return {
862
- attempted: true,
863
- success: true,
864
- prNumber: prInfo.number,
865
- prUrl: prInfo.url,
866
- };
867
- }
868
- catch {
869
- // Fall through to error
870
- }
851
+ const retryInfo = github.viewPRByBranchSync(branch, worktreePath);
852
+ if (retryInfo) {
853
+ return {
854
+ attempted: true,
855
+ success: true,
856
+ prNumber: retryInfo.number,
857
+ prUrl: retryInfo.url,
858
+ };
871
859
  }
872
860
  }
873
861
  console.log(chalk.yellow(` ⚠️ PR creation failed: ${prError}`));
@@ -878,7 +866,7 @@ export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose,
878
866
  };
879
867
  }
880
868
  // Step 4: Extract PR URL from output and get PR details
881
- const prOutput = prResult.stdout?.toString().trim() ?? "";
869
+ const prOutput = prResult.stdout.trim();
882
870
  const prUrlMatch = prOutput.match(/https:\/\/github\.com\/[^\s]+\/pull\/(\d+)/);
883
871
  if (prUrlMatch) {
884
872
  const prNumber = parseInt(prUrlMatch[1], 10);
@@ -892,21 +880,15 @@ export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose,
892
880
  };
893
881
  }
894
882
  // Fallback: try gh pr view to get details
895
- const viewResult = spawnSync("gh", ["pr", "view", branch, "--json", "number,url"], { stdio: "pipe", cwd: worktreePath, timeout: 15000 });
896
- if (viewResult.status === 0 && viewResult.stdout) {
897
- try {
898
- const prInfo = JSON.parse(viewResult.stdout.toString());
899
- console.log(chalk.green(` ✅ PR #${prInfo.number} created: ${prInfo.url}`));
900
- return {
901
- attempted: true,
902
- success: true,
903
- prNumber: prInfo.number,
904
- prUrl: prInfo.url,
905
- };
906
- }
907
- catch {
908
- // Fall through
909
- }
883
+ const viewInfo = github.viewPRByBranchSync(branch, worktreePath);
884
+ if (viewInfo) {
885
+ console.log(chalk.green(` ✅ PR #${viewInfo.number} created: ${viewInfo.url}`));
886
+ return {
887
+ attempted: true,
888
+ success: true,
889
+ prNumber: viewInfo.number,
890
+ prUrl: viewInfo.url,
891
+ };
910
892
  }
911
893
  // PR was created but we couldn't parse the URL
912
894
  console.log(chalk.yellow(` ⚠️ PR created but could not extract URL from output: ${prOutput}`));
@@ -0,0 +1,4 @@
1
+ /**
2
+ * MCP server public API
3
+ */
4
+ export { createServer } from "./server.js";
@@ -0,0 +1,4 @@
1
+ /**
2
+ * MCP server public API
3
+ */
4
+ export { createServer } from "./server.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * MCP Resources for Sequant
3
+ *
4
+ * Exposes workflow state and configuration as readable resources.
5
+ */
6
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ export declare function registerResources(server: McpServer): void;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * MCP Resources for Sequant
3
+ *
4
+ * Exposes workflow state and configuration as readable resources.
5
+ */
6
+ import * as fs from "fs";
7
+ import { StateManager } from "../lib/workflow/state-manager.js";
8
+ import { SETTINGS_PATH } from "../lib/settings.js";
9
+ export function registerResources(server) {
10
+ // sequant://state — current workflow state
11
+ server.registerResource("state", "sequant://state", {
12
+ description: "Dashboard view of all tracked GitHub issues and their workflow progress. " +
13
+ "Contains per-issue phase status (spec/exec/qa), worktree paths, PR links, and QA verdicts. " +
14
+ "Read this to understand which issues are in-flight before starting new work.",
15
+ mimeType: "application/json",
16
+ }, async () => {
17
+ try {
18
+ const stateManager = new StateManager();
19
+ if (!stateManager.stateExists()) {
20
+ return {
21
+ contents: [
22
+ {
23
+ uri: "sequant://state",
24
+ mimeType: "application/json",
25
+ text: JSON.stringify({ issues: {} }),
26
+ },
27
+ ],
28
+ };
29
+ }
30
+ // Use getAllIssueStates() which applies TTL filtering
31
+ const filteredIssues = await stateManager.getAllIssueStates();
32
+ const state = await stateManager.getState();
33
+ const output = {
34
+ version: state.version,
35
+ lastUpdated: state.lastUpdated,
36
+ lastSynced: state.lastSynced,
37
+ issues: Object.fromEntries(Object.values(filteredIssues).map((v) => [String(v.number), v])),
38
+ };
39
+ return {
40
+ contents: [
41
+ {
42
+ uri: "sequant://state",
43
+ mimeType: "application/json",
44
+ text: JSON.stringify(output, null, 2),
45
+ },
46
+ ],
47
+ };
48
+ }
49
+ catch (error) {
50
+ return {
51
+ contents: [
52
+ {
53
+ uri: "sequant://state",
54
+ mimeType: "application/json",
55
+ text: JSON.stringify({
56
+ error: "Failed to read state",
57
+ message: error instanceof Error ? error.message : String(error),
58
+ }),
59
+ },
60
+ ],
61
+ };
62
+ }
63
+ });
64
+ // sequant://config — current Sequant configuration
65
+ server.registerResource("config", "sequant://config", {
66
+ description: "Current Sequant workflow settings including default phases, timeout limits, " +
67
+ "quality loop configuration, and agent preferences. " +
68
+ "Read this to understand how sequant_run will behave before invoking it.",
69
+ mimeType: "application/json",
70
+ }, async () => {
71
+ try {
72
+ if (!fs.existsSync(SETTINGS_PATH)) {
73
+ return {
74
+ contents: [
75
+ {
76
+ uri: "sequant://config",
77
+ mimeType: "application/json",
78
+ text: JSON.stringify({
79
+ message: "No settings file found. Run `sequant init` to create one.",
80
+ }),
81
+ },
82
+ ],
83
+ };
84
+ }
85
+ const content = fs.readFileSync(SETTINGS_PATH, "utf-8");
86
+ return {
87
+ contents: [
88
+ {
89
+ uri: "sequant://config",
90
+ mimeType: "application/json",
91
+ text: content,
92
+ },
93
+ ],
94
+ };
95
+ }
96
+ catch (error) {
97
+ return {
98
+ contents: [
99
+ {
100
+ uri: "sequant://config",
101
+ mimeType: "application/json",
102
+ text: JSON.stringify({
103
+ error: "Failed to read config",
104
+ message: error instanceof Error ? error.message : String(error),
105
+ }),
106
+ },
107
+ ],
108
+ };
109
+ }
110
+ });
111
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Active run registry for MCP server
3
+ *
4
+ * Tracks which workflow runs are currently active so that
5
+ * sequant_status can report isRunning state in real time.
6
+ *
7
+ * Module-level singleton — all tools in the same process share one registry.
8
+ */
9
+ export interface ActiveRun {
10
+ startedAt: string;
11
+ }
12
+ /**
13
+ * Register an active run for an issue.
14
+ * If a run is already registered for this issue, it is replaced.
15
+ */
16
+ export declare function registerRun(issue: number): void;
17
+ /**
18
+ * Unregister a run for an issue (called on process exit/error/abort).
19
+ */
20
+ export declare function unregisterRun(issue: number): void;
21
+ /**
22
+ * Check if a run is currently active for the given issue.
23
+ * Returns false for untracked issues.
24
+ */
25
+ export declare function isRunning(issue: number): boolean;
26
+ /**
27
+ * Get info about all currently active runs.
28
+ */
29
+ export declare function getActiveRuns(): ReadonlyMap<number, ActiveRun>;
30
+ /**
31
+ * Clear all active runs. Intended for testing only.
32
+ * @internal
33
+ */
34
+ export declare function clearRegistry(): void;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Active run registry for MCP server
3
+ *
4
+ * Tracks which workflow runs are currently active so that
5
+ * sequant_status can report isRunning state in real time.
6
+ *
7
+ * Module-level singleton — all tools in the same process share one registry.
8
+ */
9
+ const activeRuns = new Map();
10
+ /**
11
+ * Register an active run for an issue.
12
+ * If a run is already registered for this issue, it is replaced.
13
+ */
14
+ export function registerRun(issue) {
15
+ activeRuns.set(issue, { startedAt: new Date().toISOString() });
16
+ }
17
+ /**
18
+ * Unregister a run for an issue (called on process exit/error/abort).
19
+ */
20
+ export function unregisterRun(issue) {
21
+ activeRuns.delete(issue);
22
+ }
23
+ /**
24
+ * Check if a run is currently active for the given issue.
25
+ * Returns false for untracked issues.
26
+ */
27
+ export function isRunning(issue) {
28
+ return activeRuns.has(issue);
29
+ }
30
+ /**
31
+ * Get info about all currently active runs.
32
+ */
33
+ export function getActiveRuns() {
34
+ return activeRuns;
35
+ }
36
+ /**
37
+ * Clear all active runs. Intended for testing only.
38
+ * @internal
39
+ */
40
+ export function clearRegistry() {
41
+ activeRuns.clear();
42
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Sequant MCP Server
3
+ *
4
+ * Exposes Sequant workflow orchestration as an MCP server.
5
+ * Any MCP client (Claude Desktop, Cursor, VS Code, etc.) can invoke
6
+ * Sequant tools to drive structured AI workflows.
7
+ */
8
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ /**
10
+ * Create and configure the Sequant MCP server instance.
11
+ */
12
+ export declare function createServer(version: string): McpServer;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Sequant MCP Server
3
+ *
4
+ * Exposes Sequant workflow orchestration as an MCP server.
5
+ * Any MCP client (Claude Desktop, Cursor, VS Code, etc.) can invoke
6
+ * Sequant tools to drive structured AI workflows.
7
+ */
8
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import { registerRunTool } from "./tools/run.js";
10
+ import { registerStatusTool } from "./tools/status.js";
11
+ import { registerLogsTool } from "./tools/logs.js";
12
+ import { registerResources } from "./resources.js";
13
+ /**
14
+ * Create and configure the Sequant MCP server instance.
15
+ */
16
+ export function createServer(version) {
17
+ const options = {
18
+ instructions: [
19
+ "Sequant orchestrates AI-driven development workflows for GitHub issues.",
20
+ "Each issue progresses through phases: spec (plan) → exec (implement) → qa (review).",
21
+ "",
22
+ "Tools:",
23
+ "- sequant_status: Check issue progress. Poll every 5-10s during active runs. Always check before calling sequant_run.",
24
+ "- sequant_run: Execute workflow phases. Long-running (up to 30 min). Returns structured JSON on completion.",
25
+ "- sequant_logs: Review past run results and debug failures.",
26
+ "",
27
+ "Resources:",
28
+ "- sequant://state: Dashboard view of all tracked issues and their phases.",
29
+ "- sequant://config: Current workflow settings (timeout, phases, quality loop).",
30
+ "",
31
+ "Workflow: Check sequant_status first → sequant_run if needed → poll sequant_status → review sequant_logs on failure.",
32
+ "Do NOT call sequant_run for issues that are already merged or completed.",
33
+ ].join("\n"),
34
+ capabilities: {
35
+ tools: {},
36
+ resources: {},
37
+ },
38
+ };
39
+ const server = new McpServer({
40
+ name: "sequant",
41
+ version,
42
+ }, options);
43
+ // Register tools
44
+ registerRunTool(server);
45
+ registerStatusTool(server);
46
+ registerLogsTool(server);
47
+ // Register resources
48
+ registerResources(server);
49
+ return server;
50
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * sequant_logs MCP tool
3
+ *
4
+ * Get run logs and metrics.
5
+ */
6
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ export declare function registerLogsTool(server: McpServer): void;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * sequant_logs MCP tool
3
+ *
4
+ * Get run logs and metrics.
5
+ */
6
+ import { z } from "zod";
7
+ import * as fs from "fs";
8
+ import * as path from "path";
9
+ import * as os from "os";
10
+ import { LOG_PATHS, RunLogSchema } from "../../lib/workflow/run-log-schema.js";
11
+ function resolveLogPath() {
12
+ const projectPath = LOG_PATHS.project;
13
+ if (fs.existsSync(projectPath)) {
14
+ return projectPath;
15
+ }
16
+ const userPath = LOG_PATHS.user.replace("~", os.homedir());
17
+ if (fs.existsSync(userPath)) {
18
+ return userPath;
19
+ }
20
+ return projectPath;
21
+ }
22
+ function parseLogFile(filePath) {
23
+ try {
24
+ const content = fs.readFileSync(filePath, "utf-8");
25
+ const data = JSON.parse(content);
26
+ return RunLogSchema.parse(data);
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ export function registerLogsTool(server) {
33
+ server.registerTool("sequant_logs", {
34
+ title: "Sequant Logs",
35
+ description: "Get structured run logs for recent workflow executions. " +
36
+ "Each log contains per-issue phase results (duration, status, errors) and QA verdicts. " +
37
+ "Use after a sequant_run completes or fails to understand what happened. " +
38
+ "Logs are stored as run-<ISO-timestamp>-<uuid>.json files.",
39
+ annotations: {
40
+ readOnlyHint: true,
41
+ idempotentHint: true,
42
+ },
43
+ inputSchema: {
44
+ runId: z
45
+ .string()
46
+ .optional()
47
+ .describe("Specific run ID prefix to filter by (e.g. 'run-2026-03-24'). " +
48
+ "Omit to return the most recent runs."),
49
+ limit: z
50
+ .number()
51
+ .optional()
52
+ .describe("Number of recent runs to return (default: 5, max: all available)"),
53
+ },
54
+ }, async ({ runId, limit }) => {
55
+ try {
56
+ const logDir = resolveLogPath();
57
+ if (!fs.existsSync(logDir)) {
58
+ return {
59
+ content: [
60
+ {
61
+ type: "text",
62
+ text: JSON.stringify({
63
+ logs: [],
64
+ message: "No log directory found. Run `sequant run` to generate logs.",
65
+ }),
66
+ },
67
+ ],
68
+ };
69
+ }
70
+ const logFiles = fs
71
+ .readdirSync(logDir)
72
+ .filter((f) => f.startsWith("run-") && f.endsWith(".json"))
73
+ .sort()
74
+ .reverse();
75
+ if (logFiles.length === 0) {
76
+ return {
77
+ content: [
78
+ {
79
+ type: "text",
80
+ text: JSON.stringify({
81
+ logs: [],
82
+ message: "No logs found.",
83
+ }),
84
+ },
85
+ ],
86
+ };
87
+ }
88
+ let logs = [];
89
+ for (const filename of logFiles) {
90
+ const filePath = path.join(logDir, filename);
91
+ const log = parseLogFile(filePath);
92
+ if (log) {
93
+ // Filter by runId if specified
94
+ if (runId && !log.runId.startsWith(runId)) {
95
+ continue;
96
+ }
97
+ logs.push(log);
98
+ }
99
+ }
100
+ // Apply limit
101
+ const maxResults = limit && limit > 0 ? limit : 5;
102
+ logs = logs.slice(0, maxResults);
103
+ return {
104
+ content: [
105
+ {
106
+ type: "text",
107
+ text: JSON.stringify({
108
+ count: logs.length,
109
+ logs: logs.map((log) => ({
110
+ runId: log.runId,
111
+ startTime: log.startTime,
112
+ config: log.config,
113
+ summary: log.summary,
114
+ issues: log.issues.map((issue) => ({
115
+ issueNumber: issue.issueNumber,
116
+ title: issue.title,
117
+ status: issue.status,
118
+ totalDurationSeconds: issue.totalDurationSeconds,
119
+ phases: issue.phases.map((p) => ({
120
+ phase: p.phase,
121
+ status: p.status,
122
+ durationSeconds: p.durationSeconds,
123
+ error: p.error,
124
+ ...(p.verdict && { verdict: p.verdict }),
125
+ ...(p.summary && { summary: p.summary }),
126
+ })),
127
+ })),
128
+ })),
129
+ }),
130
+ },
131
+ ],
132
+ };
133
+ }
134
+ catch (error) {
135
+ return {
136
+ content: [
137
+ {
138
+ type: "text",
139
+ text: JSON.stringify({
140
+ error: "LOGS_ERROR",
141
+ message: error instanceof Error ? error.message : String(error),
142
+ }),
143
+ },
144
+ ],
145
+ isError: true,
146
+ };
147
+ }
148
+ });
149
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * sequant_run MCP tool
3
+ *
4
+ * Execute workflow phases for GitHub issues.
5
+ * Returns structured JSON with per-issue summaries parsed from run logs.
6
+ * Uses async spawn to keep the MCP server responsive during execution.
7
+ */
8
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import type { RunLog } from "../../lib/workflow/run-log-schema.js";
10
+ /**
11
+ * Per-issue summary in the structured response
12
+ */
13
+ interface RunToolIssueSummary {
14
+ issueNumber: number;
15
+ status: "success" | "failure" | "partial";
16
+ phases: Array<{
17
+ phase: string;
18
+ status: string;
19
+ durationSeconds: number;
20
+ }>;
21
+ verdict?: string;
22
+ durationSeconds: number;
23
+ }
24
+ /**
25
+ * Structured response from sequant_run
26
+ */
27
+ interface RunToolResponse {
28
+ status: "success" | "failure";
29
+ exitCode?: number;
30
+ issues: RunToolIssueSummary[];
31
+ summary: {
32
+ total: number;
33
+ passed: number;
34
+ failed: number;
35
+ durationSeconds: number;
36
+ };
37
+ phases: string;
38
+ rawOutput?: string;
39
+ error?: string;
40
+ }
41
+ /**
42
+ * Resolve the CLI binary path to avoid nested npx version mismatches (#389).
43
+ *
44
+ * Priority:
45
+ * 1. process.argv[1] — the script currently running (works for npx, global, local node)
46
+ * 2. __dirname-relative resolution — fallback for bundled/compiled entry points
47
+ * 3. "npx" + "sequant" — last resort if nothing else resolves
48
+ *
49
+ * Returns [command, prefixArgs] where the full invocation is:
50
+ * spawnAsync(command, [...prefixArgs, "run", ...userArgs])
51
+ */
52
+ export declare function resolveCliBinary(): [string, string[]];
53
+ /**
54
+ * Find and parse the most recent run log file.
55
+ *
56
+ * When runStartTime is provided, only log files created within
57
+ * MAX_LOG_AGE_MS of that timestamp are considered, preventing
58
+ * stale logs from a previous run being returned.
59
+ */
60
+ export declare function readLatestRunLog(runStartTime?: Date): Promise<RunLog | null>;
61
+ /**
62
+ * Build a structured response from a parsed RunLog
63
+ */
64
+ export declare function buildStructuredResponse(runLog: RunLog, rawOutput: string, overallStatus: "success" | "failure", exitCode?: number | null, errorOutput?: string): RunToolResponse;
65
+ /** Parsed progress event from a SEQUANT_PROGRESS line. */
66
+ export interface ProgressEvent {
67
+ issue: number;
68
+ phase: string;
69
+ event: "start" | "complete" | "failed";
70
+ durationSeconds?: number;
71
+ error?: string;
72
+ }
73
+ /**
74
+ * Parse a SEQUANT_PROGRESS line emitted by the batch executor.
75
+ * Returns the parsed event or null if the line isn't a progress line.
76
+ */
77
+ export declare function parseProgressLine(line: string): ProgressEvent | null;
78
+ /**
79
+ * Build a human-readable message for a progress notification (AC-3).
80
+ * @internal Exported for testing only.
81
+ */
82
+ export declare function formatProgressMessage(event: ProgressEvent): string;
83
+ /**
84
+ * Create a line buffer that accumulates stream chunks and yields complete lines.
85
+ * Handles the case where a single `data` event spans partial lines.
86
+ */
87
+ export declare function createLineBuffer(onLine: (line: string) => void): (chunk: string) => void;
88
+ export declare function registerRunTool(server: McpServer): void;
89
+ export interface SpawnResult {
90
+ exitCode: number | null;
91
+ stdout: string;
92
+ stderr: string;
93
+ }
94
+ /** Per-phase timeout ceiling (30 minutes) */
95
+ export declare const PHASE_TIMEOUT = 1800000;
96
+ /** Absolute maximum run duration (2 hours), even with progress resets */
97
+ export declare const MAX_TOTAL_TIMEOUT = 7200000;
98
+ export interface SpawnOptions {
99
+ timeout: number;
100
+ env?: NodeJS.ProcessEnv;
101
+ signal?: AbortSignal;
102
+ /** Called with each stderr chunk (real-time, before process exits) */
103
+ onStderr?: (chunk: string) => void;
104
+ /** Called with each stdout chunk (real-time, before process exits) */
105
+ onStdout?: (chunk: string) => void;
106
+ /**
107
+ * Called when a progress event is detected from stderr.
108
+ * When set, enables resettable timeout: the per-phase timeout resets on
109
+ * each SEQUANT_PROGRESS line, but the absolute ceiling still applies.
110
+ */
111
+ onProgress?: () => void;
112
+ /**
113
+ * Override the absolute timeout ceiling (defaults to MAX_TOTAL_TIMEOUT).
114
+ * Only applies when onProgress is set. Useful for testing.
115
+ * @internal
116
+ */
117
+ maxTotalTimeout?: number;
118
+ }
119
+ /** @internal Exported for testing only */
120
+ export declare function spawnAsync(command: string, args: string[], options: SpawnOptions): Promise<SpawnResult>;
121
+ export {};