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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +20 -5
- package/dist/bin/cli.js +143 -18
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
- package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
- package/dist/marketplace/external_plugins/sequant/README.md +56 -2
- package/dist/marketplace/external_plugins/sequant/hooks/parallel-marker.sh +50 -0
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +20 -7
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +75 -10
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/trust-model.md +18 -0
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +215 -390
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +4 -4
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +26 -6
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +3 -1
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +38 -2
- package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +337 -57
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +6 -6
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +21 -7
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +11 -11
- package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
- package/dist/src/commands/assess-render.d.ts +23 -0
- package/dist/src/commands/assess-render.js +60 -0
- package/dist/src/commands/doctor.js +8 -13
- package/dist/src/commands/init.js +13 -1
- package/dist/src/commands/locks.d.ts +20 -1
- package/dist/src/commands/locks.js +208 -4
- package/dist/src/commands/logs.js +14 -5
- package/dist/src/commands/merge.js +154 -2
- package/dist/src/commands/ready-tui-adapter.js +6 -1
- package/dist/src/commands/ready.d.ts +31 -3
- package/dist/src/commands/ready.js +53 -13
- package/dist/src/commands/resume.d.ts +113 -0
- package/dist/src/commands/resume.js +193 -0
- package/dist/src/commands/run-display.js +38 -4
- package/dist/src/commands/run-flags.d.ts +42 -1
- package/dist/src/commands/run-flags.js +53 -1
- package/dist/src/commands/run-progress.js +26 -1
- package/dist/src/commands/run.js +11 -10
- package/dist/src/commands/state.js +7 -0
- package/dist/src/commands/status.d.ts +9 -0
- package/dist/src/commands/status.js +24 -1
- package/dist/src/commands/sync.js +90 -19
- package/dist/src/commands/update.js +28 -5
- package/dist/src/commands/worktree.d.ts +31 -0
- package/dist/src/commands/worktree.js +95 -0
- package/dist/src/lib/ac-parser.d.ts +14 -0
- package/dist/src/lib/ac-parser.js +99 -5
- package/dist/src/lib/assess/index.d.ts +10 -0
- package/dist/src/lib/assess/index.js +9 -0
- package/dist/src/lib/assess/renderer.d.ts +64 -0
- package/dist/src/lib/assess/renderer.js +481 -0
- package/dist/src/lib/assess/types.d.ts +224 -0
- package/dist/src/lib/assess/types.js +241 -0
- package/dist/src/lib/ci/outputs.d.ts +15 -2
- package/dist/src/lib/ci/outputs.js +17 -5
- package/dist/src/lib/cli-flags.d.ts +56 -0
- package/dist/src/lib/cli-flags.js +89 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +23 -7
- package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
- package/dist/src/lib/cli-ui/run-renderer.js +69 -1
- package/dist/src/lib/errors.d.ts +78 -6
- package/dist/src/lib/errors.js +133 -15
- package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
- package/dist/src/lib/locks/checkout-lock.js +389 -0
- package/dist/src/lib/locks/index.d.ts +7 -4
- package/dist/src/lib/locks/index.js +5 -3
- package/dist/src/lib/locks/lock-manager.d.ts +101 -3
- package/dist/src/lib/locks/lock-manager.js +277 -8
- package/dist/src/lib/locks/types.d.ts +106 -3
- package/dist/src/lib/locks/types.js +44 -0
- package/dist/src/lib/mcp-config.d.ts +45 -0
- package/dist/src/lib/mcp-config.js +77 -1
- package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
- package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
- package/dist/src/lib/merge-check/command-result.d.ts +36 -0
- package/dist/src/lib/merge-check/command-result.js +56 -0
- package/dist/src/lib/merge-check/index.d.ts +4 -0
- package/dist/src/lib/merge-check/index.js +1 -1
- package/dist/src/lib/merge-check/report.js +5 -1
- package/dist/src/lib/merge-check/types.d.ts +9 -0
- package/dist/src/lib/merge-check/watch.d.ts +121 -0
- package/dist/src/lib/merge-check/watch.js +215 -0
- package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
- package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
- package/dist/src/lib/settings.d.ts +87 -0
- package/dist/src/lib/settings.js +106 -8
- package/dist/src/lib/shutdown.d.ts +42 -2
- package/dist/src/lib/shutdown.js +75 -5
- package/dist/src/lib/skills-check.d.ts +26 -0
- package/dist/src/lib/skills-check.js +31 -0
- package/dist/src/lib/stacks.d.ts +110 -1
- package/dist/src/lib/stacks.js +244 -5
- package/dist/src/lib/templates.d.ts +74 -0
- package/dist/src/lib/templates.js +171 -16
- package/dist/src/lib/test-tautology-detector.d.ts +10 -2
- package/dist/src/lib/test-tautology-detector.js +213 -12
- package/dist/src/lib/version-check.d.ts +31 -0
- package/dist/src/lib/version-check.js +45 -2
- package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
- package/dist/src/lib/workflow/batch-executor.js +552 -60
- package/dist/src/lib/workflow/chain-resume.d.ts +13 -11
- package/dist/src/lib/workflow/chain-resume.js +24 -20
- package/dist/src/lib/workflow/completed-status.d.ts +52 -0
- package/dist/src/lib/workflow/completed-status.js +57 -0
- package/dist/src/lib/workflow/config-resolver.d.ts +51 -0
- package/dist/src/lib/workflow/config-resolver.js +143 -2
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +22 -0
- package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
- package/dist/src/lib/workflow/drivers/aider.js +2 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +14 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +25 -2
- package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
- package/dist/src/lib/workflow/effort-escalation.js +82 -0
- package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
- package/dist/src/lib/workflow/error-classifier.js +4 -0
- package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
- package/dist/src/lib/workflow/git-diff-utils.js +77 -4
- package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
- package/dist/src/lib/workflow/heartbeat.js +90 -0
- package/dist/src/lib/workflow/log-writer.d.ts +54 -2
- package/dist/src/lib/workflow/log-writer.js +95 -6
- package/dist/src/lib/workflow/metrics-schema.d.ts +52 -9
- package/dist/src/lib/workflow/metrics-schema.js +33 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
- package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
- package/dist/src/lib/workflow/phase-detection.js +5 -1
- package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
- package/dist/src/lib/workflow/phase-executor.js +467 -52
- package/dist/src/lib/workflow/phase-registry.js +2 -1
- package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
- package/dist/src/lib/workflow/platforms/github.js +152 -0
- package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
- package/dist/src/lib/workflow/qa-cache.js +22 -9
- package/dist/src/lib/workflow/ready-gate.d.ts +45 -4
- package/dist/src/lib/workflow/ready-gate.js +49 -12
- package/dist/src/lib/workflow/run-log-schema.d.ts +69 -0
- package/dist/src/lib/workflow/run-log-schema.js +78 -2
- package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
- package/dist/src/lib/workflow/run-orchestrator.js +168 -20
- package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
- package/dist/src/lib/workflow/skills-preflight.js +112 -0
- package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
- package/dist/src/lib/workflow/spec-recommendation.js +142 -0
- package/dist/src/lib/workflow/state-cleanup.js +9 -2
- package/dist/src/lib/workflow/state-manager.d.ts +33 -0
- package/dist/src/lib/workflow/state-manager.js +83 -0
- package/dist/src/lib/workflow/state-schema.d.ts +26 -0
- package/dist/src/lib/workflow/state-schema.js +47 -0
- package/dist/src/lib/workflow/types.d.ts +190 -4
- package/dist/src/lib/workflow/types.js +1 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +36 -3
- package/dist/src/lib/workflow/worktree-manager.js +111 -24
- package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
- package/dist/src/lib/workflow/worktree-resolver.js +126 -0
- package/dist/src/mcp/tools/run.d.ts +9 -1
- package/dist/src/mcp/tools/run.js +20 -1
- package/dist/src/mcp/tools/status.js +6 -0
- package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
- package/dist/src/ui/tui/ElapsedTimer.js +12 -10
- package/dist/src/ui/tui/IssueBox.js +10 -2
- package/dist/src/ui/tui/row-cap.js +2 -1
- package/dist/src/ui/tui/theme.d.ts +7 -0
- package/dist/src/ui/tui/theme.js +9 -0
- package/package.json +10 -8
- package/templates/hooks/parallel-marker.sh +50 -0
- package/templates/hooks/post-tool.sh +20 -7
- package/templates/hooks/pre-tool.sh +303 -10
- package/templates/scripts/cleanup-worktree.sh +217 -33
- package/templates/scripts/new-feature.sh +289 -35
- package/templates/skills/_shared/references/subagent-types.md +7 -18
- package/templates/skills/_shared/references/trust-model.md +18 -0
- package/templates/skills/assess/SKILL.md +220 -391
- package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
- package/templates/skills/docs/SKILL.md +4 -4
- package/templates/skills/exec/SKILL.md +87 -13
- package/templates/skills/fullsolve/SKILL.md +130 -22
- package/templates/skills/loop/SKILL.md +94 -13
- package/templates/skills/merger/SKILL.md +100 -12
- package/templates/skills/qa/SKILL.md +396 -63
- package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
- package/templates/skills/qa/references/call-site-review.md +2 -2
- package/templates/skills/qa/references/code-review-checklist.md +2 -2
- package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/templates/skills/qa/references/quality-gates.md +21 -7
- package/templates/skills/qa/references/testing-requirements.md +1 -1
- package/templates/skills/qa/scripts/quality-checks.sh +11 -11
- package/templates/skills/release/SKILL.md +80 -1
- package/templates/skills/setup/SKILL.md +5 -5
- package/templates/skills/spec/SKILL.md +33 -15
- package/templates/skills/spec/references/recommended-workflow.md +14 -1
- package/templates/skills/test/SKILL.md +1 -1
- package/templates/skills/testgen/SKILL.md +23 -6
- package/dist/src/lib/phase-spinner.d.ts +0 -146
- package/dist/src/lib/phase-spinner.js +0 -255
- package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
- package/dist/src/lib/workflow/pr-operations.js +0 -326
- package/dist/src/lib/workflow/run-summary.d.ts +0 -36
- package/dist/src/lib/workflow/run-summary.js +0 -142
- 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
|
+
};
|