sequant 2.8.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +9 -1
  4. package/dist/bin/cli.js +2 -1
  5. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  6. package/dist/marketplace/external_plugins/sequant/README.md +2 -0
  7. package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +18 -3
  8. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +330 -57
  9. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +96 -15
  10. package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +9 -6
  11. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +1 -1
  12. package/dist/marketplace/external_plugins/sequant/skills/reflect/SKILL.md +27 -13
  13. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/documentation-tiers.md +80 -68
  14. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/phase-reflection.md +31 -15
  15. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +10 -2
  16. package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +1 -1
  17. package/dist/src/commands/logs.js +6 -1
  18. package/dist/src/commands/run-display.d.ts +20 -0
  19. package/dist/src/commands/run-display.js +80 -1
  20. package/dist/src/commands/stats.js +47 -0
  21. package/dist/src/lib/assess-collision-detect.d.ts +19 -2
  22. package/dist/src/lib/assess-collision-detect.js +68 -4
  23. package/dist/src/lib/cli-ui/run-renderer.js +17 -9
  24. package/dist/src/lib/errors.d.ts +6 -0
  25. package/dist/src/lib/errors.js +9 -2
  26. package/dist/src/lib/manifest.js +1 -17
  27. package/dist/src/lib/version-check.js +1 -5
  28. package/dist/src/lib/workflow/batch-executor.d.ts +13 -0
  29. package/dist/src/lib/workflow/batch-executor.js +81 -18
  30. package/dist/src/lib/workflow/chain-preflight.d.ts +89 -0
  31. package/dist/src/lib/workflow/chain-preflight.js +199 -0
  32. package/dist/src/lib/workflow/chain-resume.d.ts +116 -0
  33. package/dist/src/lib/workflow/chain-resume.js +166 -0
  34. package/dist/src/lib/workflow/dependency-markers.d.ts +29 -0
  35. package/dist/src/lib/workflow/dependency-markers.js +79 -0
  36. package/dist/src/lib/workflow/drivers/claude-code.d.ts +7 -0
  37. package/dist/src/lib/workflow/drivers/claude-code.js +30 -6
  38. package/dist/src/lib/workflow/error-classifier.d.ts +9 -2
  39. package/dist/src/lib/workflow/error-classifier.js +14 -1
  40. package/dist/src/lib/workflow/log-writer.js +6 -8
  41. package/dist/src/lib/workflow/metrics-schema.d.ts +39 -0
  42. package/dist/src/lib/workflow/metrics-schema.js +16 -0
  43. package/dist/src/lib/workflow/metrics-writer.d.ts +2 -1
  44. package/dist/src/lib/workflow/phase-executor.d.ts +32 -0
  45. package/dist/src/lib/workflow/phase-executor.js +77 -5
  46. package/dist/src/lib/workflow/run-log-schema.d.ts +23 -0
  47. package/dist/src/lib/workflow/run-log-schema.js +45 -1
  48. package/dist/src/lib/workflow/run-orchestrator.d.ts +14 -0
  49. package/dist/src/lib/workflow/run-orchestrator.js +291 -30
  50. package/dist/src/lib/workflow/status-derivation.d.ts +30 -0
  51. package/dist/src/lib/workflow/status-derivation.js +27 -0
  52. package/dist/src/lib/workflow/types.d.ts +23 -0
  53. package/dist/src/lib/workflow/worktree-manager.d.ts +43 -1
  54. package/dist/src/lib/workflow/worktree-manager.js +103 -33
  55. package/dist/src/mcp/tools/run.d.ts +2 -0
  56. package/dist/src/mcp/tools/run.js +2 -0
  57. package/package.json +2 -4
  58. package/templates/hooks/post-tool.sh +18 -3
  59. package/templates/hooks/pre-tool.sh +330 -57
  60. package/templates/scripts/cleanup-worktree.sh +103 -14
  61. package/templates/skills/assess/SKILL.md +96 -15
  62. package/templates/skills/assess/references/predicted-collision-detection.md +9 -6
  63. package/templates/skills/fullsolve/SKILL.md +1 -1
  64. package/templates/skills/reflect/SKILL.md +27 -13
  65. package/templates/skills/reflect/references/documentation-tiers.md +80 -68
  66. package/templates/skills/reflect/references/phase-reflection.md +31 -15
  67. package/templates/skills/release/SKILL.md +10 -2
  68. package/templates/skills/spec/references/verification-criteria.md +1 -1
@@ -365,21 +365,14 @@ export async function ensureWorktree(issueNumber, title, verbose, packageManager
365
365
  if (verbose) {
366
366
  console.log(chalk.gray(` Rebasing existing worktree onto chain base (${baseBranch})...`));
367
367
  }
368
- const rebaseResult = spawnSync("git", ["-C", existingPath, "rebase", baseBranch], { stdio: "pipe" });
369
- if (rebaseResult.status !== 0) {
370
- const rebaseError = rebaseResult.stderr.toString();
371
- // Check if it's a conflict
372
- if (rebaseError.includes("CONFLICT") ||
373
- rebaseError.includes("could not apply")) {
368
+ const rebase = rebaseOntoLocalBranch(existingPath, baseBranch, verbose);
369
+ if (!rebase.success) {
370
+ if (rebase.conflict) {
374
371
  console.log(chalk.yellow(` ! Rebase conflict detected. Aborting rebase and keeping original branch state.`));
375
372
  console.log(chalk.yellow(` ℹ️ Branch ${branch} is not properly chained. Manual rebase may be required.`));
376
- // Abort the rebase to restore branch state
377
- spawnSync("git", ["-C", existingPath, "rebase", "--abort"], {
378
- stdio: "pipe",
379
- });
380
373
  }
381
374
  else {
382
- console.log(chalk.yellow(` ! Rebase failed: ${rebaseError.trim()}`));
375
+ console.log(chalk.yellow(` ! Rebase failed: ${rebase.error ?? ""}`));
383
376
  console.log(chalk.yellow(` ℹ️ Continuing with branch in its original state.`));
384
377
  }
385
378
  return {
@@ -390,9 +383,6 @@ export async function ensureWorktree(issueNumber, title, verbose, packageManager
390
383
  rebased: false,
391
384
  };
392
385
  }
393
- if (verbose) {
394
- console.log(chalk.green(` ✔ Existing worktree rebased onto ${baseBranch}`));
395
- }
396
386
  return {
397
387
  issue: issueNumber,
398
388
  path: existingPath,
@@ -700,9 +690,10 @@ passed QA in chain mode. It serves as a recovery point if later issues fail.`;
700
690
  const commitResult = spawnSync("git", ["-C", worktreePath, "commit", "-m", commitMessage], { stdio: "pipe" });
701
691
  if (commitResult.status !== 0) {
702
692
  const error = commitResult.stderr.toString();
703
- if (verbose) {
704
- console.log(chalk.yellow(` ! Could not create checkpoint commit: ${error}`));
705
- }
693
+ // #760: chain resume rebases the next link onto this checkpoint, so a
694
+ // commit failure is surfaced regardless of --verbose (the call site adds
695
+ // the resume-impact warning; here we show the underlying git error).
696
+ console.log(chalk.yellow(` ! Could not create checkpoint commit for #${issueNumber}: ${error.trim()}`));
706
697
  return false;
707
698
  }
708
699
  console.log(chalk.green(` 📌 Checkpoint commit created for #${issueNumber}`));
@@ -766,6 +757,39 @@ export function reinstallIfLockfileChanged(worktreePath, packageManager, verbose
766
757
  console.log(chalk.green(` ✔ Dependencies reinstalled`));
767
758
  return true;
768
759
  }
760
+ /**
761
+ * Rebase a worktree's branch onto a *local* branch ref (e.g. a chain
762
+ * predecessor's feature branch), NOT origin/main. Used to chain a successor
763
+ * onto its predecessor's committed work at execution time (#748), and to
764
+ * re-chain a pre-existing worktree onto its chain base.
765
+ *
766
+ * On conflict, aborts the rebase to restore the original branch state and
767
+ * returns `{ success: false, conflict: true }` so callers can warn rather than
768
+ * silently treat a broken link as healthy.
769
+ */
770
+ export function rebaseOntoLocalBranch(worktreePath, ontoBranch, verbose = false) {
771
+ const rebaseResult = spawnSync("git", ["-C", worktreePath, "rebase", ontoBranch], { stdio: "pipe" });
772
+ if (rebaseResult.status === 0) {
773
+ if (verbose) {
774
+ console.log(chalk.green(` ✔ Rebased worktree onto ${ontoBranch}`));
775
+ }
776
+ return { performed: true, success: true, conflict: false };
777
+ }
778
+ const rebaseError = rebaseResult.stderr.toString();
779
+ const isConflict = rebaseError.includes("CONFLICT") || rebaseError.includes("could not apply");
780
+ if (isConflict) {
781
+ // Abort to restore the original branch state.
782
+ spawnSync("git", ["-C", worktreePath, "rebase", "--abort"], {
783
+ stdio: "pipe",
784
+ });
785
+ }
786
+ return {
787
+ performed: true,
788
+ success: false,
789
+ conflict: isConflict,
790
+ error: rebaseError.trim(),
791
+ };
792
+ }
769
793
  /**
770
794
  * Rebase the worktree branch onto the base branch before PR creation.
771
795
  * This ensures the branch is up-to-date and prevents lockfile drift.
@@ -832,6 +856,63 @@ export function rebaseBeforePR(worktreePath, issueNumber, packageManager, verbos
832
856
  reinstalled,
833
857
  };
834
858
  }
859
+ /**
860
+ * Render a non-A+ QA verdict as a PR-body note, or `null` for an A+ / absent
861
+ * verdict.
862
+ *
863
+ * Only stopping states that still break to a PR reach this path
864
+ * (`AC_MET_BUT_NOT_A_PLUS`, `NEEDS_VERIFICATION`); `READY_FOR_MERGE` and an
865
+ * absent verdict produce no note. (#749)
866
+ */
867
+ function qaVerdictNote(verdict) {
868
+ if (!verdict || verdict === "READY_FOR_MERGE")
869
+ return null;
870
+ switch (verdict) {
871
+ case "AC_MET_BUT_NOT_A_PLUS":
872
+ return `> **QA verdict: AC_MET_BUT_NOT_A_PLUS** — acceptance criteria met, but not A+. The run broke to this PR for review rather than entering the quality loop; see the QA review comment for improvement notes.`;
873
+ case "NEEDS_VERIFICATION":
874
+ return `> **QA verdict: NEEDS_VERIFICATION** — acceptance criteria met, pending external verification (e.g. CI or a manual test). See the QA review comment.`;
875
+ default:
876
+ return `> **QA verdict: ${verdict}** — see the QA review comment.`;
877
+ }
878
+ }
879
+ /**
880
+ * Build the automated PR body for `sequant run`.
881
+ *
882
+ * Pure and I/O-free, kept separate from {@link createPR} (which shells out to
883
+ * `gh`) so the body — including the non-A+ QA note (#749) — is unit-testable
884
+ * directly.
885
+ *
886
+ * @param issueNumber Issue the PR closes
887
+ * @param opts.stackManifest Appended before the trailer under --stacked (#605)
888
+ * @param opts.qaVerdict QA verdict for the run; a non-A+ stopping state
889
+ * (anything other than `READY_FOR_MERGE`) surfaces a note so a human reviewer
890
+ * sees why the run broke to PR without reaching A+ (#749).
891
+ * @internal Exported for testing
892
+ */
893
+ export function buildAutomatedPRBody(issueNumber, opts) {
894
+ const bodyLines = [
895
+ `## Summary`,
896
+ ``,
897
+ `Automated PR for issue #${issueNumber}.`,
898
+ ``,
899
+ `Fixes #${issueNumber}`,
900
+ ``,
901
+ ];
902
+ // #749: surface a non-A+ QA verdict in the PR body (not just the run log) so
903
+ // a reviewer sees why the run broke to PR rather than reaching A+.
904
+ const note = qaVerdictNote(opts?.qaVerdict);
905
+ if (note) {
906
+ bodyLines.push(note, ``);
907
+ }
908
+ // #605 AC-4: emit stack manifest before the trailer so reviewers see the
909
+ // chain at the top of the body. Manifest is only set under --stacked.
910
+ if (opts?.stackManifest) {
911
+ bodyLines.push(opts.stackManifest, ``);
912
+ }
913
+ bodyLines.push(`---`, `🤖 Generated by \`sequant run\``);
914
+ return bodyLines.join("\n");
915
+ }
835
916
  /**
836
917
  * Push branch and create a PR after successful QA.
837
918
  *
@@ -850,7 +931,7 @@ export function rebaseBeforePR(worktreePath, issueNumber, packageManager, verbos
850
931
  * @returns PRCreationResult with PR info or error
851
932
  * @internal Exported for testing
852
933
  */
853
- export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose, labels, stackOptions) {
934
+ export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose, labels, stackOptions, qaVerdict) {
854
935
  const github = new GitHubProvider();
855
936
  // Step 1: Check for existing PR on this branch
856
937
  const existingPRInfo = github.viewPRByBranchSync(branch, worktreePath);
@@ -886,21 +967,10 @@ export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose,
886
967
  const isBug = labels?.some((l) => /^bug/i.test(l));
887
968
  const prefix = isBug ? "fix" : "feat";
888
969
  const prTitle = `${prefix}(#${issueNumber}): ${issueTitle}`;
889
- const bodyLines = [
890
- `## Summary`,
891
- ``,
892
- `Automated PR for issue #${issueNumber}.`,
893
- ``,
894
- `Fixes #${issueNumber}`,
895
- ``,
896
- ];
897
- // #605 AC-4: emit stack manifest before the trailer so reviewers see the
898
- // chain at the top of the body. Manifest is only set under --stacked.
899
- if (stackOptions?.stackManifest) {
900
- bodyLines.push(stackOptions.stackManifest, ``);
901
- }
902
- bodyLines.push(`---`, `🤖 Generated by \`sequant run\``);
903
- const prBody = bodyLines.join("\n");
970
+ const prBody = buildAutomatedPRBody(issueNumber, {
971
+ stackManifest: stackOptions?.stackManifest,
972
+ qaVerdict,
973
+ });
904
974
  const prResult = github.createPRCliSync(prTitle, prBody, branch, worktreePath, stackOptions?.prBase);
905
975
  if (prResult.exitCode !== 0) {
906
976
  const prError = prResult.stderr.trim() || "Unknown error";
@@ -32,6 +32,8 @@ interface RunToolResponse {
32
32
  total: number;
33
33
  passed: number;
34
34
  failed: number;
35
+ /** Issues that ended `partial` — timed out, no failure, no recovery (#766). */
36
+ partial: number;
35
37
  durationSeconds: number;
36
38
  };
37
39
  phases: string;
@@ -182,6 +182,7 @@ export function buildStructuredResponse(runLog, rawOutput, overallStatus, exitCo
182
182
  total: runLog.summary.totalIssues,
183
183
  passed: runLog.summary.passed,
184
184
  failed: runLog.summary.failed,
185
+ partial: runLog.summary.partial ?? 0,
185
186
  durationSeconds: runLog.summary.totalDurationSeconds,
186
187
  },
187
188
  phases: phasesRan || runLog.config.phases.join(","),
@@ -232,6 +233,7 @@ function buildFallbackResponse(stdout, issueNumbers, overallStatus, phases, exit
232
233
  total: issueNumbers.length,
233
234
  passed: overallStatus === "success" ? issueNumbers.length : 0,
234
235
  failed: overallStatus === "failure" ? issueNumbers.length : 0,
236
+ partial: 0,
235
237
  durationSeconds: 0,
236
238
  },
237
239
  phases,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sequant",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "description": "AI coding agent orchestrator — resolve GitHub issues end-to-end with isolated git worktrees, quality gates, and an MCP server. Works with Claude Code or Aider.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -97,7 +97,6 @@
97
97
  "cli-table3": "^0.6.5",
98
98
  "commander": "^14.0.3",
99
99
  "diff": "^9.0.0",
100
- "gradient-string": "^3.0.0",
101
100
  "hono": "^4.12.1",
102
101
  "ink": "^7.0.1",
103
102
  "inquirer": "^14.0.1",
@@ -112,9 +111,8 @@
112
111
  },
113
112
  "devDependencies": {
114
113
  "@eslint/js": "^10.0.1",
115
- "@types/gradient-string": "^1.1.6",
116
114
  "@types/inquirer": "^9.0.7",
117
- "@types/node": "^25.4.0",
115
+ "@types/node": "^26.0.1",
118
116
  "@types/react": "^19.2.14",
119
117
  "@typescript-eslint/eslint-plugin": "^8.58.0",
120
118
  "@typescript-eslint/parser": "^8.58.0",
@@ -51,13 +51,28 @@ fi
51
51
 
52
52
  _TMPDIR="${TMPDIR:-/tmp}"
53
53
 
54
- # Use CLAUDE_PLUGIN_DATA for persistent logs (survives plugin updates)
55
- if [[ -n "${CLAUDE_PLUGIN_DATA}" ]]; then
54
+ # Log sink (#763 AC-5c: blocked-command history must survive to be a useful
55
+ # regression corpus, so $TMPDIR which macOS purges — is a last resort only).
56
+ #
57
+ # This block MUST stay identical to the one in pre-tool.sh. The two hooks
58
+ # write the *same* claude-timing.log (START there, END here) and the same
59
+ # claude-quality.log, so any divergence silently splits every START/END pair
60
+ # across two files.
61
+ #
62
+ # Every candidate is absolute and lives outside the repo. A repo-local or
63
+ # relative path is wrong three times over: it resolves against the hook's cwd
64
+ # (which Claude Code does not pin), it yields one sink per directory instead
65
+ # of the single corpus AC-5 asks for, and — worst — creating it inside a repo
66
+ # makes `git status --porcelain` non-empty, which silently defeats pre-tool.sh's
67
+ # no-changes guard that reads exactly that output.
68
+ if [[ -n "${CLAUDE_PLUGIN_DATA:-}" ]]; then
56
69
  _LOG_DIR="${CLAUDE_PLUGIN_DATA}/logs"
57
- mkdir -p "$_LOG_DIR"
70
+ elif [[ -n "${HOME:-}" ]]; then
71
+ _LOG_DIR="${HOME}/.sequant/logs"
58
72
  else
59
73
  _LOG_DIR="${_TMPDIR}"
60
74
  fi
75
+ mkdir -p "$_LOG_DIR" 2>/dev/null || _LOG_DIR="${_TMPDIR}"
61
76
 
62
77
  TIMING_LOG="${_LOG_DIR}/claude-timing.log"
63
78
  QUALITY_LOG="${_LOG_DIR}/claude-quality.log"