sequant 2.9.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +20 -5
  4. package/dist/bin/cli.js +143 -18
  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.d.ts +20 -1
  36. package/dist/src/commands/locks.js +208 -4
  37. package/dist/src/commands/logs.js +14 -5
  38. package/dist/src/commands/merge.js +154 -2
  39. package/dist/src/commands/ready-tui-adapter.js +6 -1
  40. package/dist/src/commands/ready.d.ts +31 -3
  41. package/dist/src/commands/ready.js +53 -13
  42. package/dist/src/commands/resume.d.ts +113 -0
  43. package/dist/src/commands/resume.js +193 -0
  44. package/dist/src/commands/run-display.js +38 -4
  45. package/dist/src/commands/run-flags.d.ts +42 -1
  46. package/dist/src/commands/run-flags.js +53 -1
  47. package/dist/src/commands/run-progress.js +26 -1
  48. package/dist/src/commands/run.js +11 -10
  49. package/dist/src/commands/state.js +7 -0
  50. package/dist/src/commands/status.d.ts +9 -0
  51. package/dist/src/commands/status.js +24 -1
  52. package/dist/src/commands/sync.js +90 -19
  53. package/dist/src/commands/update.js +28 -5
  54. package/dist/src/commands/worktree.d.ts +31 -0
  55. package/dist/src/commands/worktree.js +95 -0
  56. package/dist/src/lib/ac-parser.d.ts +14 -0
  57. package/dist/src/lib/ac-parser.js +99 -5
  58. package/dist/src/lib/assess/index.d.ts +10 -0
  59. package/dist/src/lib/assess/index.js +9 -0
  60. package/dist/src/lib/assess/renderer.d.ts +64 -0
  61. package/dist/src/lib/assess/renderer.js +481 -0
  62. package/dist/src/lib/assess/types.d.ts +224 -0
  63. package/dist/src/lib/assess/types.js +241 -0
  64. package/dist/src/lib/ci/outputs.d.ts +15 -2
  65. package/dist/src/lib/ci/outputs.js +17 -5
  66. package/dist/src/lib/cli-flags.d.ts +56 -0
  67. package/dist/src/lib/cli-flags.js +89 -0
  68. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +23 -7
  69. package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
  70. package/dist/src/lib/cli-ui/run-renderer.js +69 -1
  71. package/dist/src/lib/errors.d.ts +78 -6
  72. package/dist/src/lib/errors.js +133 -15
  73. package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
  74. package/dist/src/lib/locks/checkout-lock.js +389 -0
  75. package/dist/src/lib/locks/index.d.ts +7 -4
  76. package/dist/src/lib/locks/index.js +5 -3
  77. package/dist/src/lib/locks/lock-manager.d.ts +101 -3
  78. package/dist/src/lib/locks/lock-manager.js +277 -8
  79. package/dist/src/lib/locks/types.d.ts +106 -3
  80. package/dist/src/lib/locks/types.js +44 -0
  81. package/dist/src/lib/mcp-config.d.ts +45 -0
  82. package/dist/src/lib/mcp-config.js +77 -1
  83. package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
  84. package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
  85. package/dist/src/lib/merge-check/command-result.d.ts +36 -0
  86. package/dist/src/lib/merge-check/command-result.js +56 -0
  87. package/dist/src/lib/merge-check/index.d.ts +4 -0
  88. package/dist/src/lib/merge-check/index.js +1 -1
  89. package/dist/src/lib/merge-check/report.js +5 -1
  90. package/dist/src/lib/merge-check/types.d.ts +9 -0
  91. package/dist/src/lib/merge-check/watch.d.ts +121 -0
  92. package/dist/src/lib/merge-check/watch.js +215 -0
  93. package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
  94. package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
  95. package/dist/src/lib/settings.d.ts +87 -0
  96. package/dist/src/lib/settings.js +106 -8
  97. package/dist/src/lib/shutdown.d.ts +42 -2
  98. package/dist/src/lib/shutdown.js +75 -5
  99. package/dist/src/lib/skills-check.d.ts +26 -0
  100. package/dist/src/lib/skills-check.js +31 -0
  101. package/dist/src/lib/stacks.d.ts +110 -1
  102. package/dist/src/lib/stacks.js +244 -5
  103. package/dist/src/lib/templates.d.ts +74 -0
  104. package/dist/src/lib/templates.js +171 -16
  105. package/dist/src/lib/test-tautology-detector.d.ts +10 -2
  106. package/dist/src/lib/test-tautology-detector.js +213 -12
  107. package/dist/src/lib/version-check.d.ts +31 -0
  108. package/dist/src/lib/version-check.js +45 -2
  109. package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
  110. package/dist/src/lib/workflow/batch-executor.js +552 -60
  111. package/dist/src/lib/workflow/chain-resume.d.ts +13 -11
  112. package/dist/src/lib/workflow/chain-resume.js +24 -20
  113. package/dist/src/lib/workflow/completed-status.d.ts +52 -0
  114. package/dist/src/lib/workflow/completed-status.js +57 -0
  115. package/dist/src/lib/workflow/config-resolver.d.ts +51 -0
  116. package/dist/src/lib/workflow/config-resolver.js +143 -2
  117. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +22 -0
  118. package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
  119. package/dist/src/lib/workflow/drivers/aider.js +2 -0
  120. package/dist/src/lib/workflow/drivers/claude-code.d.ts +14 -0
  121. package/dist/src/lib/workflow/drivers/claude-code.js +25 -2
  122. package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
  123. package/dist/src/lib/workflow/effort-escalation.js +82 -0
  124. package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
  125. package/dist/src/lib/workflow/error-classifier.js +4 -0
  126. package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
  127. package/dist/src/lib/workflow/git-diff-utils.js +77 -4
  128. package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
  129. package/dist/src/lib/workflow/heartbeat.js +90 -0
  130. package/dist/src/lib/workflow/log-writer.d.ts +54 -2
  131. package/dist/src/lib/workflow/log-writer.js +95 -6
  132. package/dist/src/lib/workflow/metrics-schema.d.ts +52 -9
  133. package/dist/src/lib/workflow/metrics-schema.js +33 -0
  134. package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
  135. package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
  136. package/dist/src/lib/workflow/phase-detection.js +5 -1
  137. package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
  138. package/dist/src/lib/workflow/phase-executor.js +467 -52
  139. package/dist/src/lib/workflow/phase-registry.js +2 -1
  140. package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
  141. package/dist/src/lib/workflow/platforms/github.js +152 -0
  142. package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
  143. package/dist/src/lib/workflow/qa-cache.js +22 -9
  144. package/dist/src/lib/workflow/ready-gate.d.ts +45 -4
  145. package/dist/src/lib/workflow/ready-gate.js +49 -12
  146. package/dist/src/lib/workflow/run-log-schema.d.ts +69 -0
  147. package/dist/src/lib/workflow/run-log-schema.js +78 -2
  148. package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
  149. package/dist/src/lib/workflow/run-orchestrator.js +168 -20
  150. package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
  151. package/dist/src/lib/workflow/skills-preflight.js +112 -0
  152. package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
  153. package/dist/src/lib/workflow/spec-recommendation.js +142 -0
  154. package/dist/src/lib/workflow/state-cleanup.js +9 -2
  155. package/dist/src/lib/workflow/state-manager.d.ts +33 -0
  156. package/dist/src/lib/workflow/state-manager.js +83 -0
  157. package/dist/src/lib/workflow/state-schema.d.ts +26 -0
  158. package/dist/src/lib/workflow/state-schema.js +47 -0
  159. package/dist/src/lib/workflow/types.d.ts +190 -4
  160. package/dist/src/lib/workflow/types.js +1 -0
  161. package/dist/src/lib/workflow/worktree-manager.d.ts +36 -3
  162. package/dist/src/lib/workflow/worktree-manager.js +111 -24
  163. package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
  164. package/dist/src/lib/workflow/worktree-resolver.js +126 -0
  165. package/dist/src/mcp/tools/run.d.ts +9 -1
  166. package/dist/src/mcp/tools/run.js +20 -1
  167. package/dist/src/mcp/tools/status.js +6 -0
  168. package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
  169. package/dist/src/ui/tui/ElapsedTimer.js +12 -10
  170. package/dist/src/ui/tui/IssueBox.js +10 -2
  171. package/dist/src/ui/tui/row-cap.js +2 -1
  172. package/dist/src/ui/tui/theme.d.ts +7 -0
  173. package/dist/src/ui/tui/theme.js +9 -0
  174. package/package.json +10 -8
  175. package/templates/hooks/parallel-marker.sh +50 -0
  176. package/templates/hooks/post-tool.sh +20 -7
  177. package/templates/hooks/pre-tool.sh +303 -10
  178. package/templates/scripts/cleanup-worktree.sh +217 -33
  179. package/templates/scripts/new-feature.sh +289 -35
  180. package/templates/skills/_shared/references/subagent-types.md +7 -18
  181. package/templates/skills/_shared/references/trust-model.md +18 -0
  182. package/templates/skills/assess/SKILL.md +220 -391
  183. package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
  184. package/templates/skills/docs/SKILL.md +4 -4
  185. package/templates/skills/exec/SKILL.md +87 -13
  186. package/templates/skills/fullsolve/SKILL.md +130 -22
  187. package/templates/skills/loop/SKILL.md +94 -13
  188. package/templates/skills/merger/SKILL.md +100 -12
  189. package/templates/skills/qa/SKILL.md +396 -63
  190. package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
  191. package/templates/skills/qa/references/call-site-review.md +2 -2
  192. package/templates/skills/qa/references/code-review-checklist.md +2 -2
  193. package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  194. package/templates/skills/qa/references/quality-gates.md +21 -7
  195. package/templates/skills/qa/references/testing-requirements.md +1 -1
  196. package/templates/skills/qa/scripts/quality-checks.sh +11 -11
  197. package/templates/skills/release/SKILL.md +80 -1
  198. package/templates/skills/setup/SKILL.md +5 -5
  199. package/templates/skills/spec/SKILL.md +33 -15
  200. package/templates/skills/spec/references/recommended-workflow.md +14 -1
  201. package/templates/skills/test/SKILL.md +1 -1
  202. package/templates/skills/testgen/SKILL.md +23 -6
  203. package/dist/src/lib/phase-spinner.d.ts +0 -146
  204. package/dist/src/lib/phase-spinner.js +0 -255
  205. package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
  206. package/dist/src/lib/workflow/pr-operations.js +0 -326
  207. package/dist/src/lib/workflow/run-summary.d.ts +0 -36
  208. package/dist/src/lib/workflow/run-summary.js +0 -142
  209. package/templates/agents/sequant-explorer.md +0 -24
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Deterministic rendering for `/assess` output (#823).
3
+ *
4
+ * `types.ts` owns the contract, `renderer.ts` owns the geometry. Neither touches
5
+ * TTY state, ANSI, or I/O — see the header comment in `renderer.ts` for why that
6
+ * rules out the `cli-ui.ts` helpers.
7
+ */
8
+ export { AssessActionSchema, AssessChainSchema, AssessCleanupSchema, AssessCommandSchema, AssessFlagSchema, AssessIssueSchema, AssessResultSchema, AssessResultValidationError, AssessWarningSchema, parseAssessResult, } from "./types.js";
9
+ export { GEOMETRY, render, renderBatch, renderSingle } from "./renderer.js";
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Deterministic renderer for `/assess` output (#823).
3
+ *
4
+ * ## Why this module exists
5
+ *
6
+ * `/assess` was asked to hand-draw an aligned ASCII dashboard inside a prompt.
7
+ * It did two things wrong, both recurring: it silently substituted a prose
8
+ * summary for the block entirely, and when it did draw the table the columns
9
+ * drifted (every worked example in `SKILL.md` had its header offset from its own
10
+ * data rows). Both are the same root cause — deterministic formatting done by
11
+ * hand. This module takes the arithmetic away from the model. The model keeps
12
+ * the judgment; a tool call, which is observable in the transcript, produces the
13
+ * geometry.
14
+ *
15
+ * ## Why not `cli-ui.ts`
16
+ *
17
+ * Its `table()` / `box()` / `divider()` / `sectionHeader()` helpers are the
18
+ * obvious first instinct and the wrong tool. They emit ANSI via `chalk`, gate on
19
+ * `config.isTTY` / `config.jsonMode` (so they silently degrade when stdout is a
20
+ * pipe — which it always is here, since this runs from a Bash call), and
21
+ * `table()` delegates to `cli-table3`'s bordered grid. This output lands in a
22
+ * chat transcript *and* is posted to GitHub, then re-parsed by
23
+ * `assess-comment-parser.ts`. It must be byte-stable, ANSI-free, and
24
+ * copy-pasteable.
25
+ *
26
+ * The only thing shared with the terminal UI is the `string-width` dependency.
27
+ * The module is otherwise modeled on `src/lib/cli-ui/format.ts`: pure
28
+ * deterministic string formatting, no TTY state, no I/O.
29
+ */
30
+ import type { AssessResult } from "./types.js";
31
+ /** Column at which the `Reason` field starts, given whether `ACs` is shown. */
32
+ declare function reasonCol(withAcs: boolean): number;
33
+ /** Width of the `Reason` field — whatever is left before {@link RUN_COL}. */
34
+ declare function reasonWidth(withAcs: boolean): number;
35
+ /**
36
+ * Render the batch dashboard: table → `Commands:` → annotations → markers.
37
+ *
38
+ * Every section is conditional (Section Visibility Rules). A section that has no
39
+ * content takes its adjacent separator with it, so an all-clear batch renders as
40
+ * table → separator → commands → separator → markers and nothing else.
41
+ */
42
+ export declare function renderBatch(result: AssessResult): string;
43
+ /**
44
+ * Render the single-issue assessment for one verdict.
45
+ *
46
+ * PROCEED and REWRITE templates define a `⚠` region between two trailing
47
+ * separators. CLOSE / CLARIFY / PARK / MERGE do not, so a carried warning
48
+ * becomes its own separator-delimited block immediately above the markers — the
49
+ * one case where a posted comment extends a slot-less template.
50
+ */
51
+ export declare function renderSingle(result: AssessResult): string;
52
+ /** Dispatch on `mode`. The entry point the CLI subcommand calls. */
53
+ export declare function render(result: AssessResult): string;
54
+ /**
55
+ * Geometry constants, exported for tests and for documenting the layout.
56
+ * Consumers should treat these as read-only.
57
+ */
58
+ export declare const GEOMETRY: {
59
+ readonly RUN_COL: 47;
60
+ readonly SEPARATOR_WIDTH: 64;
61
+ readonly reasonCol: typeof reasonCol;
62
+ readonly reasonWidth: typeof reasonWidth;
63
+ };
64
+ export {};
@@ -0,0 +1,481 @@
1
+ /**
2
+ * Deterministic renderer for `/assess` output (#823).
3
+ *
4
+ * ## Why this module exists
5
+ *
6
+ * `/assess` was asked to hand-draw an aligned ASCII dashboard inside a prompt.
7
+ * It did two things wrong, both recurring: it silently substituted a prose
8
+ * summary for the block entirely, and when it did draw the table the columns
9
+ * drifted (every worked example in `SKILL.md` had its header offset from its own
10
+ * data rows). Both are the same root cause — deterministic formatting done by
11
+ * hand. This module takes the arithmetic away from the model. The model keeps
12
+ * the judgment; a tool call, which is observable in the transcript, produces the
13
+ * geometry.
14
+ *
15
+ * ## Why not `cli-ui.ts`
16
+ *
17
+ * Its `table()` / `box()` / `divider()` / `sectionHeader()` helpers are the
18
+ * obvious first instinct and the wrong tool. They emit ANSI via `chalk`, gate on
19
+ * `config.isTTY` / `config.jsonMode` (so they silently degrade when stdout is a
20
+ * pipe — which it always is here, since this runs from a Bash call), and
21
+ * `table()` delegates to `cli-table3`'s bordered grid. This output lands in a
22
+ * chat transcript *and* is posted to GitHub, then re-parsed by
23
+ * `assess-comment-parser.ts`. It must be byte-stable, ANSI-free, and
24
+ * copy-pasteable.
25
+ *
26
+ * The only thing shared with the terminal UI is the `string-width` dependency.
27
+ * The module is otherwise modeled on `src/lib/cli-ui/format.ts`: pure
28
+ * deterministic string formatting, no TTY state, no I/O.
29
+ */
30
+ import stringWidth from "string-width";
31
+ // ---------------------------------------------------------------------------
32
+ // Geometry — the single source of truth (AC-11)
33
+ // ---------------------------------------------------------------------------
34
+ /**
35
+ * Column (0-indexed) at which the `Run` field starts, in **both** table
36
+ * variants.
37
+ *
38
+ * This is the one hand-chosen number in the module; every other offset is
39
+ * derived from it. Measured from the prototype in #823, and chosen so a typical
40
+ * row lands near 80 columns: 47 + `spec → security-review → exec → qa` (34) =
41
+ * 81. The `Reason` column absorbs the optional `ACs` column so that adding or
42
+ * removing `ACs` never shifts `Run` — that invariant is what makes the two table
43
+ * forms visually interchangeable, and it is asserted directly in the tests.
44
+ */
45
+ const RUN_COL = 47;
46
+ /** Leading gutter before the issue number. */
47
+ const TABLE_INDENT = 1;
48
+ /** Width of the `#` field — 4 digits covers issue numbers past 9999. */
49
+ const NUMBER_WIDTH = 4;
50
+ /** Width of the `Action` field — `PROCEED`/`REWRITE`/`CLARIFY` are the longest at 7. */
51
+ const ACTION_WIDTH = 10;
52
+ /** Width of the optional `ACs` field. */
53
+ const ACS_WIDTH = 3;
54
+ /** Single space between every pair of columns. */
55
+ const COL_GAP = 1;
56
+ /** Width of every separator line (AC-16). Rows may legitimately overflow it. */
57
+ const SEPARATOR_WIDTH = 64;
58
+ const SEPARATOR = "─".repeat(SEPARATOR_WIDTH);
59
+ /** Indent for `Commands:` / `Flags:` / `Cleanup:` entries. */
60
+ const BLOCK_INDENT = " ";
61
+ /** Gap between a label-block's first column and its aligned second column. */
62
+ const BLOCK_GUTTER = 2;
63
+ /**
64
+ * Below this fraction of the available width, a word-boundary truncation wastes
65
+ * too many columns to be worth it and we cut mid-word instead. Keeps
66
+ * `CI billing-lockout misclassified…` from collapsing to `CI billing-lockout…`.
67
+ */
68
+ const WORD_BOUNDARY_MIN_FILL = 0.7;
69
+ const ELLIPSIS = "…";
70
+ /** Column at which the `Reason` field starts, given whether `ACs` is shown. */
71
+ function reasonCol(withAcs) {
72
+ const base = TABLE_INDENT + NUMBER_WIDTH + COL_GAP + ACTION_WIDTH + COL_GAP;
73
+ return withAcs ? base + ACS_WIDTH + COL_GAP : base;
74
+ }
75
+ /** Width of the `Reason` field — whatever is left before {@link RUN_COL}. */
76
+ function reasonWidth(withAcs) {
77
+ return RUN_COL - reasonCol(withAcs) - COL_GAP;
78
+ }
79
+ // ---------------------------------------------------------------------------
80
+ // Width-aware string helpers
81
+ // ---------------------------------------------------------------------------
82
+ /**
83
+ * Pad `text` on the right to `width` display columns.
84
+ *
85
+ * Uses `string-width`, so a CJK glyph counts as the two columns it actually
86
+ * occupies and does not shift everything after it. Over-wide input is returned
87
+ * unpadded rather than truncated — callers truncate first when they mean to.
88
+ */
89
+ function padTo(text, width) {
90
+ const w = stringWidth(text);
91
+ return w >= width ? text : text + " ".repeat(width - w);
92
+ }
93
+ /**
94
+ * Truncate `text` to `width` display columns, appending `…`.
95
+ *
96
+ * Prefers a word boundary, but only when the boundary retains at least
97
+ * {@link WORD_BOUNDARY_MIN_FILL} of the available width — otherwise a single
98
+ * long final word would leave a ragged gap before the `Run` column.
99
+ * Width-aware throughout, so wide glyphs cannot push the result past `width`.
100
+ */
101
+ function truncateToWidth(text, width) {
102
+ if (stringWidth(text) <= width)
103
+ return text;
104
+ if (width <= 1)
105
+ return ELLIPSIS.slice(0, Math.max(0, width));
106
+ const budget = width - stringWidth(ELLIPSIS);
107
+ const chars = Array.from(text);
108
+ let hard = "";
109
+ let used = 0;
110
+ let consumed = 0;
111
+ for (const ch of chars) {
112
+ const chWidth = stringWidth(ch);
113
+ if (used + chWidth > budget)
114
+ break;
115
+ hard += ch;
116
+ used += chWidth;
117
+ consumed += 1;
118
+ }
119
+ // The hard cut already landed on a word boundary — the next character is the
120
+ // space that would have separated the following word. Backing off to the
121
+ // *previous* space here would drop a word that fit perfectly.
122
+ if (/\s/.test(chars[consumed] ?? "")) {
123
+ return hard.trimEnd() + ELLIPSIS;
124
+ }
125
+ const lastSpace = hard.lastIndexOf(" ");
126
+ if (lastSpace > 0) {
127
+ const wordCut = hard.slice(0, lastSpace).trimEnd();
128
+ if (stringWidth(wordCut) >= Math.floor(budget * WORD_BOUNDARY_MIN_FILL)) {
129
+ return wordCut + ELLIPSIS;
130
+ }
131
+ }
132
+ return hard.trimEnd() + ELLIPSIS;
133
+ }
134
+ /** Fit `text` into exactly `width` columns: truncate if long, pad if short. */
135
+ function fit(text, width) {
136
+ return padTo(truncateToWidth(text, width), width);
137
+ }
138
+ /**
139
+ * Join lines and strip trailing whitespace from each.
140
+ *
141
+ * Trailing spaces are invisible in a terminal but show up as diff noise in a
142
+ * GitHub comment, and they would make snapshot output depend on which column
143
+ * happened to be last.
144
+ */
145
+ function assemble(lines) {
146
+ return lines.map((line) => line.replace(/\s+$/, "")).join("\n");
147
+ }
148
+ /**
149
+ * Wrap `text` to {@link SEPARATOR_WIDTH}, hanging continuation lines at
150
+ * `contIndent` columns. `firstIndent` defaults to 0 so `Order: ...` starts flush
151
+ * and its wrapped remainder tucks under the text; `Chain:`'s comment line passes
152
+ * the same indent for both so the whole comment hangs under the command.
153
+ */
154
+ function wrapAnnotation(text, contIndent, firstIndent = 0) {
155
+ const words = text.split(/\s+/).filter(Boolean);
156
+ if (words.length === 0)
157
+ return [];
158
+ const lines = [];
159
+ let current = "";
160
+ const indentFor = (lineIndex) => lineIndex === 0 ? firstIndent : contIndent;
161
+ const emit = () => lines.push(" ".repeat(indentFor(lines.length)) + current);
162
+ for (const word of words) {
163
+ const candidate = current === "" ? word : `${current} ${word}`;
164
+ const prefixWidth = indentFor(lines.length);
165
+ if (current !== "" &&
166
+ prefixWidth + stringWidth(candidate) > SEPARATOR_WIDTH) {
167
+ emit();
168
+ current = word;
169
+ }
170
+ else {
171
+ current = candidate;
172
+ }
173
+ }
174
+ emit();
175
+ return lines;
176
+ }
177
+ /**
178
+ * Render an aligned two-column block (`Flags:`, `Cleanup:`, annotated commands).
179
+ *
180
+ * The second column starts at `max(width of first column) + BLOCK_GUTTER`, so
181
+ * the gutter adapts to content instead of being a hand-authored offset.
182
+ *
183
+ * Returns `[]` for no rows. Every current call site is already guarded by a
184
+ * `.length` check, but `Math.max()` of an empty list is `-Infinity`, which would
185
+ * turn into a `RangeError` inside `padTo`'s `repeat()` — a sharp edge to leave
186
+ * for the next caller.
187
+ */
188
+ function alignedBlock(rows, commentPrefix = "") {
189
+ if (rows.length === 0)
190
+ return [];
191
+ const gutter = Math.max(...rows.map((row) => stringWidth(row.left))) + BLOCK_GUTTER;
192
+ return rows.map((row) => row.right
193
+ ? `${BLOCK_INDENT}${padTo(row.left, gutter)}${commentPrefix}${row.right}`
194
+ : `${BLOCK_INDENT}${row.left}`);
195
+ }
196
+ // ---------------------------------------------------------------------------
197
+ // Shared section builders
198
+ // ---------------------------------------------------------------------------
199
+ /**
200
+ * A labeled block, or nothing at all.
201
+ *
202
+ * Each builder returns `[]` for no entries rather than a bare header. Emitting
203
+ * `Flags:` with no flags under it would be its own defect — the Section
204
+ * Visibility Rules say an empty section disappears entirely — and it keeps the
205
+ * callers' `.length` guards from being the only thing standing between an empty
206
+ * array and malformed output.
207
+ */
208
+ function commandLines(commands, prefix, header = "Commands:") {
209
+ if (commands.length === 0)
210
+ return [];
211
+ const rows = commands.map((command) => ({
212
+ left: `${prefix} ${command.args}`,
213
+ right: command.comment,
214
+ }));
215
+ return [header, ...alignedBlock(rows, "# ")];
216
+ }
217
+ function flagLines(flags) {
218
+ if (flags.length === 0)
219
+ return [];
220
+ return [
221
+ "Flags:",
222
+ ...alignedBlock(flags.map((f) => ({ left: f.flag, right: f.reason }))),
223
+ ];
224
+ }
225
+ /**
226
+ * `Considered:` — flags whose trigger was evaluated and not met, with the
227
+ * why-not reason. Same two-column geometry as `Flags:`; the header is what
228
+ * distinguishes applied from declined.
229
+ */
230
+ function consideredLines(considered) {
231
+ if (considered.length === 0)
232
+ return [];
233
+ return [
234
+ "Considered:",
235
+ ...alignedBlock(considered.map((f) => ({ left: f.flag, right: f.reason }))),
236
+ ];
237
+ }
238
+ function cleanupLines(cleanup) {
239
+ if (cleanup.length === 0)
240
+ return [];
241
+ return [
242
+ "Cleanup:",
243
+ ...alignedBlock(cleanup.map((c) => ({ left: c.command, right: c.reason })), "# "),
244
+ ];
245
+ }
246
+ /** Compact one-line marker used by the batch dashboard only. */
247
+ function batchMarker(issue) {
248
+ const parts = [`#${issue.number}`, `assess:action=${issue.action}`];
249
+ if (issue.phases?.length) {
250
+ parts.push(`assess:phases=${issue.phases.join(",")}`);
251
+ }
252
+ if (issue.qualityLoop !== undefined) {
253
+ parts.push(`assess:quality-loop=${issue.qualityLoop}`);
254
+ }
255
+ return `<!-- ${parts.join(" ")} -->`;
256
+ }
257
+ /** Three-line marker block used by single mode and every posted comment. */
258
+ function singleMarkers(issue) {
259
+ const lines = [`<!-- assess:action=${issue.action} -->`];
260
+ if (issue.phases?.length) {
261
+ lines.push(`<!-- assess:phases=${issue.phases.join(",")} -->`);
262
+ }
263
+ if (issue.qualityLoop !== undefined) {
264
+ lines.push(`<!-- assess:quality-loop=${issue.qualityLoop} -->`);
265
+ }
266
+ return lines;
267
+ }
268
+ // ---------------------------------------------------------------------------
269
+ // Batch dashboard
270
+ // ---------------------------------------------------------------------------
271
+ /**
272
+ * Render the batch dashboard: table → `Commands:` → annotations → markers.
273
+ *
274
+ * Every section is conditional (Section Visibility Rules). A section that has no
275
+ * content takes its adjacent separator with it, so an all-clear batch renders as
276
+ * table → separator → commands → separator → markers and nothing else.
277
+ */
278
+ export function renderBatch(result) {
279
+ const { issues, commandPrefix } = result;
280
+ // The `ACs` column is all-or-nothing: partial values erode trust in the table.
281
+ const withAcs = issues.every((issue) => issue.acCount !== undefined);
282
+ const rWidth = reasonWidth(withAcs);
283
+ const lines = [];
284
+ const header = " ".repeat(TABLE_INDENT) +
285
+ padTo("#", NUMBER_WIDTH) +
286
+ " ".repeat(COL_GAP) +
287
+ padTo("Action", ACTION_WIDTH) +
288
+ " ".repeat(COL_GAP) +
289
+ (withAcs ? padTo("ACs", ACS_WIDTH) + " ".repeat(COL_GAP) : "") +
290
+ padTo("Reason", rWidth) +
291
+ " ".repeat(COL_GAP) +
292
+ "Run";
293
+ lines.push(header);
294
+ for (const issue of issues) {
295
+ lines.push(" ".repeat(TABLE_INDENT) +
296
+ padTo(String(issue.number), NUMBER_WIDTH) +
297
+ " ".repeat(COL_GAP) +
298
+ padTo(issue.action, ACTION_WIDTH) +
299
+ " ".repeat(COL_GAP) +
300
+ (withAcs
301
+ ? padTo(String(issue.acCount), ACS_WIDTH) + " ".repeat(COL_GAP)
302
+ : "") +
303
+ fit(issue.reason, rWidth) +
304
+ " ".repeat(COL_GAP) +
305
+ // Never truncated: a clipped workflow loses information precisely on the
306
+ // issues with the longest and least predictable workflows (AC-9).
307
+ (issue.run ?? ""));
308
+ }
309
+ // Section visibility is decided in exactly one place — the builder. Callers
310
+ // ask "did that produce anything?" rather than re-deriving emptiness, so
311
+ // there is no second `.length` check to drift out of step with the first.
312
+ const commands = commandLines(result.commands ?? [], commandPrefix);
313
+ if (commands.length > 0) {
314
+ lines.push(SEPARATOR);
315
+ lines.push(...commands);
316
+ }
317
+ // Order: → ⚠ → Chain: → Flags: → Considered:, blank-line separated, inside
318
+ // one separator pair.
319
+ const annotations = [];
320
+ for (const order of result.orders ?? []) {
321
+ annotations.push(...wrapAnnotation(`Order: ${order}`, "Order: ".length));
322
+ }
323
+ if (result.warnings?.length) {
324
+ if (annotations.length > 0)
325
+ annotations.push("");
326
+ for (const warning of result.warnings) {
327
+ annotations.push(warning.issue !== undefined
328
+ ? `⚠ #${warning.issue} ${warning.text}`
329
+ : `⚠ ${warning.text}`);
330
+ }
331
+ }
332
+ if (result.chain) {
333
+ if (annotations.length > 0)
334
+ annotations.push("");
335
+ const chainIndent = "Chain: ".length;
336
+ annotations.push(`Chain: ${commandPrefix} ${result.chain.args}`);
337
+ annotations.push(...wrapAnnotation(`# alternative — ${result.chain.reason}`, chainIndent, chainIndent));
338
+ }
339
+ const flags = flagLines(result.flags ?? []);
340
+ if (flags.length > 0) {
341
+ if (annotations.length > 0)
342
+ annotations.push("");
343
+ annotations.push(...flags);
344
+ }
345
+ const considered = consideredLines(result.considered ?? []);
346
+ if (considered.length > 0) {
347
+ if (annotations.length > 0)
348
+ annotations.push("");
349
+ annotations.push(...considered);
350
+ }
351
+ if (annotations.length > 0) {
352
+ lines.push(SEPARATOR);
353
+ lines.push(...annotations);
354
+ }
355
+ const cleanup = cleanupLines(result.cleanup ?? []);
356
+ if (cleanup.length > 0) {
357
+ lines.push(SEPARATOR);
358
+ lines.push(...cleanup);
359
+ }
360
+ // Closing separator, but only if something opened one after the table.
361
+ if (commands.length > 0 || annotations.length > 0 || cleanup.length > 0) {
362
+ lines.push(SEPARATOR);
363
+ }
364
+ lines.push("");
365
+ lines.push(...issues.map(batchMarker));
366
+ return assemble(lines);
367
+ }
368
+ // ---------------------------------------------------------------------------
369
+ // Single mode
370
+ // ---------------------------------------------------------------------------
371
+ function singleHeader(issue) {
372
+ const meta = issue.labels?.length
373
+ ? `${issue.state} · ${issue.labels.join(", ")}`
374
+ : `${issue.state}`;
375
+ return [`#${issue.number} — ${issue.title}`, meta, SEPARATOR, ""];
376
+ }
377
+ /**
378
+ * Body for the verdicts that carry a workflow — PROCEED and REWRITE. Both use
379
+ * the same shape; REWRITE only differs in its verdict line and the `# fresh
380
+ * start` annotation the caller supplies on the command.
381
+ */
382
+ function workflowBody(issue, prefix) {
383
+ const lines = [];
384
+ const commands = commandLines(issue.command ? [issue.command] : [], prefix);
385
+ if (commands.length > 0) {
386
+ lines.push("");
387
+ lines.push(...commands);
388
+ }
389
+ if (issue.phases?.length) {
390
+ lines.push("");
391
+ const acs = issue.acCount !== undefined ? ` · ${issue.acCount} ACs` : "";
392
+ lines.push(`${issue.phases.join(" → ")}${acs}`);
393
+ }
394
+ const flags = flagLines(issue.flags ?? []);
395
+ if (flags.length > 0) {
396
+ lines.push("");
397
+ lines.push(...flags);
398
+ }
399
+ const considered = consideredLines(issue.considered ?? []);
400
+ if (considered.length > 0) {
401
+ lines.push("");
402
+ lines.push(...considered);
403
+ }
404
+ return lines;
405
+ }
406
+ /**
407
+ * Render the single-issue assessment for one verdict.
408
+ *
409
+ * PROCEED and REWRITE templates define a `⚠` region between two trailing
410
+ * separators. CLOSE / CLARIFY / PARK / MERGE do not, so a carried warning
411
+ * becomes its own separator-delimited block immediately above the markers — the
412
+ * one case where a posted comment extends a slot-less template.
413
+ */
414
+ export function renderSingle(result) {
415
+ const issue = result.issues[0];
416
+ const prefix = result.commandPrefix;
417
+ const lines = [...singleHeader(issue)];
418
+ if (issue.supersession)
419
+ lines.push(issue.supersession, "");
420
+ switch (issue.action) {
421
+ case "PROCEED":
422
+ lines.push(`→ PROCEED — ${issue.reason}`);
423
+ lines.push(...workflowBody(issue, prefix));
424
+ break;
425
+ case "REWRITE":
426
+ lines.push(`→ REWRITE — ${issue.reason}`);
427
+ lines.push(...workflowBody(issue, prefix));
428
+ break;
429
+ case "CLOSE":
430
+ lines.push(`→ CLOSE — ${issue.reason}`);
431
+ break;
432
+ case "CLARIFY":
433
+ lines.push(`→ CLARIFY — ${issue.reason}`);
434
+ lines.push("");
435
+ lines.push(`Need: ${issue.need}`);
436
+ if (issue.needDetail)
437
+ lines.push(`${BLOCK_INDENT}${issue.needDetail}`);
438
+ break;
439
+ case "PARK":
440
+ lines.push(`→ PARK — ${issue.reason}`);
441
+ lines.push(`${BLOCK_INDENT}Resume after: ${issue.resumeAfter}`);
442
+ break;
443
+ case "MERGE": {
444
+ lines.push(`→ MERGE → #${issue.mergeTarget} — ${issue.reason}`);
445
+ const rows = [
446
+ { left: "This issue:", right: issue.scopeSelf },
447
+ { left: "Target:", right: issue.scopeTarget },
448
+ ].filter((row) => row.right);
449
+ if (rows.length > 0)
450
+ lines.push(...alignedBlock(rows));
451
+ break;
452
+ }
453
+ }
454
+ lines.push(SEPARATOR);
455
+ const cleanup = cleanupLines(issue.cleanup ?? []);
456
+ if (cleanup.length > 0) {
457
+ lines.push(...cleanup);
458
+ lines.push(SEPARATOR);
459
+ }
460
+ if (issue.warnings?.length) {
461
+ lines.push(...issue.warnings.map((text) => `⚠ ${text}`));
462
+ lines.push(SEPARATOR);
463
+ }
464
+ lines.push("");
465
+ lines.push(...singleMarkers(issue));
466
+ return assemble(lines);
467
+ }
468
+ /** Dispatch on `mode`. The entry point the CLI subcommand calls. */
469
+ export function render(result) {
470
+ return result.mode === "batch" ? renderBatch(result) : renderSingle(result);
471
+ }
472
+ /**
473
+ * Geometry constants, exported for tests and for documenting the layout.
474
+ * Consumers should treat these as read-only.
475
+ */
476
+ export const GEOMETRY = {
477
+ RUN_COL,
478
+ SEPARATOR_WIDTH,
479
+ reasonCol,
480
+ reasonWidth,
481
+ };