mandrel 1.90.0 → 1.92.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/.agents/README.md +5 -5
- package/.agents/docs/SDLC.md +75 -107
- package/.agents/docs/configuration.md +1 -1
- package/.agents/docs/quality-gates.md +8 -7
- package/.agents/docs/workflows.md +4 -10
- package/.agents/instructions.md +2 -2
- package/.agents/schemas/agentrc.schema.json +1 -1
- package/.agents/schemas/lifecycle/README.md +2 -1
- package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
- package/.agents/schemas/risk-verdict.schema.json +14 -1
- package/.agents/scripts/README.md +0 -46
- package/.agents/scripts/analyze-execution.js +76 -1
- package/.agents/scripts/check-doc-links.js +3 -0
- package/.agents/scripts/check-workflow-cli-lint.js +208 -0
- package/.agents/scripts/epic-audit-recheck.js +1 -1
- package/.agents/scripts/epic-deliver-preflight.js +37 -1
- package/.agents/scripts/epic-plan-clarity.js +16 -6
- package/.agents/scripts/epic-plan-decompose.js +37 -59
- package/.agents/scripts/epic-plan-healthcheck.js +39 -6
- package/.agents/scripts/epic-plan-spec.js +41 -168
- package/.agents/scripts/epic-reconcile.js +7 -1
- package/.agents/scripts/lib/Logger.js +4 -4
- package/.agents/scripts/lib/audit-suite/index.js +4 -5
- package/.agents/scripts/lib/audit-suite/runner.js +6 -4
- package/.agents/scripts/lib/audit-suite/selector.js +2 -3
- package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
- package/.agents/scripts/lib/command-header.js +20 -0
- package/.agents/scripts/lib/config/github.js +1 -3
- package/.agents/scripts/lib/config-settings-schema.js +4 -3
- package/.agents/scripts/lib/degraded-mode.js +1 -1
- package/.agents/scripts/lib/label-constants.js +18 -3
- package/.agents/scripts/lib/label-taxonomy.js +14 -3
- package/.agents/scripts/lib/mutation/baseline-snapshot.js +7 -6
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +18 -8
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +3 -4
- package/.agents/scripts/lib/orchestration/docs-digest.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +4 -0
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +22 -12
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +6 -1
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +18 -2
- package/.agents/scripts/{lifecycle-diff.js → lib/orchestration/lifecycle/ledger-diff.js} +10 -76
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +32 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +510 -0
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +177 -0
- package/.agents/scripts/lib/orchestration/plan-metrics.js +445 -0
- package/.agents/scripts/lib/orchestration/plan-persist/amend.js +359 -0
- package/.agents/scripts/lib/orchestration/plan-persist/delivery-mode.js +127 -0
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +978 -0
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +191 -0
- package/.agents/scripts/lib/orchestration/plan-reachability.js +160 -0
- package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +3 -3
- package/.agents/scripts/lib/orchestration/planning-risk.js +1 -1
- package/.agents/scripts/lib/orchestration/remote-verifier.js +165 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +10 -0
- package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +1 -1
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +11 -2
- package/.agents/scripts/lib/plan-phase-cleanup.js +16 -0
- package/.agents/scripts/lib/presentation/manifest-persistence.js +2 -3
- package/.agents/scripts/lib/templates/spec-author-prompts.js +74 -0
- package/.agents/scripts/plan-context.js +186 -0
- package/.agents/scripts/plan-critics.js +227 -0
- package/.agents/scripts/plan-persist.js +383 -0
- package/.agents/scripts/pr-watch-with-update.js +8 -7
- package/.agents/scripts/run-lint.js +10 -11
- package/.agents/scripts/single-story-init.js +22 -0
- package/.agents/scripts/story-plan.js +19 -7
- package/.agents/scripts/sync-claude-commands.js +10 -5
- package/.agents/skills/core/knowledge-transfer/SKILL.md +11 -7
- package/.agents/workflows/audit-documentation.md +5 -7
- package/.agents/workflows/audit-lighthouse.md +1 -0
- package/.agents/workflows/audit-security.md +1 -0
- package/.agents/workflows/audit-to-stories.md +16 -5
- package/.agents/workflows/deliver.md +8 -0
- package/.agents/workflows/git-deliver.md +76 -243
- package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
- package/.agents/workflows/helpers/code-review.md +7 -6
- package/.agents/workflows/helpers/deliver-epic.md +16 -4
- package/.agents/workflows/helpers/plan-epic-reference.md +109 -96
- package/.agents/workflows/helpers/plan-epic.md +246 -999
- package/.agents/workflows/helpers/plan-story.md +14 -14
- package/.agents/workflows/helpers/scope-triage-gate.md +6 -4
- package/.agents/workflows/helpers/single-story-deliver.md +13 -4
- package/.agents/workflows/helpers/worktree-lifecycle.md +9 -7
- package/.agents/workflows/mandrel-update.md +144 -466
- package/.agents/workflows/plan.md +73 -143
- package/docs/CHANGELOG.md +20 -0
- package/lib/cli/registry.js +21 -3
- package/package.json +1 -1
- package/.agents/schemas/loop-unit.schema.json +0 -70
- package/.agents/scripts/assert-branch.js +0 -81
- package/.agents/scripts/check-loop-units.js +0 -204
- package/.agents/scripts/detect-merges.js +0 -111
- package/.agents/scripts/git-pr-quality-gate.js +0 -205
- package/.agents/scripts/git-rebase-and-resolve.js +0 -234
- package/.agents/scripts/hierarchy-gate.js +0 -192
- package/.agents/scripts/hydrate-context.js +0 -179
- package/.agents/scripts/lib/loop-units/validate-loop-unit.js +0 -197
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/cli.js +0 -167
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/cli-args.js +0 -69
- package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +0 -86
- package/.agents/scripts/loc-delta.js +0 -205
- package/.agents/scripts/render-manifest.js +0 -143
- package/.agents/scripts/run-audit-suite.js +0 -97
- package/.agents/scripts/select-audits.js +0 -155
- package/.agents/scripts/update-mutation-baseline.js +0 -189
- package/.agents/workflows/explain.md +0 -118
- package/.agents/workflows/git-merge-pr.md +0 -377
- package/.agents/workflows/helpers/epic-plan-decompose.md +0 -22
- package/.agents/workflows/helpers/epic-plan-spec.md +0 -22
- package/.agents/workflows/loops/README.md +0 -65
- package/.agents/workflows/loops/fix-failing-tests.md +0 -74
- package/.agents/workflows/loops/nightly-audit.md +0 -81
- 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' });
|