mandrel 1.91.0 → 1.93.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 (112) hide show
  1. package/.agents/README.md +5 -5
  2. package/.agents/docs/SDLC.md +75 -107
  3. package/.agents/docs/configuration.md +1 -1
  4. package/.agents/docs/quality-gates.md +8 -7
  5. package/.agents/docs/workflows.md +4 -10
  6. package/.agents/instructions.md +2 -2
  7. package/.agents/schemas/agentrc.schema.json +1 -1
  8. package/.agents/schemas/lifecycle/README.md +2 -1
  9. package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
  10. package/.agents/schemas/risk-verdict.schema.json +14 -1
  11. package/.agents/scripts/README.md +0 -46
  12. package/.agents/scripts/analyze-execution.js +76 -1
  13. package/.agents/scripts/check-doc-links.js +3 -0
  14. package/.agents/scripts/check-workflow-cli-lint.js +208 -0
  15. package/.agents/scripts/epic-audit-recheck.js +1 -1
  16. package/.agents/scripts/epic-plan-clarity.js +16 -6
  17. package/.agents/scripts/epic-plan-decompose.js +37 -59
  18. package/.agents/scripts/epic-plan-healthcheck.js +39 -6
  19. package/.agents/scripts/epic-plan-spec.js +41 -168
  20. package/.agents/scripts/epic-reconcile.js +7 -1
  21. package/.agents/scripts/lib/Logger.js +4 -4
  22. package/.agents/scripts/lib/audit-suite/index.js +4 -5
  23. package/.agents/scripts/lib/audit-suite/runner.js +6 -4
  24. package/.agents/scripts/lib/audit-suite/selector.js +2 -3
  25. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  26. package/.agents/scripts/lib/command-header.js +20 -0
  27. package/.agents/scripts/lib/config/github.js +1 -3
  28. package/.agents/scripts/lib/config-settings-schema.js +4 -3
  29. package/.agents/scripts/lib/degraded-mode.js +1 -1
  30. package/.agents/scripts/lib/label-constants.js +18 -3
  31. package/.agents/scripts/lib/label-taxonomy.js +14 -3
  32. package/.agents/scripts/lib/mutation/baseline-snapshot.js +7 -6
  33. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +18 -8
  34. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +3 -4
  35. package/.agents/scripts/lib/orchestration/docs-digest.js +2 -2
  36. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +4 -0
  37. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +22 -12
  38. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +6 -1
  39. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +18 -2
  40. package/.agents/scripts/lib/orchestration/file-assumptions.js +68 -7
  41. package/.agents/scripts/{lifecycle-diff.js → lib/orchestration/lifecycle/ledger-diff.js} +10 -76
  42. package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +1 -1
  43. package/.agents/scripts/lib/orchestration/plan-context.js +696 -0
  44. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +177 -0
  45. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +99 -0
  46. package/.agents/scripts/lib/orchestration/plan-metrics.js +445 -0
  47. package/.agents/scripts/lib/orchestration/plan-persist/amend.js +359 -0
  48. package/.agents/scripts/lib/orchestration/plan-persist/delivery-mode.js +127 -0
  49. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +1015 -0
  50. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +206 -0
  51. package/.agents/scripts/lib/orchestration/plan-reachability.js +160 -0
  52. package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +3 -3
  53. package/.agents/scripts/lib/orchestration/planning-risk.js +1 -1
  54. package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
  55. package/.agents/scripts/lib/orchestration/ticket-validator.js +18 -1
  56. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +1 -1
  57. package/.agents/scripts/lib/orchestration/ticketing/reads.js +11 -2
  58. package/.agents/scripts/lib/plan-phase-cleanup.js +16 -0
  59. package/.agents/scripts/lib/presentation/manifest-persistence.js +2 -3
  60. package/.agents/scripts/lib/templates/spec-author-prompts.js +74 -0
  61. package/.agents/scripts/plan-context.js +204 -0
  62. package/.agents/scripts/plan-critics.js +199 -0
  63. package/.agents/scripts/plan-persist.js +383 -0
  64. package/.agents/scripts/pr-watch-with-update.js +8 -7
  65. package/.agents/scripts/run-lint.js +10 -11
  66. package/.agents/scripts/story-plan.js +19 -7
  67. package/.agents/scripts/sync-claude-commands.js +10 -5
  68. package/.agents/skills/core/knowledge-transfer/SKILL.md +11 -7
  69. package/.agents/workflows/audit-documentation.md +5 -7
  70. package/.agents/workflows/audit-lighthouse.md +1 -0
  71. package/.agents/workflows/audit-security.md +1 -0
  72. package/.agents/workflows/audit-to-stories.md +16 -5
  73. package/.agents/workflows/git-deliver.md +76 -243
  74. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  75. package/.agents/workflows/helpers/code-review.md +7 -6
  76. package/.agents/workflows/helpers/deliver-epic.md +5 -4
  77. package/.agents/workflows/helpers/plan-epic-reference.md +120 -96
  78. package/.agents/workflows/helpers/plan-epic.md +307 -994
  79. package/.agents/workflows/helpers/plan-story.md +14 -14
  80. package/.agents/workflows/helpers/scope-triage-gate.md +15 -4
  81. package/.agents/workflows/helpers/single-story-deliver.md +5 -4
  82. package/.agents/workflows/helpers/worktree-lifecycle.md +9 -7
  83. package/.agents/workflows/mandrel-update.md +144 -466
  84. package/.agents/workflows/plan.md +87 -145
  85. package/docs/CHANGELOG.md +20 -0
  86. package/lib/cli/registry.js +21 -3
  87. package/package.json +1 -1
  88. package/.agents/schemas/loop-unit.schema.json +0 -70
  89. package/.agents/scripts/assert-branch.js +0 -81
  90. package/.agents/scripts/check-loop-units.js +0 -204
  91. package/.agents/scripts/detect-merges.js +0 -111
  92. package/.agents/scripts/git-pr-quality-gate.js +0 -205
  93. package/.agents/scripts/git-rebase-and-resolve.js +0 -234
  94. package/.agents/scripts/hierarchy-gate.js +0 -192
  95. package/.agents/scripts/hydrate-context.js +0 -179
  96. package/.agents/scripts/lib/loop-units/validate-loop-unit.js +0 -197
  97. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/cli.js +0 -167
  98. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/cli-args.js +0 -69
  99. package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +0 -86
  100. package/.agents/scripts/loc-delta.js +0 -205
  101. package/.agents/scripts/render-manifest.js +0 -143
  102. package/.agents/scripts/run-audit-suite.js +0 -97
  103. package/.agents/scripts/select-audits.js +0 -155
  104. package/.agents/scripts/update-mutation-baseline.js +0 -189
  105. package/.agents/workflows/explain.md +0 -118
  106. package/.agents/workflows/git-merge-pr.md +0 -377
  107. package/.agents/workflows/helpers/epic-plan-decompose.md +0 -22
  108. package/.agents/workflows/helpers/epic-plan-spec.md +0 -22
  109. package/.agents/workflows/loops/README.md +0 -65
  110. package/.agents/workflows/loops/fix-failing-tests.md +0 -74
  111. package/.agents/workflows/loops/nightly-audit.md +0 -81
  112. package/.agents/workflows/loops/watch-ci.md +0 -68
@@ -1,204 +0,0 @@
1
- /**
2
- * CLI: loop-unit lint gate (Story #4288, Epic #4284).
3
- *
4
- * Validates every loop-unit markdown file under `.agents/workflows/loops/`
5
- * against `.agents/schemas/loop-unit.schema.json` via
6
- * `lib/loop-units/validate-loop-unit.js`. An absent or empty loops
7
- * directory is a **clean pass** (exit 0) — the gate only fails when a unit
8
- * file is present and invalid.
9
- *
10
- * On any invalid (or structurally unparseable) unit the CLI prints a
11
- * message naming the offending file and the missing/invalid field, then
12
- * exits non-zero. This is wired into `npm run lint` so a malformed loop
13
- * unit fails the lint gate.
14
- *
15
- * Flags:
16
- * --dir <path> override the loops directory (default
17
- * `.agents/workflows/loops`, resolved from cwd)
18
- * --json write a structured envelope to stdout instead of the
19
- * human-readable preview
20
- */
21
-
22
- import fs from 'node:fs';
23
- import path from 'node:path';
24
- import process from 'node:process';
25
- import { runAsCli } from './lib/cli-utils.js';
26
- import {
27
- LoopUnitParseError,
28
- validateLoopUnit,
29
- } from './lib/loop-units/validate-loop-unit.js';
30
-
31
- export const DEFAULT_LOOPS_DIR = path.join('.agents', 'workflows', 'loops');
32
-
33
- /**
34
- * Parse argv for `--dir <path>` and `--json`. Exported so tests can pin
35
- * the parser.
36
- *
37
- * @param {string[]} argv
38
- * @returns {{ dir: string | null, json: boolean }}
39
- */
40
- export function parseArgv(argv = []) {
41
- let dir = null;
42
- let json = false;
43
- for (let i = 0; i < argv.length; i += 1) {
44
- const a = argv[i];
45
- if (a === '--dir') {
46
- const next = argv[i + 1];
47
- if (next && !next.startsWith('--')) {
48
- dir = next;
49
- i += 1;
50
- }
51
- } else if (a === '--json') {
52
- json = true;
53
- }
54
- }
55
- return { dir, json };
56
- }
57
-
58
- /**
59
- * `README.md` (any case) under the loops directory is namespace
60
- * documentation, not a loop unit — it carries no `loop:` frontmatter and is
61
- * not projected as a `/loops:` command (see `sync-claude-commands.js`). It is
62
- * excluded from the loop-unit collector so the lint gate never flags the
63
- * directory's own README as a malformed unit.
64
- *
65
- * @param {string} name a directory-entry basename
66
- * @returns {boolean}
67
- */
68
- export function isLoopUnitFile(name) {
69
- return name.endsWith('.md') && name.toLowerCase() !== 'readme.md';
70
- }
71
-
72
- /**
73
- * Collect `*.md` loop-unit files directly under `dir`, sorted. Returns an
74
- * empty array when the directory is absent (the clean-pass case). The
75
- * directory's `README.md` is excluded — it is namespace documentation, not a
76
- * unit (see `isLoopUnitFile`).
77
- *
78
- * @param {string} dir absolute path
79
- * @returns {string[]} absolute paths
80
- */
81
- export function collectLoopUnitFiles(dir) {
82
- let entries;
83
- try {
84
- entries = fs.readdirSync(dir, { withFileTypes: true });
85
- } catch {
86
- return [];
87
- }
88
- return entries
89
- .filter((e) => e.isFile() && isLoopUnitFile(e.name))
90
- .map((e) => path.join(dir, e.name))
91
- .sort();
92
- }
93
-
94
- /**
95
- * Validate every loop unit under `dir`. Returns the per-file results and a
96
- * roll-up `failures` array carrying `{ file, issues }` for each invalid or
97
- * unparseable unit.
98
- *
99
- * @param {string} dir absolute path
100
- * @param {{ schemaPath?: string }} [opts]
101
- * @returns {{ files: string[], failures: Array<{ file: string, issues: Array<{path:string,message:string}> }> }}
102
- */
103
- export function checkLoopUnits(dir, opts = {}) {
104
- const files = collectLoopUnitFiles(dir);
105
- const failures = [];
106
- for (const file of files) {
107
- try {
108
- const { valid, issues } = validateLoopUnit(file, opts);
109
- if (!valid) failures.push({ file, issues });
110
- } catch (err) {
111
- if (err instanceof LoopUnitParseError) {
112
- failures.push({ file, issues: [{ path: '/', message: err.reason }] });
113
- } else {
114
- throw err;
115
- }
116
- }
117
- }
118
- return { files, failures };
119
- }
120
-
121
- /**
122
- * Render the human-readable report. Each failure lists the offending file
123
- * and one line per issue naming the field path and message.
124
- *
125
- * @param {{ files: string[], failures: Array<{ file: string, issues: Array<{path:string,message:string}> }> }} result
126
- * @returns {string}
127
- */
128
- export function renderReport({ files, failures }) {
129
- const lines = [];
130
- if (files.length === 0) {
131
- lines.push('[check-loop-units] no loop units found (ok)');
132
- return lines.join('\n');
133
- }
134
- for (const { file, issues } of failures) {
135
- lines.push(`✖ ${file}`);
136
- for (const issue of issues) {
137
- lines.push(` ${issue.path}: ${issue.message}`);
138
- }
139
- }
140
- const tag = failures.length > 0 ? '(gate fail)' : '(ok)';
141
- lines.push(
142
- `[check-loop-units] checked=${files.length} invalid=${failures.length} ${tag}`,
143
- );
144
- return lines.join('\n');
145
- }
146
-
147
- /**
148
- * Top-level CLI entry. Exported so tests can drive the full pipeline
149
- * against a tmpdir fixture directory.
150
- *
151
- * @param {{
152
- * argv?: string[],
153
- * cwd?: string,
154
- * stdout?: { write: (s: string) => void },
155
- * stderr?: { write: (s: string) => void },
156
- * }} [opts]
157
- * @returns {Promise<number>} 0 = clean; 1 = at least one invalid unit
158
- */
159
- export async function runCli({
160
- argv = process.argv.slice(2),
161
- cwd = process.cwd(),
162
- stdout = process.stdout,
163
- stderr = process.stderr,
164
- } = {}) {
165
- const { dir, json } = parseArgv(argv);
166
- const loopsDir = path.resolve(cwd, dir ?? DEFAULT_LOOPS_DIR);
167
- const result = checkLoopUnits(loopsDir);
168
- const exitCode = result.failures.length > 0 ? 1 : 0;
169
-
170
- if (json) {
171
- stdout.write(
172
- `${JSON.stringify(
173
- {
174
- kind: 'loop-units-report',
175
- dir: loopsDir,
176
- checked: result.files.length,
177
- failures: result.failures,
178
- exitCode,
179
- },
180
- null,
181
- 2,
182
- )}\n`,
183
- );
184
- } else {
185
- const report = renderReport(result);
186
- if (exitCode === 0) {
187
- stdout.write(`${report}\n`);
188
- } else {
189
- stderr.write(`${report}\n`);
190
- }
191
- }
192
-
193
- return exitCode;
194
- }
195
-
196
- async function main() {
197
- return runCli();
198
- }
199
-
200
- runAsCli(import.meta.url, main, {
201
- source: 'check-loop-units',
202
- propagateExitCode: true,
203
- errorPrefix: '[check-loop-units] ❌ Fatal error',
204
- });
@@ -1,111 +0,0 @@
1
- /* node:coverage ignore file -- pre-push conflict-marker scanner; pure I/O glue over `git ls-files` */
2
-
3
- import { execFileSync } from 'node:child_process';
4
- import fs from 'node:fs';
5
- import path from 'node:path';
6
- import { runAsCli } from './lib/cli-utils.js';
7
- import { Logger } from './lib/Logger.js';
8
- import { concurrentMap } from './lib/util/concurrent-map.js';
9
-
10
- // Paths under these prefixes are treated as template/documentation files
11
- // that may legitimately contain literal conflict-marker strings (e.g. the
12
- // merge-conflict runbook). Paths use forward slashes to match `git ls-files`.
13
- export const TEMPLATE_PATH_PREFIXES = ['.agents/workflows/'];
14
-
15
- // Standard git conflict markers. The leading '\n' on '=======' avoids
16
- // matching plain separator lines.
17
- const CONFLICT_MARKERS = ['<<<<<<< ', '\n=======', '>>>>>>> '];
18
-
19
- const SELF_PATH = '.agents/scripts/detect-merges.js';
20
-
21
- // Test fixtures for detect-merges itself embed literal conflict-marker strings
22
- // to exercise the scanner. Matches any file named `detect-merges*.js` (including
23
- // `.test.js`) under any depth below `tests/`.
24
- const TEST_FIXTURE_PATTERN = /(^|\/)tests\/(?:[^/]+\/)*detect-merges[^/]*\.js$/;
25
-
26
- // Cap concurrent `fs.readFile` calls. Unbounded `Promise.all` over every
27
- // tracked file blows past EMFILE on large repos and starves the event loop;
28
- // 64 keeps the kernel happy while still saturating typical SSDs.
29
- export const FILE_READ_CAP = 64;
30
-
31
- export function isTemplatePath(file) {
32
- return TEMPLATE_PATH_PREFIXES.some((prefix) => file.startsWith(prefix));
33
- }
34
-
35
- function isDetectMergesTestFixture(file) {
36
- return TEST_FIXTURE_PATTERN.test(file);
37
- }
38
-
39
- /**
40
- * Scan the given files for conflict markers, skipping templates and self.
41
- *
42
- * @param {string[]} files - Repo-relative paths (forward slashes).
43
- * @param {string} root - Repo root used to resolve file paths.
44
- * @returns {Promise<Array<{ file: string, marker: string }>>}
45
- */
46
- export async function scanForConflicts(files, root) {
47
- const mapped = await concurrentMap(
48
- files,
49
- async (file) => {
50
- if (
51
- file === SELF_PATH ||
52
- isTemplatePath(file) ||
53
- isDetectMergesTestFixture(file)
54
- ) {
55
- return null;
56
- }
57
- try {
58
- const content = await fs.promises.readFile(
59
- path.join(root, file),
60
- 'utf8',
61
- );
62
- for (const marker of CONFLICT_MARKERS) {
63
- if (content.includes(marker)) {
64
- return { file, marker };
65
- }
66
- }
67
- return null;
68
- } catch (_readErr) {
69
- // Ignore unreadable files (binaries, broken symlinks, etc.).
70
- return null;
71
- }
72
- },
73
- { concurrency: FILE_READ_CAP },
74
- );
75
- // `concurrentMap` preserves input order, so filtering yields a hit list in
76
- // the same order as `files`. Sort by file for byte-stable output regardless
77
- // of caller-supplied ordering.
78
- const hits = mapped.filter((hit) => hit !== null);
79
- hits.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
80
- return hits;
81
- }
82
-
83
- export async function main() {
84
- try {
85
- const root = process.cwd();
86
- const filesOutput = execFileSync('git', ['ls-files'], {
87
- cwd: root,
88
- }).toString();
89
- const files = filesOutput.split('\n').filter(Boolean);
90
-
91
- const hits = await scanForConflicts(files, root);
92
-
93
- if (hits.length > 0) {
94
- for (const { file, marker } of hits) {
95
- Logger.error(
96
- `Conflict marker '${marker.trim()}' found in tracked file: ${file}`,
97
- );
98
- }
99
- throw new Error(
100
- '\nERROR: Merge conflicts detected. Please resolve them before proceeding.',
101
- );
102
- } else {
103
- Logger.info('No conflict markers found in tracked files.');
104
- process.exit(0);
105
- }
106
- } catch (err) {
107
- throw new Error(`Error detecting merges: ${err.message}`);
108
- }
109
- }
110
-
111
- runAsCli(import.meta.url, main, { source: 'detect-merges' });
@@ -1,205 +0,0 @@
1
- #!/usr/bin/env node
2
- /* node:coverage ignore file -- top-level CLI gate; spawns lint/format/test and asserts exit codes — heavy mocking would assert only mock structure */
3
-
4
- /**
5
- * git-pr-quality-gate.js — Lint / test / baselines gate for `/git-merge-pr`.
6
- *
7
- * `/git-merge-pr` Steps 3–4 previously hardcoded the command sequence
8
- * (`npm run lint`, `npm run format:check`, `npm test`) in the workflow
9
- * markdown. That coupled the .md to specific tooling names; a project that
10
- * renames `lint` to `lint:ci` or swaps Biome for ESLint had to patch the
11
- * skill every time.
12
- *
13
- * This script runs the gate and emits a structured result so the .md routes
14
- * on outcome rather than re-implementing the command sequence. The checks it
15
- * runs are read from
16
- * `.agentrc.json → github.branchProtection.requiredChecks` when present,
17
- * falling back to the hardcoded default trio, which mirrors the live required
18
- * check set (`lint`, `test`, `baselines` — Story #4356, Epic #4355; the stale
19
- * `lifecycle-doc-drift` check was pruned in the same slice).
20
- *
21
- * Usage:
22
- * node .agents/scripts/git-pr-quality-gate.js [--json] [--skip <name>[,<name>]]
23
- *
24
- * `--skip` takes a comma-separated list of check names to bypass (useful when
25
- * a flake needs a targeted rerun).
26
- *
27
- * Output (always JSON when --json; human-readable otherwise):
28
- * {
29
- * ok: boolean,
30
- * checks: [{ name, cmd, status, stdout, stderr, durationMs }, ...],
31
- * failed: [{ name, reason }, ...]
32
- * }
33
- *
34
- * Exit codes:
35
- * 0 — every check passed.
36
- * 1 — one or more checks failed (see `failed[]`).
37
- * 2 — usage / config error.
38
- */
39
-
40
- import { spawnSync } from 'node:child_process';
41
- import { parseArgs } from 'node:util';
42
- import { runAsCli } from './lib/cli-utils.js';
43
- import { PROJECT_ROOT, resolveConfig } from './lib/config-resolver.js';
44
- import { Logger } from './lib/Logger.js';
45
-
46
- /**
47
- * Default check suite when `.agentrc.json` supplies no override. Each entry:
48
- * name — short identifier surfaced in output
49
- * cmd — argv array (no shell expansion; first element is the binary)
50
- */
51
- export const DEFAULT_CHECKS = Object.freeze([
52
- { name: 'lint', cmd: ['npm', 'run', 'lint'] },
53
- { name: 'test', cmd: ['npm', 'test'] },
54
- { name: 'baselines', cmd: ['node', '.agents/scripts/check-baselines.js'] },
55
- ]);
56
-
57
- function resolveChecks(config) {
58
- // Canonical: required checks live under `github.branchProtection.requiredChecks`.
59
- const configured = config?.github?.branchProtection?.requiredChecks;
60
- if (!Array.isArray(configured) || configured.length === 0) {
61
- return DEFAULT_CHECKS;
62
- }
63
- return configured.map((c) => ({ name: c.name, cmd: c.cmd }));
64
- }
65
-
66
- function runCheck(check, cwd) {
67
- const start = Date.now();
68
- const [bin, ...args] = check.cmd;
69
- const result = spawnSync(bin, args, {
70
- cwd,
71
- encoding: 'utf8',
72
- shell: process.platform === 'win32',
73
- stdio: ['pipe', 'pipe', 'pipe'],
74
- timeout: 600_000,
75
- });
76
- const durationMs = Date.now() - start;
77
- return {
78
- name: check.name,
79
- cmd: check.cmd.join(' '),
80
- status: result.status ?? -1,
81
- stdout: (result.stdout ?? '').slice(-4000),
82
- stderr: (result.stderr ?? '').slice(-4000),
83
- durationMs,
84
- };
85
- }
86
-
87
- /**
88
- * Pure test seam: run the supplied check suite with a caller-provided runner.
89
- *
90
- * @param {{
91
- * checks: Array<{ name: string, cmd: string[] }>,
92
- * cwd?: string,
93
- * skip?: Set<string>|string[],
94
- * runner?: (check: {name: string, cmd: string[]}, cwd: string) => { name, cmd, status, stdout, stderr, durationMs },
95
- * }} opts
96
- */
97
- export function runQualityGate({
98
- checks,
99
- cwd = PROJECT_ROOT,
100
- skip,
101
- runner = runCheck,
102
- }) {
103
- const skipSet =
104
- skip instanceof Set ? skip : new Set((skip ?? []).filter(Boolean));
105
- const results = [];
106
- for (const c of checks) {
107
- if (skipSet.has(c.name)) {
108
- results.push({
109
- name: c.name,
110
- cmd: c.cmd.join(' '),
111
- status: 0,
112
- stdout: '',
113
- stderr: '',
114
- durationMs: 0,
115
- skipped: true,
116
- });
117
- continue;
118
- }
119
- results.push(runner(c, cwd));
120
- }
121
- const failed = results
122
- .filter((r) => !r.skipped && r.status !== 0)
123
- .map((r) => ({
124
- name: r.name,
125
- reason: `exit ${r.status} — ${(r.stderr || r.stdout || '').trim().split('\n').slice(0, 3).join(' | ')}`,
126
- }));
127
- return { ok: failed.length === 0, checks: results, failed };
128
- }
129
-
130
- /**
131
- * Pure: format the per-check status line shown in the human report.
132
- *
133
- * @param {{ name: string, status: number, durationMs: number, skipped?: boolean }} check
134
- */
135
- export function formatCheckLine(check) {
136
- const icon = check.skipped ? '⏭' : check.status === 0 ? '✅' : '❌';
137
- const suffix = check.skipped ? ' (skipped)' : ` (${check.durationMs}ms)`;
138
- return `[quality-gate] ${icon} ${check.name}${suffix}`;
139
- }
140
-
141
- /**
142
- * Pure: render the human report for a quality-gate verdict, returning info
143
- * lines (always emitted) and error lines (emitted on failure).
144
- *
145
- * @param {{ ok: boolean, checks: Array<object>, failed: Array<{ name: string, reason: string }> }} result
146
- */
147
- export function renderHumanReport(result) {
148
- const info = result.checks.map(formatCheckLine);
149
- const errors = [];
150
- if (!result.ok) {
151
- errors.push(
152
- `[quality-gate] ❌ ${result.failed.length} check(s) failed:`,
153
- ...result.failed.map((f) => ` - ${f.name}: ${f.reason}`),
154
- );
155
- } else {
156
- info.push(`[quality-gate] ✅ All ${result.checks.length} check(s) passed.`);
157
- }
158
- return { info, errors };
159
- }
160
-
161
- /**
162
- * Pure: split a comma-separated `--skip` value into a clean list of check
163
- * names.
164
- *
165
- * @param {string|null|undefined} value
166
- */
167
- export function parseSkipList(value) {
168
- return String(value ?? '')
169
- .split(',')
170
- .map((s) => s.trim())
171
- .filter(Boolean);
172
- }
173
-
174
- /* node:coverage ignore next */
175
- async function main() {
176
- const { values } = parseArgs({
177
- options: {
178
- json: { type: 'boolean', default: false },
179
- skip: { type: 'string' },
180
- },
181
- strict: false,
182
- });
183
- const config = resolveConfig();
184
- const checks = resolveChecks(config);
185
- const skip = parseSkipList(values.skip);
186
-
187
- const result = runQualityGate({ checks, skip });
188
-
189
- if (values.json === true) {
190
- process.stdout.write(`${JSON.stringify(result)}\n`);
191
- if (!result.ok) process.exit(1);
192
- return;
193
- }
194
-
195
- const { info, errors } = renderHumanReport(result);
196
- for (const line of info) Logger.info(line);
197
- for (const line of errors) Logger.error(line);
198
- if (!result.ok) process.exit(1);
199
- }
200
-
201
- // Re-export Logger so callers that dynamic-import this module can surface
202
- // fatal errors through the same channel as other scripts.
203
- export { Logger };
204
-
205
- runAsCli(import.meta.url, main, { source: 'git-pr-quality-gate' });