sequant 1.14.2 → 1.15.1

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sequant",
3
3
  "description": "Structured workflow system for Claude Code - GitHub issue resolution with spec, exec, test, and QA phases",
4
- "version": "1.14.2",
4
+ "version": "1.15.1",
5
5
  "author": {
6
6
  "name": "admarble",
7
7
  "email": "github@admarble.com"
package/dist/bin/cli.js CHANGED
@@ -43,7 +43,8 @@ import { logsCommand } from "../src/commands/logs.js";
43
43
  import { statsCommand } from "../src/commands/stats.js";
44
44
  import { dashboardCommand } from "../src/commands/dashboard.js";
45
45
  import { stateInitCommand, stateRebuildCommand, stateCleanCommand, } from "../src/commands/state.js";
46
- import { syncCommand } from "../src/commands/sync.js";
46
+ import { syncCommand, areSkillsOutdated } from "../src/commands/sync.js";
47
+ import { getManifest } from "../src/lib/manifest.js";
47
48
  const program = new Command();
48
49
  // Handle --no-color before parsing
49
50
  if (process.argv.includes("--no-color")) {
@@ -191,6 +192,21 @@ stateCmd
191
192
  .option("--max-age <days>", "Remove entries older than N days", parseInt)
192
193
  .option("--all", "Remove all orphaned entries (merged and abandoned)")
193
194
  .action(stateCleanCommand);
195
+ // Auto-sync skills after npm upgrade (version mismatch detection)
196
+ // Only triggers when skills were previously synced (has .sequant-version marker).
197
+ // Projects that manage skills manually (no marker) are not affected.
198
+ program.hook("preAction", async (thisCommand) => {
199
+ const cmd = thisCommand.name();
200
+ if (cmd === "init" || cmd === "sync")
201
+ return;
202
+ const manifest = await getManifest();
203
+ if (!manifest)
204
+ return;
205
+ const { outdated, currentVersion } = await areSkillsOutdated();
206
+ if (outdated && currentVersion !== null) {
207
+ await syncCommand({ quiet: true });
208
+ }
209
+ });
194
210
  // Parse and execute
195
211
  program.parse();
196
212
  // Show help if no command provided
@@ -835,6 +835,37 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
835
835
  };
836
836
  }
837
837
  }
838
+ /**
839
+ * Cold-start retry threshold in seconds.
840
+ * Failures under this duration are likely Claude Code subprocess initialization
841
+ * issues rather than genuine phase failures (based on empirical data: cold-start
842
+ * failures consistently complete in 15-39s vs 150-310s for real work).
843
+ */
844
+ const COLD_START_THRESHOLD_SECONDS = 60;
845
+ const COLD_START_MAX_RETRIES = 2;
846
+ /**
847
+ * Execute a phase with automatic retry for cold-start failures.
848
+ * If a phase fails within COLD_START_THRESHOLD_SECONDS, it's likely a subprocess
849
+ * initialization issue — retry up to COLD_START_MAX_RETRIES times before giving up.
850
+ */
851
+ async function executePhaseWithRetry(issueNumber, phase, config, sessionId, worktreePath, shutdownManager, spinner) {
852
+ let lastResult;
853
+ for (let attempt = 0; attempt <= COLD_START_MAX_RETRIES; attempt++) {
854
+ lastResult = await executePhase(issueNumber, phase, config, sessionId, worktreePath, shutdownManager, spinner);
855
+ const duration = lastResult.durationSeconds ?? 0;
856
+ // Success or genuine failure (took long enough to be real work)
857
+ if (lastResult.success || duration >= COLD_START_THRESHOLD_SECONDS) {
858
+ return lastResult;
859
+ }
860
+ // Cold-start failure detected — retry
861
+ if (attempt < COLD_START_MAX_RETRIES) {
862
+ if (config.verbose) {
863
+ console.log(chalk.yellow(`\n ⟳ Cold-start failure detected (${duration.toFixed(1)}s), retrying... (attempt ${attempt + 2}/${COLD_START_MAX_RETRIES + 1})`));
864
+ }
865
+ }
866
+ }
867
+ return lastResult;
868
+ }
838
869
  /**
839
870
  * Fetch issue info from GitHub
840
871
  */
@@ -1141,6 +1172,12 @@ export async function runCommand(issues, options) {
1141
1172
  noSmartTests: mergedOptions.noSmartTests ?? false,
1142
1173
  mcp: mcpEnabled,
1143
1174
  };
1175
+ // Propagate verbose mode to UI config so spinners use text-only mode.
1176
+ // This prevents animated spinner control characters from colliding with
1177
+ // verbose console.log() calls from StateManager/MetricsWriter (#282).
1178
+ if (config.verbose) {
1179
+ ui.configure({ verbose: true });
1180
+ }
1144
1181
  // Initialize log writer if JSON logging enabled
1145
1182
  // Default: enabled via settings (logJson: true), can be disabled with --no-log
1146
1183
  let logWriter = null;
@@ -1575,7 +1612,7 @@ async function runIssueWithLogging(issueNumber, config, logWriter, stateManager,
1575
1612
  }
1576
1613
  const specStartTime = new Date();
1577
1614
  // Note: spec runs in main repo (not worktree) for planning
1578
- const specResult = await executePhase(issueNumber, "spec", config, sessionId, worktreePath, // Will be ignored for spec (non-isolated phase)
1615
+ const specResult = await executePhaseWithRetry(issueNumber, "spec", config, sessionId, worktreePath, // Will be ignored for spec (non-isolated phase)
1579
1616
  shutdownManager, specSpinner);
1580
1617
  const specEndTime = new Date();
1581
1618
  if (specResult.sessionId) {
@@ -1718,7 +1755,7 @@ async function runIssueWithLogging(issueNumber, config, logWriter, stateManager,
1718
1755
  }
1719
1756
  }
1720
1757
  const phaseStartTime = new Date();
1721
- const result = await executePhase(issueNumber, phase, config, sessionId, worktreePath, shutdownManager, phaseSpinner);
1758
+ const result = await executePhaseWithRetry(issueNumber, phase, config, sessionId, worktreePath, shutdownManager, phaseSpinner);
1722
1759
  const phaseEndTime = new Date();
1723
1760
  // Capture session ID for subsequent phases
1724
1761
  if (result.sessionId) {
@@ -1778,7 +1815,7 @@ async function runIssueWithLogging(issueNumber, config, logWriter, stateManager,
1778
1815
  iteration,
1779
1816
  });
1780
1817
  loopSpinner.start();
1781
- const loopResult = await executePhase(issueNumber, "loop", config, sessionId, worktreePath, shutdownManager, loopSpinner);
1818
+ const loopResult = await executePhaseWithRetry(issueNumber, "loop", config, sessionId, worktreePath, shutdownManager, loopSpinner);
1782
1819
  phaseResults.push(loopResult);
1783
1820
  if (loopResult.sessionId) {
1784
1821
  sessionId = loopResult.sessionId;
@@ -7,7 +7,23 @@ import { getManifest, getPackageVersion } from "../lib/manifest.js";
7
7
  import { fileExists } from "../lib/fs.js";
8
8
  import { readdir } from "fs/promises";
9
9
  import { StateManager } from "../lib/workflow/state-manager.js";
10
- import { rebuildStateFromLogs, cleanupStaleEntries, } from "../lib/workflow/state-utils.js";
10
+ import { rebuildStateFromLogs, cleanupStaleEntries, checkPRMergeStatus, } from "../lib/workflow/state-utils.js";
11
+ /**
12
+ * Auto-detect merged PRs and update state for issues stuck at ready_for_merge.
13
+ * Queries GitHub for each ready_for_merge issue that has a PR number.
14
+ */
15
+ async function refreshMergedStatuses(stateManager, issues) {
16
+ const readyIssues = issues.filter((i) => i.status === "ready_for_merge" && i.pr?.number);
17
+ if (readyIssues.length === 0)
18
+ return;
19
+ for (const issue of readyIssues) {
20
+ const prStatus = checkPRMergeStatus(issue.pr.number);
21
+ if (prStatus === "MERGED") {
22
+ issue.status = "merged";
23
+ await stateManager.updateIssueStatus(issue.number, "merged");
24
+ }
25
+ }
26
+ }
11
27
  /**
12
28
  * Color-code issue status
13
29
  */
@@ -220,7 +236,7 @@ export async function statusCommand(options = {}) {
220
236
  return;
221
237
  }
222
238
  console.log(chalk.green("Status: Initialized"));
223
- console.log(chalk.gray(`Installed version: ${manifest.version}`));
239
+ console.log(chalk.gray(`Skills version: ${manifest.version}`));
224
240
  console.log(chalk.gray(`Stack: ${manifest.stack}`));
225
241
  console.log(chalk.gray(`Installed: ${manifest.installedAt}`));
226
242
  if (manifest.updatedAt) {
@@ -250,6 +266,7 @@ export async function statusCommand(options = {}) {
250
266
  const allIssues = await stateManager.getAllIssueStates();
251
267
  const issues = Object.values(allIssues);
252
268
  if (issues.length > 0) {
269
+ await refreshMergedStatuses(stateManager, issues);
253
270
  displayIssueSummary(issues);
254
271
  }
255
272
  }
@@ -279,6 +296,9 @@ async function displayIssueState(options) {
279
296
  if (options.issue !== undefined) {
280
297
  // Show single issue details
281
298
  const issueState = await stateManager.getIssueState(options.issue);
299
+ if (issueState) {
300
+ await refreshMergedStatuses(stateManager, [issueState]);
301
+ }
282
302
  if (options.json) {
283
303
  console.log(JSON.stringify(issueState, null, 2));
284
304
  }
@@ -294,6 +314,7 @@ async function displayIssueState(options) {
294
314
  // Show all issues
295
315
  const allIssues = await stateManager.getAllIssueStates();
296
316
  const issues = Object.values(allIssues);
317
+ await refreshMergedStatuses(stateManager, issues);
297
318
  if (options.json) {
298
319
  console.log(JSON.stringify({ issues: allIssues }, null, 2));
299
320
  }
@@ -93,9 +93,6 @@ export class MetricsWriter {
93
93
  fs.renameSync(tempPath, this.metricsPath);
94
94
  // Update cache
95
95
  this.cachedMetrics = metrics;
96
- if (this.verbose) {
97
- console.log(`📊 Metrics saved: ${this.metricsPath}`);
98
- }
99
96
  }
100
97
  catch (error) {
101
98
  // Clean up temp file on error
@@ -95,9 +95,6 @@ export class StateManager {
95
95
  fs.renameSync(tempPath, this.statePath);
96
96
  // Update cache
97
97
  this.cachedState = state;
98
- if (this.verbose) {
99
- console.log(`📊 State saved: ${this.statePath}`);
100
- }
101
98
  }
102
99
  catch (error) {
103
100
  // Clean up temp file on error
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sequant",
3
- "version": "1.14.2",
3
+ "version": "1.15.1",
4
4
  "description": "Quantize your development workflow - Sequential AI phases with quality gates",
5
5
  "type": "module",
6
6
  "bin": {