sequant 2.9.0 → 2.10.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 (188) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +14 -3
  4. package/dist/bin/cli.js +97 -17
  5. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  6. package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
  7. package/dist/marketplace/external_plugins/sequant/README.md +56 -2
  8. package/dist/marketplace/external_plugins/sequant/hooks/parallel-marker.sh +50 -0
  9. package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +20 -7
  10. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +75 -10
  11. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/trust-model.md +18 -0
  12. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +215 -390
  13. package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +1 -1
  14. package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +4 -4
  15. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +26 -6
  16. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +3 -1
  17. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +38 -2
  18. package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +2 -2
  19. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +337 -57
  20. package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +6 -6
  21. package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +2 -2
  22. package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +2 -2
  23. package/dist/marketplace/external_plugins/sequant/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  24. package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +21 -7
  25. package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +1 -1
  26. package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +11 -11
  27. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +1 -1
  28. package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
  29. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
  30. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
  31. package/dist/src/commands/assess-render.d.ts +23 -0
  32. package/dist/src/commands/assess-render.js +60 -0
  33. package/dist/src/commands/doctor.js +8 -13
  34. package/dist/src/commands/init.js +13 -1
  35. package/dist/src/commands/locks.js +2 -0
  36. package/dist/src/commands/logs.js +14 -5
  37. package/dist/src/commands/merge.js +154 -2
  38. package/dist/src/commands/ready-tui-adapter.js +6 -1
  39. package/dist/src/commands/ready.d.ts +25 -3
  40. package/dist/src/commands/ready.js +39 -13
  41. package/dist/src/commands/resume.d.ts +113 -0
  42. package/dist/src/commands/resume.js +193 -0
  43. package/dist/src/commands/run-display.js +37 -4
  44. package/dist/src/commands/run-flags.d.ts +42 -1
  45. package/dist/src/commands/run-flags.js +53 -1
  46. package/dist/src/commands/run-progress.js +26 -1
  47. package/dist/src/commands/run.js +11 -10
  48. package/dist/src/commands/state.js +7 -0
  49. package/dist/src/commands/status.d.ts +9 -0
  50. package/dist/src/commands/status.js +24 -1
  51. package/dist/src/commands/sync.js +90 -19
  52. package/dist/src/commands/update.js +28 -5
  53. package/dist/src/lib/ac-parser.d.ts +14 -0
  54. package/dist/src/lib/ac-parser.js +99 -5
  55. package/dist/src/lib/assess/index.d.ts +10 -0
  56. package/dist/src/lib/assess/index.js +9 -0
  57. package/dist/src/lib/assess/renderer.d.ts +64 -0
  58. package/dist/src/lib/assess/renderer.js +481 -0
  59. package/dist/src/lib/assess/types.d.ts +224 -0
  60. package/dist/src/lib/assess/types.js +241 -0
  61. package/dist/src/lib/ci/outputs.d.ts +15 -2
  62. package/dist/src/lib/ci/outputs.js +17 -5
  63. package/dist/src/lib/cli-flags.d.ts +33 -0
  64. package/dist/src/lib/cli-flags.js +46 -0
  65. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +21 -7
  66. package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
  67. package/dist/src/lib/cli-ui/run-renderer.js +62 -0
  68. package/dist/src/lib/errors.d.ts +78 -6
  69. package/dist/src/lib/errors.js +133 -15
  70. package/dist/src/lib/locks/index.d.ts +3 -3
  71. package/dist/src/lib/locks/index.js +2 -2
  72. package/dist/src/lib/locks/lock-manager.d.ts +20 -2
  73. package/dist/src/lib/locks/lock-manager.js +47 -3
  74. package/dist/src/lib/locks/types.d.ts +34 -3
  75. package/dist/src/lib/locks/types.js +16 -0
  76. package/dist/src/lib/mcp-config.d.ts +45 -0
  77. package/dist/src/lib/mcp-config.js +77 -1
  78. package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
  79. package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
  80. package/dist/src/lib/merge-check/command-result.d.ts +36 -0
  81. package/dist/src/lib/merge-check/command-result.js +56 -0
  82. package/dist/src/lib/merge-check/index.d.ts +4 -0
  83. package/dist/src/lib/merge-check/index.js +1 -1
  84. package/dist/src/lib/merge-check/report.js +5 -1
  85. package/dist/src/lib/merge-check/types.d.ts +9 -0
  86. package/dist/src/lib/merge-check/watch.d.ts +121 -0
  87. package/dist/src/lib/merge-check/watch.js +215 -0
  88. package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
  89. package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
  90. package/dist/src/lib/settings.d.ts +14 -0
  91. package/dist/src/lib/settings.js +61 -8
  92. package/dist/src/lib/shutdown.d.ts +42 -2
  93. package/dist/src/lib/shutdown.js +75 -5
  94. package/dist/src/lib/skills-check.d.ts +26 -0
  95. package/dist/src/lib/skills-check.js +31 -0
  96. package/dist/src/lib/stacks.d.ts +110 -1
  97. package/dist/src/lib/stacks.js +244 -5
  98. package/dist/src/lib/templates.d.ts +74 -0
  99. package/dist/src/lib/templates.js +171 -16
  100. package/dist/src/lib/test-tautology-detector.d.ts +9 -2
  101. package/dist/src/lib/test-tautology-detector.js +153 -12
  102. package/dist/src/lib/version-check.d.ts +31 -0
  103. package/dist/src/lib/version-check.js +45 -2
  104. package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
  105. package/dist/src/lib/workflow/batch-executor.js +477 -44
  106. package/dist/src/lib/workflow/chain-resume.d.ts +13 -11
  107. package/dist/src/lib/workflow/chain-resume.js +24 -20
  108. package/dist/src/lib/workflow/completed-status.d.ts +52 -0
  109. package/dist/src/lib/workflow/completed-status.js +57 -0
  110. package/dist/src/lib/workflow/config-resolver.d.ts +26 -0
  111. package/dist/src/lib/workflow/config-resolver.js +54 -2
  112. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +7 -0
  113. package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
  114. package/dist/src/lib/workflow/drivers/aider.js +2 -0
  115. package/dist/src/lib/workflow/drivers/claude-code.d.ts +14 -0
  116. package/dist/src/lib/workflow/drivers/claude-code.js +20 -2
  117. package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
  118. package/dist/src/lib/workflow/git-diff-utils.js +77 -4
  119. package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
  120. package/dist/src/lib/workflow/heartbeat.js +90 -0
  121. package/dist/src/lib/workflow/log-writer.d.ts +44 -1
  122. package/dist/src/lib/workflow/log-writer.js +75 -6
  123. package/dist/src/lib/workflow/metrics-schema.d.ts +3 -3
  124. package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
  125. package/dist/src/lib/workflow/phase-executor.js +457 -52
  126. package/dist/src/lib/workflow/phase-registry.js +2 -1
  127. package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
  128. package/dist/src/lib/workflow/platforms/github.js +152 -0
  129. package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
  130. package/dist/src/lib/workflow/qa-cache.js +22 -9
  131. package/dist/src/lib/workflow/ready-gate.d.ts +17 -4
  132. package/dist/src/lib/workflow/ready-gate.js +25 -9
  133. package/dist/src/lib/workflow/run-log-schema.d.ts +14 -0
  134. package/dist/src/lib/workflow/run-log-schema.js +47 -1
  135. package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
  136. package/dist/src/lib/workflow/run-orchestrator.js +141 -20
  137. package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
  138. package/dist/src/lib/workflow/skills-preflight.js +112 -0
  139. package/dist/src/lib/workflow/state-cleanup.js +9 -2
  140. package/dist/src/lib/workflow/state-manager.d.ts +33 -0
  141. package/dist/src/lib/workflow/state-manager.js +83 -0
  142. package/dist/src/lib/workflow/state-schema.d.ts +26 -0
  143. package/dist/src/lib/workflow/state-schema.js +47 -0
  144. package/dist/src/lib/workflow/types.d.ts +126 -4
  145. package/dist/src/lib/workflow/types.js +1 -0
  146. package/dist/src/lib/workflow/worktree-manager.d.ts +28 -2
  147. package/dist/src/lib/workflow/worktree-manager.js +102 -23
  148. package/dist/src/mcp/tools/run.d.ts +9 -1
  149. package/dist/src/mcp/tools/run.js +20 -1
  150. package/dist/src/mcp/tools/status.js +6 -0
  151. package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
  152. package/dist/src/ui/tui/ElapsedTimer.js +12 -10
  153. package/dist/src/ui/tui/IssueBox.js +10 -2
  154. package/dist/src/ui/tui/row-cap.js +2 -1
  155. package/dist/src/ui/tui/theme.d.ts +7 -0
  156. package/dist/src/ui/tui/theme.js +9 -0
  157. package/package.json +8 -7
  158. package/templates/hooks/parallel-marker.sh +50 -0
  159. package/templates/hooks/post-tool.sh +20 -7
  160. package/templates/hooks/pre-tool.sh +75 -10
  161. package/templates/scripts/cleanup-worktree.sh +182 -19
  162. package/templates/scripts/new-feature.sh +264 -16
  163. package/templates/skills/_shared/references/trust-model.md +18 -0
  164. package/templates/skills/assess/SKILL.md +215 -390
  165. package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
  166. package/templates/skills/docs/SKILL.md +4 -4
  167. package/templates/skills/exec/SKILL.md +26 -6
  168. package/templates/skills/fullsolve/SKILL.md +3 -1
  169. package/templates/skills/loop/SKILL.md +38 -2
  170. package/templates/skills/merger/SKILL.md +2 -2
  171. package/templates/skills/qa/SKILL.md +337 -57
  172. package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
  173. package/templates/skills/qa/references/call-site-review.md +2 -2
  174. package/templates/skills/qa/references/code-review-checklist.md +2 -2
  175. package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  176. package/templates/skills/qa/references/quality-gates.md +21 -7
  177. package/templates/skills/qa/references/testing-requirements.md +1 -1
  178. package/templates/skills/qa/scripts/quality-checks.sh +11 -11
  179. package/templates/skills/release/SKILL.md +1 -1
  180. package/templates/skills/setup/SKILL.md +5 -5
  181. package/templates/skills/spec/SKILL.md +2 -0
  182. package/templates/skills/test/SKILL.md +1 -1
  183. package/dist/src/lib/phase-spinner.d.ts +0 -146
  184. package/dist/src/lib/phase-spinner.js +0 -255
  185. package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
  186. package/dist/src/lib/workflow/pr-operations.js +0 -326
  187. package/dist/src/lib/workflow/run-summary.d.ts +0 -36
  188. package/dist/src/lib/workflow/run-summary.js +0 -142
@@ -100,9 +100,16 @@ export declare function extractTestBlocks(content: string): Array<{
100
100
  style: "it" | "test";
101
101
  }>;
102
102
  /**
103
- * Check if a test block contains calls to any of the imported production functions
103
+ * Check if a test block calls production code. A block counts as non-tautological
104
+ * when it references an imported production function, directly spawns the
105
+ * project's build output, or calls a helper that (transitively) does so.
106
+ *
107
+ * @param spawnHandles Names of describe/module-scope helpers that spawn the
108
+ * build output (see {@link collectSpawnHandles}).
109
+ * @param buildOutputVars Variable names bound to a build-output path (see
110
+ * {@link collectBuildOutputVars}).
104
111
  */
105
- export declare function testBlockCallsProductionCode(body: string, importedFunctions: ImportedFunction[]): boolean;
112
+ export declare function testBlockCallsProductionCode(body: string, importedFunctions: ImportedFunction[], spawnHandles?: string[], buildOutputVars?: string[]): boolean;
106
113
  /**
107
114
  * Check if a file opts out of tautology detection via pragma comment.
108
115
  *
@@ -341,20 +341,159 @@ function escapeRegex(str) {
341
341
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
342
342
  }
343
343
  /**
344
- * Check if a test block contains calls to any of the imported production functions
344
+ * Build a whole-identifier reference matcher for `name`, bounded by
345
+ * non-identifier chars ([\w$]). Catches direct calls, method calls, callback
346
+ * references, and assignments while rejecting substring matches.
345
347
  */
346
- export function testBlockCallsProductionCode(body, importedFunctions) {
347
- if (importedFunctions.length === 0) {
348
- return false;
348
+ function referenceMatcher(name) {
349
+ return new RegExp(`(?<![\\w$])${escapeRegex(name)}(?![\\w$])`);
350
+ }
351
+ /**
352
+ * Child-process spawn functions. A test that spawns the project's *build
353
+ * output* is exercising production code across a process boundary that static
354
+ * import analysis cannot see through — issue #885.
355
+ *
356
+ * Two alternates by name ambiguity: the long names are unambiguous
357
+ * child-process API and match anywhere, including method-style calls from a
358
+ * namespace import (`cp.execSync(...)`). The short names (`exec`, `spawn`,
359
+ * `fork`) collide with unrelated methods — `RegExp.prototype.exec` most of
360
+ * all — so they must not be preceded by `.` (or an identifier char). The
361
+ * cost is that method-style callback `cp.exec(...)` no longer counts; tests
362
+ * that spawn build output overwhelmingly use the sync variants, and a false
363
+ * tautology report is loud where the `.exec()` collision was silent.
364
+ */
365
+ const SPAWN_PATTERN = /(?:\b(?:execFileSync|spawnSync|execSync|execFile)\s*\(|(?<![\w$.])(?:exec|fork|spawn)\s*\()/;
366
+ /**
367
+ * Marker for the project's build-output directory. A spawn whose arguments
368
+ * reach this path is running compiled production code.
369
+ */
370
+ const BUILD_OUTPUT_PATTERN = /\bdist\//;
371
+ /**
372
+ * Collect names of variables bound to a build-output path, e.g.
373
+ * const cliPath = resolve(projectRoot, "dist/bin/cli.js");
374
+ * captures `cliPath`. Tests almost always spawn via such a handle rather than
375
+ * an inline string, so these names stand in for the literal build path.
376
+ *
377
+ * The right-hand side is statement-bounded (`[^;]`) so a match cannot bleed
378
+ * across declarations, and must contain the `dist/` marker.
379
+ */
380
+ function collectBuildOutputVars(content) {
381
+ const names = new Set();
382
+ const pattern = /(?:const|let|var)\s+(\w+)\s*=\s*[^;]*?\bdist\//g;
383
+ let match;
384
+ while ((match = pattern.exec(content)) !== null) {
385
+ names.add(match[1]);
386
+ }
387
+ return [...names];
388
+ }
389
+ /**
390
+ * Whether a code body references a build-output token: either the literal
391
+ * `dist/` marker or one of the collected build-path variable names.
392
+ */
393
+ function referencesBuildOutput(body, buildOutputVars) {
394
+ if (BUILD_OUTPUT_PATTERN.test(body)) {
395
+ return true;
396
+ }
397
+ return buildOutputVars.some((name) => referenceMatcher(name).test(body));
398
+ }
399
+ /**
400
+ * Whether a code body itself spawns the build output: it must contain BOTH a
401
+ * child-process spawn call AND a build-output token. Requiring co-occurrence
402
+ * keeps a helper that merely mentions `dist/` in a string (but never spawns)
403
+ * from counting as production (#885 AC-5).
404
+ */
405
+ function spawnsBuildOutput(body, buildOutputVars) {
406
+ return (SPAWN_PATTERN.test(body) && referencesBuildOutput(body, buildOutputVars));
407
+ }
408
+ /**
409
+ * Extract module/describe-scope helper definitions (named block-bodied arrow
410
+ * consts) as { name, body } pairs.
411
+ *
412
+ * Anchors on `=> {` so a return-type object annotation
413
+ * const run = (): { stdout: string } => { ... }
414
+ * is not mistaken for the function body. Params are matched with `[^()]*` (no
415
+ * nested parens) to keep the scan from running away across the file. Only
416
+ * block-bodied arrows are collected; the subprocess integration tests this
417
+ * targets all use them.
418
+ */
419
+ function extractHelperDefinitions(content) {
420
+ const helpers = [];
421
+ const pattern = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^()]*\)\s*(?::[^=]*?)?=>\s*\{/g;
422
+ let match;
423
+ while ((match = pattern.exec(content)) !== null) {
424
+ if (isInsideString(content, match.index)) {
425
+ continue;
426
+ }
427
+ const name = match[1];
428
+ // The final `{` of the match is the function body's opening brace.
429
+ const braceIndex = match.index + match[0].length - 1;
430
+ const body = extractBlockBody(content.substring(braceIndex));
431
+ helpers.push({ name, body });
432
+ }
433
+ return helpers;
434
+ }
435
+ /**
436
+ * Collect the names of helper functions that spawn the build output, resolving
437
+ * indirection transitively: a helper that calls an already-known spawn helper
438
+ * is itself a spawn helper. This lets a test that only calls
439
+ * `expectFlagAccepted(...)` (which calls `runInUninitializedDir`, which spawns
440
+ * the CLI) count as exercising production code.
441
+ */
442
+ function collectSpawnHandles(content, buildOutputVars) {
443
+ const helpers = extractHelperDefinitions(content);
444
+ const handles = new Set();
445
+ // Seed: helpers that directly spawn the build output.
446
+ for (const helper of helpers) {
447
+ if (spawnsBuildOutput(helper.body, buildOutputVars)) {
448
+ handles.add(helper.name);
449
+ }
450
+ }
451
+ // Transitive closure: a helper referencing a known spawn helper is one too.
452
+ let changed = true;
453
+ while (changed) {
454
+ changed = false;
455
+ for (const helper of helpers) {
456
+ if (handles.has(helper.name)) {
457
+ continue;
458
+ }
459
+ for (const known of handles) {
460
+ if (referenceMatcher(known).test(helper.body)) {
461
+ handles.add(helper.name);
462
+ changed = true;
463
+ break;
464
+ }
465
+ }
466
+ }
349
467
  }
468
+ return [...handles];
469
+ }
470
+ /**
471
+ * Check if a test block calls production code. A block counts as non-tautological
472
+ * when it references an imported production function, directly spawns the
473
+ * project's build output, or calls a helper that (transitively) does so.
474
+ *
475
+ * @param spawnHandles Names of describe/module-scope helpers that spawn the
476
+ * build output (see {@link collectSpawnHandles}).
477
+ * @param buildOutputVars Variable names bound to a build-output path (see
478
+ * {@link collectBuildOutputVars}).
479
+ */
480
+ export function testBlockCallsProductionCode(body, importedFunctions, spawnHandles = [], buildOutputVars = []) {
481
+ // 1. References an imported production function.
350
482
  for (const fn of importedFunctions) {
351
- // Check for any reference to the imported name bounded by non-identifier chars.
352
- // Uses [\w$] to match JS identifier characters (letters, digits, _, $).
353
- // This catches direct calls (fn()), method calls (ns.method()),
354
- // callback references (arr.map(fn)), and assignments (const x = fn).
355
- const escaped = escapeRegex(fn.name);
356
- const referencePattern = new RegExp(`(?<![\\w$])${escaped}(?![\\w$])`);
357
- if (referencePattern.test(body)) {
483
+ if (referenceMatcher(fn.name).test(body)) {
484
+ return true;
485
+ }
486
+ }
487
+ // 2. Directly spawns the project's build output (#885). Static import
488
+ // analysis can't see through a subprocess boundary, so a test that runs
489
+ // `dist/bin/cli.js` looks import-less but exercises production code.
490
+ if (spawnsBuildOutput(body, buildOutputVars)) {
491
+ return true;
492
+ }
493
+ // 3. Calls a describe/module-scope helper that (transitively) spawns the
494
+ // build output.
495
+ for (const handle of spawnHandles) {
496
+ if (referenceMatcher(handle).test(body)) {
358
497
  return true;
359
498
  }
360
499
  }
@@ -389,12 +528,14 @@ export function analyzeTestFile(content, filePath) {
389
528
  }
390
529
  try {
391
530
  const importedFunctions = extractImports(content);
531
+ const buildOutputVars = collectBuildOutputVars(content);
532
+ const spawnHandles = collectSpawnHandles(content, buildOutputVars);
392
533
  const testBlocks = extractTestBlocks(content);
393
534
  const analyzedBlocks = testBlocks.map((block) => ({
394
535
  description: block.description,
395
536
  lineNumber: block.lineNumber,
396
537
  style: block.style,
397
- isTautological: !testBlockCallsProductionCode(block.body, importedFunctions),
538
+ isTautological: !testBlockCallsProductionCode(block.body, importedFunctions, spawnHandles, buildOutputVars),
398
539
  }));
399
540
  const tautologicalCount = analyzedBlocks.filter((b) => b.isTautological).length;
400
541
  const totalTests = analyzedBlocks.length;
@@ -53,6 +53,37 @@ export declare function isGlobalInstall(installPath?: string): boolean;
53
53
  * neither receives the project-local "use npm update sequant" warning.
54
54
  */
55
55
  export declare function isLocalNodeModulesInstall(installPath?: string): boolean;
56
+ /**
57
+ * Check if running from the npx cache (~/.npm/_npx/<hash>/node_modules/sequant).
58
+ *
59
+ * Separated from `isLocalNodeModulesInstall`, which deliberately excludes the
60
+ * cache: that predicate answers "is this a project dependency the user can
61
+ * `npm update`", while this one answers "did the user reach us through npx".
62
+ * Backslashes are normalized first, so the single forward-slash form also
63
+ * matches a Windows `…\.npm\_npx\…` path.
64
+ */
65
+ export declare function isNpxCacheInstall(installPath?: string): boolean;
66
+ /**
67
+ * Resolve the invocation to name in user-facing "run this next" messages.
68
+ *
69
+ * A hardcoded `npx sequant` is right for the documented install path (README:
70
+ * `npm install sequant` then `npx sequant init`) — npx resolves the project's
71
+ * `node_modules/.bin/sequant`, and a bare `sequant` would not be on PATH. It is
72
+ * wrong for a global or `npm link`ed install with no local dependency: there,
73
+ * `npx sequant` cannot resolve locally and falls through to the npx cache,
74
+ * which can hold an older *published* copy than the one currently running. The
75
+ * observed symptom is a remediation that fights itself — the recommended
76
+ * command reinstalls the very content whose staleness triggered the message.
77
+ *
78
+ * So recommend the form that actually reaches *this* build:
79
+ * - project-local dependency or npx cache → `npx sequant`
80
+ * - global, `npm link`ed, or a dev checkout → `sequant`
81
+ *
82
+ * A linked checkout resolves through its realpath (Node resolves symlinks
83
+ * unless `--preserve-symlinks` is set), so it looks like neither a global nor a
84
+ * local install and correctly lands on the bare form.
85
+ */
86
+ export declare function resolveCliInvocation(installPath?: string): "sequant" | "npx sequant";
56
87
  /**
57
88
  * Walk up from the given directory to find the directory containing
58
89
  * sequant's package.json. Returns null if not found.
@@ -8,7 +8,7 @@ import fs from "fs";
8
8
  import os from "os";
9
9
  import path from "path";
10
10
  import { fileURLToPath } from "url";
11
- import { detectPackageManagerSync, getPackageManagerCommands, } from "./stacks.js";
11
+ import { detectPackageManagerSync, resolvePackageManagerConfig, } from "./stacks.js";
12
12
  const __filename = fileURLToPath(import.meta.url);
13
13
  const __dirname = path.dirname(__filename);
14
14
  const PACKAGE_NAME = "sequant";
@@ -92,6 +92,44 @@ export function isLocalNodeModulesInstall(installPath = __dirname) {
92
92
  normalizedPath.includes("\\.npm\\_npx\\");
93
93
  return inNodeModules && !inNpxCache && !isGlobalInstall(normalizedPath);
94
94
  }
95
+ /**
96
+ * Check if running from the npx cache (~/.npm/_npx/<hash>/node_modules/sequant).
97
+ *
98
+ * Separated from `isLocalNodeModulesInstall`, which deliberately excludes the
99
+ * cache: that predicate answers "is this a project dependency the user can
100
+ * `npm update`", while this one answers "did the user reach us through npx".
101
+ * Backslashes are normalized first, so the single forward-slash form also
102
+ * matches a Windows `…\.npm\_npx\…` path.
103
+ */
104
+ export function isNpxCacheInstall(installPath = __dirname) {
105
+ return installPath.replace(/\\/g, "/").includes("/.npm/_npx/");
106
+ }
107
+ /**
108
+ * Resolve the invocation to name in user-facing "run this next" messages.
109
+ *
110
+ * A hardcoded `npx sequant` is right for the documented install path (README:
111
+ * `npm install sequant` then `npx sequant init`) — npx resolves the project's
112
+ * `node_modules/.bin/sequant`, and a bare `sequant` would not be on PATH. It is
113
+ * wrong for a global or `npm link`ed install with no local dependency: there,
114
+ * `npx sequant` cannot resolve locally and falls through to the npx cache,
115
+ * which can hold an older *published* copy than the one currently running. The
116
+ * observed symptom is a remediation that fights itself — the recommended
117
+ * command reinstalls the very content whose staleness triggered the message.
118
+ *
119
+ * So recommend the form that actually reaches *this* build:
120
+ * - project-local dependency or npx cache → `npx sequant`
121
+ * - global, `npm link`ed, or a dev checkout → `sequant`
122
+ *
123
+ * A linked checkout resolves through its realpath (Node resolves symlinks
124
+ * unless `--preserve-symlinks` is set), so it looks like neither a global nor a
125
+ * local install and correctly lands on the bare form.
126
+ */
127
+ export function resolveCliInvocation(installPath = __dirname) {
128
+ return isLocalNodeModulesInstall(installPath) ||
129
+ isNpxCacheInstall(installPath)
130
+ ? "npx sequant"
131
+ : "sequant";
132
+ }
95
133
  /**
96
134
  * Walk up from the given directory to find the directory containing
97
135
  * sequant's package.json. Returns null if not found.
@@ -314,7 +352,12 @@ export function getVersionWarning(currentVersion, latestVersion, isLocal) {
314
352
  const isLocalInstall = isLocal ?? isLocalNodeModulesInstall();
315
353
  if (isLocalInstall) {
316
354
  const pm = detectPackageManagerSync();
317
- const pmConfig = getPackageManagerCommands(pm);
355
+ // Resolved, not read off PM_CONFIG: `updatePkg` is one of the fields whose
356
+ // yarn spelling depends on the major (berry `yarn up` vs classic
357
+ // `yarn upgrade`), and this string is a command we are telling the user to
358
+ // run. Both `detectPackageManagerSync` and the resolver read the cwd, which
359
+ // is the project this local install belongs to (#871).
360
+ const pmConfig = resolvePackageManagerConfig(pm, process.cwd());
318
361
  return `sequant ${latestVersion} is available (you have ${currentVersion})
319
362
  Run: ${pmConfig.updatePkg} sequant
320
363
  Note: You have sequant as a local dependency. npx uses your node_modules version.`;
@@ -7,6 +7,7 @@
7
7
  * (including quality-loop retries, checkpoint commits, rebasing, and PR
8
8
  * creation).
9
9
  */
10
+ import { LogWriter } from "./log-writer.js";
10
11
  import { ExecutionConfig, PhaseResult, IssueResult, type RunOptions, type IssueExecutionContext, type BatchExecutionContext, type ProgressCallback } from "./types.js";
11
12
  import { type ErrorCategory } from "./error-classifier.js";
12
13
  export type { RunOptions, ProgressCallback, IssueExecutionContext, BatchExecutionContext, } from "./types.js";
@@ -21,15 +22,32 @@ export type { RunOptions, ProgressCallback, IssueExecutionContext, BatchExecutio
21
22
  * @param extra - Optional fields: durationSeconds (on complete), error (on failed)
22
23
  */
23
24
  /**
24
- * Wrap an `ExecutionConfig` with an `onActivity` hook that re-emits each
25
- * agent-output ping as a `"activity"` progress event for the dashboard (#543).
25
+ * Wrap an `ExecutionConfig` with the runtime liveness hooks:
26
+ * - `onActivity` — re-emits each agent-output ping as an `"activity"` progress
27
+ * event for the dashboard (#543).
28
+ * - `onAutoWait` — re-emits each auto-wait tick as a `"waiting"` progress
29
+ * event so the renderer and heartbeat can show the pause and its wake time
30
+ * (#804 AC-7). Under `SEQUANT_ORCHESTRATOR` it additionally emits throttled
31
+ * `SEQUANT_PROGRESS` waiting lines (#860): an MCP-driven wait was previously
32
+ * invisible on the JSON channel — indistinguishable from a hang — AND was
33
+ * killed by the MCP inactivity timeout, which resets on progress lines.
34
+ * Optionally notifies `onWaitTransition` on wait start/end so the caller
35
+ * can persist the wait to issue state (`sequant status` truthfulness).
26
36
  *
27
- * Returns the input config unchanged when no `onProgress` callback is set,
28
- * so non-TUI runs pay no overhead.
37
+ * Returns the input config unchanged when there is no consumer at all (no
38
+ * `onProgress`, no orchestrator channel, no transition callback), so plain
39
+ * non-TUI runs pay no overhead.
29
40
  *
30
41
  * @internal Exported for testing only
31
42
  */
32
- export declare function withActivityHook(base: ExecutionConfig, issueNumber: number, phase: string, onProgress: ProgressCallback | undefined): ExecutionConfig;
43
+ export declare function withActivityHook(base: ExecutionConfig, issueNumber: number, phase: string, onProgress: ProgressCallback | undefined, onWaitTransition?: (wakeAtMs: number | null) => void): ExecutionConfig;
44
+ /**
45
+ * Cadence of orchestrator-channel waiting lines during an auto-wait (#860).
46
+ * See {@link withActivityHook}.
47
+ *
48
+ * @internal Exported for testing only
49
+ */
50
+ export declare const AUTO_WAIT_PROGRESS_LINE_INTERVAL_MS = 60000;
33
51
  /**
34
52
  * Build enriched prompt context for the /loop phase from a failed phase result (#488).
35
53
  * Passes QA verdict, failed ACs, and error directly so the /loop skill doesn't need
@@ -38,10 +56,12 @@ export declare function withActivityHook(base: ExecutionConfig, issueNumber: num
38
56
  * @internal Exported for testing only
39
57
  */
40
58
  export declare function buildLoopContext(failedResult: PhaseResult): string;
41
- export declare function emitProgressLine(issue: number, phase: string, event?: "start" | "complete" | "failed", extra?: {
59
+ export declare function emitProgressLine(issue: number, phase: string, event?: "start" | "complete" | "failed" | "waiting", extra?: {
42
60
  durationSeconds?: number;
43
61
  error?: string;
44
62
  iteration?: number;
63
+ wakeAtMs?: number;
64
+ remainingMs?: number;
45
65
  }): void;
46
66
  /**
47
67
  * Emit the current run's UUID on stderr so MCP callers can look up the exact
@@ -73,6 +93,32 @@ export declare function parseBatches(batchArgs: string[]): number[][];
73
93
  * Parse environment variables for CI configuration
74
94
  */
75
95
  export declare function getEnvConfig(): Partial<RunOptions>;
96
+ /**
97
+ * Record an issue's completion in the run log in ONE place (#879): PR info, the
98
+ * PR-failure status flip, then finalize. Extracted so every batch loop shares a
99
+ * single completion sequence and cannot drift.
100
+ *
101
+ * The #879 defect was exactly such a drift: `markIssueFailed` was wired into
102
+ * `executeBatch`'s loop, but the live `sequant run` path is
103
+ * `RunOrchestrator.executeOneIssue`, which called `setPRInfo` + `completeIssue`
104
+ * without it — so a real run left the run-log status at `success` on a
105
+ * PR-creation failure. Both call sites now go through this helper.
106
+ *
107
+ * A PR-creation failure occurs after every phase has been logged, so
108
+ * `deriveIssueLogStatus` (last run at phase-log time) leaves the issue at
109
+ * `success`; the flip here is what counts it under `failed`. Safe post-hoc:
110
+ * no further phase is logged before `completeIssue`.
111
+ */
112
+ export declare function recordIssueCompletion(logWriter: LogWriter, result: IssueResult, issueNumber?: number): void;
113
+ /**
114
+ * @deprecated No live caller — `sequant run` executes issues via
115
+ * `RunOrchestrator.executeOneIssue`; this survives only as a
116
+ * `commands/run-compat` re-export. Do not build a new execution loop on it:
117
+ * any path that completes an issue MUST go through
118
+ * {@link recordIssueCompletion}, or the #879 status-drift returns (a
119
+ * completion path that skips the PR-failure flip logs a failed issue as
120
+ * `success`). Slated for removal with the run-compat surface.
121
+ */
76
122
  export declare function executeBatch(issueNumbers: number[], batchCtx: BatchExecutionContext): Promise<IssueResult[]>;
77
123
  /**
78
124
  * Derive the bounded-enum failure category for a failed issue (#761 AC-7).
@@ -86,4 +132,77 @@ export declare function executeBatch(issueNumbers: number[], batchCtx: BatchExec
86
132
  * @internal Exported for testing
87
133
  */
88
134
  export declare function deriveFailureCategory(phaseResults: PhaseResult[]): ErrorCategory | undefined;
135
+ /**
136
+ * "Halt, don't loop" predicate for the outer `-Q` quality loop (#799).
137
+ *
138
+ * A billing / out-of-credits failure (`BillingError`) or a window-exhausted
139
+ * rate limit (reset hours away, per `isWindowExhaustedRateLimit`) cannot be
140
+ * recovered by re-running the phase — every retry re-spawns into the same
141
+ * closed window and, worse, mislabels the halt as a downstream
142
+ * `QA completed without a parseable verdict`. Mirrors the `haltedByCap` (#739)
143
+ * treatment: surface the real cause and halt so the user resumes once credits
144
+ * or the rate-limit window are restored.
145
+ *
146
+ * A transient / metadata-absent rate limit is NOT a halt: it returns false and
147
+ * keeps today's outer-loop behavior (the inner retry ladder in `phase-executor`
148
+ * handles its backoff, per #761 AC-4/AC-9).
149
+ *
150
+ * @internal Exported for testing
151
+ */
152
+ export declare function isBillingOrWindowHalt(result: PhaseResult): boolean;
153
+ /**
154
+ * The billing half of {@link isBillingOrWindowHalt} (#804 AC-8).
155
+ *
156
+ * Split out because the two causes stopped being interchangeable once
157
+ * `--auto-wait` existed: a closed window can now reopen on its own, while
158
+ * out-of-credits cannot — credits are purchased, not waited out. Callers that
159
+ * need to reason about recoverability must be able to tell them apart.
160
+ *
161
+ * @internal Exported for testing
162
+ */
163
+ export declare function isBillingHalt(result: PhaseResult): boolean;
164
+ /**
165
+ * The rate-limit-window half of {@link isBillingOrWindowHalt} (#804 AC-8).
166
+ *
167
+ * NOTE — this predicate needed no behavioral change for auto-wait, and that is
168
+ * a deliberate finding rather than an oversight. AC-8 anticipated that a phase
169
+ * which waits and then succeeds would still halt the `-Q` loop. It cannot:
170
+ * every call site (`:~700` spec, `:~1030` progress label, `:~1150` halt flag)
171
+ * sits inside the `else` of an `if (result.success)`, so a successful
172
+ * post-wait result never reaches this predicate at all. When auto-wait does
173
+ * NOT fire — the default, an exhausted budget, or a spent wait bound — the
174
+ * result is still a failure carrying a window-exhausted `RateLimitError`, and
175
+ * halting is then the correct outcome (#799 behavior, preserved exactly).
176
+ *
177
+ * @internal Exported for testing
178
+ */
179
+ export declare function isWindowHalt(result: PhaseResult): boolean;
180
+ /**
181
+ * Human-readable halt reason for a billing / rate-limit-window failure (#799
182
+ * AC-3). Surfaces the driver's real cause verbatim — `result.error` is already
183
+ * the well-formatted message the driver built via `formatRateLimitMessage`
184
+ * (`Out of credits` for billing, `Rate limited — resets at <local time>` for a
185
+ * throttle with a known reset), so the phase-failed line and run summary name
186
+ * the actual cause instead of a downstream `QA completed without a parseable
187
+ * verdict`.
188
+ *
189
+ * Do NOT re-append `resetsAt` here: the rate-limit message already carries the
190
+ * reset time, and doing so produced a doubled, timezone-inconsistent string
191
+ * (`… resets at 07-24 14:32 — resets at 2026-…Z`). Credits failures carry no
192
+ * reset time by design (they need purchasing, not a window wait).
193
+ *
194
+ * @internal Exported for testing
195
+ */
196
+ export declare function billingHaltReason(result: PhaseResult): string;
197
+ /**
198
+ * Epoch ms after which a waitable-window halt can be re-entered (#892 AC-1):
199
+ * the window's `resetsAt` normalized to ms plus the same buffer auto-wait
200
+ * applies (`AUTO_WAIT_BUFFER_MS`), so in-process waits and durable halts wake
201
+ * on the same clock. Returns `null` when the result carries no future-reset
202
+ * rate-limit window — callers must then skip the `windowHalt` write rather
203
+ * than invent a resume time.
204
+ *
205
+ * @internal Exported for testing
206
+ */
207
+ export declare function windowHaltResumeAtMs(result: PhaseResult): number | null;
89
208
  export declare function runIssueWithLogging(ctx: IssueExecutionContext): Promise<IssueResult>;