@mmerterden/multi-agent-pipeline 16.27.0 → 16.28.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/CHANGELOG.md +71 -1
- package/package.json +4 -4
- package/pipeline/commands/multi-agent/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/issue/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/jira/SKILL.md +3 -0
- package/pipeline/commands/multi-agent/log/SKILL.md +7 -1
- package/pipeline/commands/multi-agent/review-issue/SKILL.md +1 -0
- package/pipeline/lib/issue-fetcher.sh +134 -5
- package/pipeline/lib/multi-repo-pipeline.sh +8 -0
- package/pipeline/multi-agent-refs/analysis/evidence.md +1 -1
- package/pipeline/multi-agent-refs/analysis/intake.md +11 -2
- package/pipeline/multi-agent-refs/analysis/locked.md +1 -0
- package/pipeline/multi-agent-refs/analysis/redesign.md +112 -0
- package/pipeline/multi-agent-refs/analysis/render.md +5 -0
- package/pipeline/multi-agent-refs/analysis/resolve.md +1 -0
- package/pipeline/multi-agent-refs/analysis/review.md +15 -0
- package/pipeline/multi-agent-refs/analysis/synthesis.md +1 -1
- package/pipeline/multi-agent-refs/analysis-template-corporate.md +3 -3
- package/pipeline/multi-agent-refs/analysis-template.md +36 -0
- package/pipeline/multi-agent-refs/cross-cli-contract.md +3 -0
- package/pipeline/multi-agent-refs/features/jira-context.md +101 -0
- package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-2-planning.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +1 -1
- package/pipeline/multi-agent-refs/readiness-review.md +1 -1
- package/pipeline/schemas/agent-state.schema.json +19 -0
- package/pipeline/schemas/prefs.schema.json +26 -0
- package/pipeline/scripts/anonymize-findings.mjs +24 -0
- package/pipeline/scripts/build-references.mjs +10 -6
- package/pipeline/scripts/council-view.mjs +144 -0
- package/pipeline/scripts/phase-tracker.sh +18 -1
- package/pipeline/scripts/skill-siblings.mjs +41 -8
- package/pipeline/scripts/validate-analysis-doc.mjs +371 -7
- package/pipeline/scripts/validate-analysis.mjs +7 -5
- package/pipeline/skills/shared/core/multi-agent-issue/SKILL.md +1 -0
- package/pipeline/skills/shared/core/multi-agent-review-issue/SKILL.md +1 -0
|
@@ -44,11 +44,20 @@ import { homedir } from "node:os";
|
|
|
44
44
|
import { fileURLToPath } from "node:url";
|
|
45
45
|
|
|
46
46
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
47
|
-
const ROOT = resolve(HERE, "..", "..");
|
|
48
47
|
const HOME = homedir();
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
// Two layouts, and the script ships into the second one. From the repo checkout
|
|
50
|
+
// HERE is <repo>/pipeline/scripts, so the authored trees sit one level up. From
|
|
51
|
+
// an install HERE is ~/.claude/scripts and there is no `pipeline/` above it:
|
|
52
|
+
// resolving blind reported "no commands found" and made every caller believe
|
|
53
|
+
// the tree had changed. Try the repo first, fall back to the install.
|
|
54
|
+
const REPO_ROOT = resolve(HERE, "..", "..");
|
|
55
|
+
const REPO_CMD_DIR = join(REPO_ROOT, "pipeline", "commands", "multi-agent");
|
|
56
|
+
const IN_REPO = existsSync(REPO_CMD_DIR);
|
|
57
|
+
const ROOT = IN_REPO ? REPO_ROOT : HOME;
|
|
58
|
+
|
|
59
|
+
const CMD_DIR = IN_REPO ? REPO_CMD_DIR : join(HOME, ".claude", "commands", "multi-agent");
|
|
60
|
+
const CORE_DIR = join(REPO_ROOT, "pipeline", "skills", "shared", "core");
|
|
52
61
|
const COPILOT_DIR = join(HOME, ".copilot", "skills");
|
|
53
62
|
const CODEX_DIR = join(HOME, ".codex", "multi-agent-refs", "commands");
|
|
54
63
|
// The dispatcher is the one skill Codex installs as a SKILL: the 51 sub-commands
|
|
@@ -107,11 +116,23 @@ function siblingsOf(name) {
|
|
|
107
116
|
);
|
|
108
117
|
const installedName = name === "multi-agent" ? "multi-agent" : `multi-agent-${name}`;
|
|
109
118
|
|
|
119
|
+
// The two authored surfaces exist only in the checkout. From an install they
|
|
120
|
+
// are not missing, they are not applicable, and counting them as absent would
|
|
121
|
+
// report every command as 3/5 forever - a number that reads as damage and is
|
|
122
|
+
// really just the layout. In an install CMD_DIR is also the claude tree, so
|
|
123
|
+
// listing "authored" there would double-count one file as two surfaces.
|
|
124
|
+
const authoredSurfaces = IN_REPO
|
|
125
|
+
? [
|
|
126
|
+
{ tree: "command (authored)", path: authored, exists: existsSync(authored), inRepo: true },
|
|
127
|
+
{ tree: "shared core skill", path: shared, exists: existsSync(shared), inRepo: true },
|
|
128
|
+
]
|
|
129
|
+
: [];
|
|
130
|
+
|
|
110
131
|
return {
|
|
111
132
|
name,
|
|
133
|
+
layout: IN_REPO ? "repo" : "install",
|
|
112
134
|
surfaces: [
|
|
113
|
-
|
|
114
|
-
{ tree: "shared core skill", path: shared, exists: existsSync(shared), inRepo: true },
|
|
135
|
+
...authoredSurfaces,
|
|
115
136
|
{
|
|
116
137
|
tree: "claude (installed)",
|
|
117
138
|
path: join(CLAUDE_DIR, name === "multi-agent" ? "SKILL.md" : join(name, "SKILL.md")),
|
|
@@ -145,11 +166,23 @@ if (AUDIT) {
|
|
|
145
166
|
});
|
|
146
167
|
const broken = rows.filter((r) => r.missingRepo.length > 0);
|
|
147
168
|
if (JSON_OUT) {
|
|
148
|
-
process.stdout.write(
|
|
169
|
+
process.stdout.write(
|
|
170
|
+
JSON.stringify(
|
|
171
|
+
{ layout: IN_REPO ? "repo" : "install", commands: names.length, broken, rows },
|
|
172
|
+
null,
|
|
173
|
+
2,
|
|
174
|
+
) + "\n",
|
|
175
|
+
);
|
|
149
176
|
} else {
|
|
150
|
-
process.stdout.write(`${names.length} command(s) checked\n`);
|
|
177
|
+
process.stdout.write(`${names.length} command(s) checked (${IN_REPO ? "repo" : "install"})\n`);
|
|
151
178
|
if (broken.length === 0) {
|
|
152
|
-
|
|
179
|
+
// From an install there is no authored side to check, and claiming one
|
|
180
|
+
// was checked is the failure this script exists to prevent one level up.
|
|
181
|
+
process.stdout.write(
|
|
182
|
+
IN_REPO
|
|
183
|
+
? " every command has its authored sibling in the repo\n"
|
|
184
|
+
: " authored siblings not checked - no repo checkout resolved from here\n",
|
|
185
|
+
);
|
|
153
186
|
}
|
|
154
187
|
for (const r of broken) {
|
|
155
188
|
process.stdout.write(` MISSING ${r.name}: ${r.missingRepo.join(", ")}\n`);
|
|
@@ -100,11 +100,27 @@ const BANNED_PUNCT = [
|
|
|
100
100
|
{ ch: "’", name: "curly-single-close" },
|
|
101
101
|
];
|
|
102
102
|
|
|
103
|
+
// A Section 20 status cell is bilingual (`Açık / Open`, `Karar verildi / Decided`),
|
|
104
|
+
// so openness is decided per token rather than by searching the row. A substring
|
|
105
|
+
// search read "hata açıklaması netleşti" as open, because "açıklama" contains
|
|
106
|
+
// "Açık" - and "açıklama" is one of the most common words in a Turkish spec, so
|
|
107
|
+
// that blocked a correctly closed document. English has the same trap in
|
|
108
|
+
// "reopened" and "opened".
|
|
109
|
+
const OPEN_STATUS = ["açık", "acik", "open", "girdi bekleniyor", "pending input"];
|
|
110
|
+
function isOpenStatus(cell) {
|
|
111
|
+
return String(cell)
|
|
112
|
+
.split("/")
|
|
113
|
+
.some((part) => {
|
|
114
|
+
const t = part.trim().toLowerCase();
|
|
115
|
+
return OPEN_STATUS.some((v) => t === v || t.startsWith(`${v} `) || t.startsWith(`${v}(`));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
103
119
|
function readInput() {
|
|
104
|
-
const args = process.argv.slice(2).filter((a) => a
|
|
120
|
+
const args = process.argv.slice(2).filter((a) => !a.startsWith("--"));
|
|
105
121
|
const arg = args[0];
|
|
106
122
|
if (!arg) {
|
|
107
|
-
console.error("usage: validate-analysis-doc.mjs <path|-> [--strict]");
|
|
123
|
+
console.error("usage: validate-analysis-doc.mjs <path|-> [--strict] [--report]");
|
|
108
124
|
process.exit(64);
|
|
109
125
|
}
|
|
110
126
|
if (arg === "-") return readFileSync(0, "utf-8");
|
|
@@ -169,11 +185,56 @@ function tableDataRows(bodyLines) {
|
|
|
169
185
|
return rows;
|
|
170
186
|
}
|
|
171
187
|
|
|
188
|
+
// The report registry. `--report` prints one line per check, and the reason a
|
|
189
|
+
// check did not run is part of the answer: the traceability matrix is corporate
|
|
190
|
+
// only, so on a global document it never executes - and until this existed the
|
|
191
|
+
// output for "ran, found nothing" and "never ran" was the same string. A summary
|
|
192
|
+
// that cannot tell those apart is the shape of defect this file exists to catch.
|
|
193
|
+
//
|
|
194
|
+
// Attribution is positional: mark() closes the check that was running and takes
|
|
195
|
+
// every error and warning pushed since the previous mark. That keeps the checks
|
|
196
|
+
// themselves untouched, and the totals reconcile by construction - a forgotten
|
|
197
|
+
// mark misnames a finding, it can never lose one.
|
|
198
|
+
//
|
|
199
|
+
// A count therefore OUTRANKS a skip label. Two blocks legitimately push a finding
|
|
200
|
+
// and then mark themselves skipped - a malformed `status` value is caught by the
|
|
201
|
+
// closure gate that then reports "draft document", and the redesign drift warning
|
|
202
|
+
// is raised by the branch that reports "not a redesign". Printing the skip label
|
|
203
|
+
// there dropped a real finding out of the report while stderr still carried it,
|
|
204
|
+
// which is the one thing this registry exists to prevent.
|
|
205
|
+
function makeReporter(errors, warns) {
|
|
206
|
+
const rows = [];
|
|
207
|
+
let e = 0;
|
|
208
|
+
let w = 0;
|
|
209
|
+
return {
|
|
210
|
+
rows,
|
|
211
|
+
mark(id, skipped) {
|
|
212
|
+
const de = errors.length - e;
|
|
213
|
+
const dw = warns.length - w;
|
|
214
|
+
e = errors.length;
|
|
215
|
+
w = warns.length;
|
|
216
|
+
const counts = [de ? `${de} error(s)` : null, dw ? `${dw} warning(s)` : null]
|
|
217
|
+
.filter(Boolean)
|
|
218
|
+
.join(", ");
|
|
219
|
+
const verdict = counts
|
|
220
|
+
? skipped
|
|
221
|
+
? `${counts} (otherwise skipped: ${skipped})`
|
|
222
|
+
: counts
|
|
223
|
+
: skipped
|
|
224
|
+
? `skipped: ${skipped}`
|
|
225
|
+
: "ok";
|
|
226
|
+
rows.push({ id, verdict });
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
172
231
|
function main() {
|
|
173
232
|
const text = readInput();
|
|
174
233
|
const strict = process.argv.includes("--strict");
|
|
234
|
+
const report = process.argv.includes("--report");
|
|
175
235
|
const errors = [];
|
|
176
236
|
const warns = [];
|
|
237
|
+
const { rows: checkRows, mark } = makeReporter(errors, warns);
|
|
177
238
|
|
|
178
239
|
// 1. Front-matter
|
|
179
240
|
const parsed = parseFrontMatter(text);
|
|
@@ -198,6 +259,7 @@ function main() {
|
|
|
198
259
|
errors.push(`front-matter profile must be global|corporate, got: ${profile}`);
|
|
199
260
|
}
|
|
200
261
|
const noPlatform = (parsed?.fm?.platform || "") === "none";
|
|
262
|
+
mark("front-matter");
|
|
201
263
|
|
|
202
264
|
// 2. Never-omitted sections (by heading keyword)
|
|
203
265
|
// Numbered section headings only. Layer headings (`# Bölüm B - Teknik Analiz`)
|
|
@@ -218,8 +280,15 @@ function main() {
|
|
|
218
280
|
errors.push(`missing required section (${rule}): ${sec.key}`);
|
|
219
281
|
}
|
|
220
282
|
}
|
|
283
|
+
mark("required-sections");
|
|
284
|
+
|
|
285
|
+
// Section 20, resolved once and read twice: the marker check needs its owner
|
|
286
|
+
// column, the closure gate needs its status column.
|
|
287
|
+
let risksRows;
|
|
288
|
+
let risksHeader = null;
|
|
289
|
+
let risksStatusIdx = -1;
|
|
221
290
|
|
|
222
|
-
// 2a.
|
|
291
|
+
// 2a. Every admitted gap owes an open question, by id - in BOTH profiles.
|
|
223
292
|
//
|
|
224
293
|
// A placeholder with nobody assigned to resolve it is how "to be filled in"
|
|
225
294
|
// reaches production. This used to pair a gap with Section 20 by matching the
|
|
@@ -228,7 +297,15 @@ function main() {
|
|
|
228
297
|
// cases it still could not separate. Stable ids remove the ambiguity the same
|
|
229
298
|
// way IG / UC / FG already do: the body writes EKLENECEK (AS-07) and Section 20
|
|
230
299
|
// defines AS-07, and each side is checked against the other.
|
|
231
|
-
|
|
300
|
+
//
|
|
301
|
+
// It ran only in the corporate profile until now, which made the global one the
|
|
302
|
+
// profile where an inline marker could point at nothing. Two documents already
|
|
303
|
+
// told it to: Locked 36 says an unstamped gap fails the dispatch gate with no
|
|
304
|
+
// profile clause, and analysis/render.md routes an external-dependency gap to
|
|
305
|
+
// `AS-NN` in Section 20 for every run. The check was the only side that
|
|
306
|
+
// disagreed. Nothing changes for a document that carries no markers: every loop
|
|
307
|
+
// below is empty, so this is free for the documents that never needed it.
|
|
308
|
+
{
|
|
232
309
|
const lines = text.split("\n");
|
|
233
310
|
const risksStart = lines.findIndex((l) => /^#{2,3}\s+\d+/.test(l) && /(Riskler|Risks)/.test(l));
|
|
234
311
|
let risksBlock = "";
|
|
@@ -271,6 +348,103 @@ function main() {
|
|
|
271
348
|
}
|
|
272
349
|
}
|
|
273
350
|
|
|
351
|
+
// The owner is what separates a plan from a defect, and until now that
|
|
352
|
+
// sentence lived only in analysis/review.md - a model's judgement call. The
|
|
353
|
+
// column is located from the header rather than by index, because the two
|
|
354
|
+
// profiles order Section 20 differently and an index would silently read the
|
|
355
|
+
// wrong cell in one of them.
|
|
356
|
+
const rows = risksBlock.split("\n").filter((l) => /^\s*\|/.test(l));
|
|
357
|
+
const header = rows.find((l) => /(Sahibi|Owner|Kime sorulacak)/i.test(l));
|
|
358
|
+
// Hoisted for the closure gate below, which has to read the SAME rows: the
|
|
359
|
+
// first pipe row in the document carrying an id is often a body table cell
|
|
360
|
+
// (the corporate template writes `| **Request** | EKLENECEK (AS-NN) |`), and
|
|
361
|
+
// judging openness from that row let a final document with live placeholders
|
|
362
|
+
// through. Section 20 is the only place that answers the question.
|
|
363
|
+
risksRows = rows;
|
|
364
|
+
risksHeader = header || rows.find((l) => /(Durum|Status)/i.test(l)) || null;
|
|
365
|
+
if (risksHeader) {
|
|
366
|
+
risksStatusIdx = risksHeader
|
|
367
|
+
.split("|")
|
|
368
|
+
.map((c) => c.trim())
|
|
369
|
+
.findIndex((c) => /(Durum|Status)/i.test(c));
|
|
370
|
+
}
|
|
371
|
+
if (header) {
|
|
372
|
+
const cols = header.split("|").map((c) => c.trim());
|
|
373
|
+
const oi = cols.findIndex((c) => /(Sahibi|Owner|Kime sorulacak)/i.test(c));
|
|
374
|
+
for (const row of rows) {
|
|
375
|
+
if (row === header || /^\s*\|[\s|:-]*\|?\s*$/.test(row)) continue;
|
|
376
|
+
if (!/\bAS-\d{2,3}\b/.test(row)) continue;
|
|
377
|
+
const cells = row.split("|").map((c) => c.trim());
|
|
378
|
+
const owner = cells[oi] || "";
|
|
379
|
+
if (!owner || /^(-|\?|TBD|EKLENECEK|N\/A)$/i.test(owner)) {
|
|
380
|
+
const id = (row.match(/\bAS-\d{2,3}\b/) || ["a row"])[0];
|
|
381
|
+
errors.push(`${id} names no owner in Risks and Open Questions (Locked 36)`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
} else if (rows.some((l) => /\bAS-\d{2,3}\b/.test(l))) {
|
|
385
|
+
// Only when there is something to own. A Section 20 with no stamped row
|
|
386
|
+
// has no owner to check, and warning there would fire on every document
|
|
387
|
+
// that never needed the column - noise that Phase 4 runs under --strict.
|
|
388
|
+
warns.push(
|
|
389
|
+
"Risks and Open Questions carries AS-NN rows but no owner column; ownership cannot be checked",
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
mark("uncertainty-markers");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// 2a-2. The closure gate. `status: final` is a publication claim, so it is the
|
|
396
|
+
// one place an open marker is fatal; `draft` reports the count and moves on.
|
|
397
|
+
// The key is optional and absent means draft - REQUIRED_FM deliberately does
|
|
398
|
+
// not list it, because a rule that fails every document written before it is
|
|
399
|
+
// not a rule, it is a migration nobody asked for.
|
|
400
|
+
{
|
|
401
|
+
const status = (parsed?.fm?.status || "draft").toLowerCase();
|
|
402
|
+
if (status !== "draft" && status !== "final") {
|
|
403
|
+
errors.push(`front-matter status must be draft|final, got: ${parsed.fm.status}`);
|
|
404
|
+
} else if (status === "final") {
|
|
405
|
+
const lines = text.split("\n");
|
|
406
|
+
const openMarkers = new Set();
|
|
407
|
+
for (const id of new Set(text.match(/\bAS-\d{2,3}\b/g) || [])) {
|
|
408
|
+
const row = risksRows.find((l) => l !== risksHeader && l.includes(id));
|
|
409
|
+
if (!row) {
|
|
410
|
+
// No Section 20 row at all. The pairing check above already errors on
|
|
411
|
+
// this; under `final` it is also unresolvable, so it stays open.
|
|
412
|
+
openMarkers.add(id);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (risksStatusIdx >= 0) {
|
|
416
|
+
const cell = row.split("|").map((c) => c.trim())[risksStatusIdx] || "";
|
|
417
|
+
if (isOpenStatus(cell)) openMarkers.add(id);
|
|
418
|
+
} else if (lines.some((l) => l.includes(id) && /\b(EKLENECEK|TBD)\b/.test(l))) {
|
|
419
|
+
// The corporate Section 20 carries no status column, so the body is the
|
|
420
|
+
// signal: an EKLENECEK still stamped with this id is the gap itself,
|
|
421
|
+
// whatever the table says.
|
|
422
|
+
openMarkers.add(id);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
for (const id of [...openMarkers].sort()) {
|
|
426
|
+
errors.push(`status: final but ${id} is still open (Locked 36 closure gate)`);
|
|
427
|
+
}
|
|
428
|
+
const stillOpen =
|
|
429
|
+
risksStatusIdx >= 0
|
|
430
|
+
? risksRows.filter(
|
|
431
|
+
(l) =>
|
|
432
|
+
l !== risksHeader &&
|
|
433
|
+
isOpenStatus(l.split("|").map((c) => c.trim())[risksStatusIdx] || ""),
|
|
434
|
+
).length
|
|
435
|
+
: 0;
|
|
436
|
+
if (stillOpen > 0 && openMarkers.size === 0) {
|
|
437
|
+
errors.push(
|
|
438
|
+
`status: final but Risks and Open Questions still has ${stillOpen} unresolved row(s)`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
mark("closure-gate", status === "final" ? undefined : "draft document");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (profile === "corporate") {
|
|
446
|
+
const lines = text.split("\n");
|
|
447
|
+
|
|
274
448
|
// 2b. JSON does not fit in a table cell (analysis-template.md Section 9.2 / 9.3).
|
|
275
449
|
//
|
|
276
450
|
// One document rendered `Request` as inline JSON with a field note glued to
|
|
@@ -289,6 +463,8 @@ function main() {
|
|
|
289
463
|
}
|
|
290
464
|
}
|
|
291
465
|
|
|
466
|
+
mark("payload-in-table-cell");
|
|
467
|
+
|
|
292
468
|
// The traceability matrix is the most expensive thing in the document to get
|
|
293
469
|
// wrong, because every downstream reader trusts it instead of re-deriving
|
|
294
470
|
// the IG -> UC -> FG chain themselves. A matrix that merely exists is not
|
|
@@ -326,7 +502,13 @@ function main() {
|
|
|
326
502
|
}
|
|
327
503
|
}
|
|
328
504
|
}
|
|
505
|
+
mark("traceability-matrix");
|
|
506
|
+
} else {
|
|
507
|
+
mark("traceability-matrix", "lite mode");
|
|
329
508
|
}
|
|
509
|
+
} else {
|
|
510
|
+
mark("payload-in-table-cell", "global profile");
|
|
511
|
+
mark("traceability-matrix", "global profile");
|
|
330
512
|
}
|
|
331
513
|
|
|
332
514
|
// 2c. Diagram labels keep their diacritics.
|
|
@@ -363,6 +545,9 @@ function main() {
|
|
|
363
545
|
}
|
|
364
546
|
}
|
|
365
547
|
}
|
|
548
|
+
mark("diagram-diacritics");
|
|
549
|
+
} else {
|
|
550
|
+
mark("diagram-diacritics", "document language is not tr");
|
|
366
551
|
}
|
|
367
552
|
|
|
368
553
|
// 2b. Opt-in coverage sections must be present when the front-matter says so.
|
|
@@ -392,6 +577,8 @@ function main() {
|
|
|
392
577
|
}
|
|
393
578
|
}
|
|
394
579
|
|
|
580
|
+
mark("optin-coverage-sections");
|
|
581
|
+
|
|
395
582
|
// 2c. The Test Plan is Phase 3's RED input (phase-3-dev.md step 5b): the tests
|
|
396
583
|
// written first come from it. Full mode without it hands dev an empty matrix.
|
|
397
584
|
// It is Section 15 in the global profile and Section 19 in the corporate one;
|
|
@@ -427,6 +614,10 @@ function main() {
|
|
|
427
614
|
}
|
|
428
615
|
}
|
|
429
616
|
|
|
617
|
+
if (mode !== "full") mark("test-plan", "lite mode");
|
|
618
|
+
else if (noPlatform) mark("test-plan", "no target platform");
|
|
619
|
+
else mark("test-plan");
|
|
620
|
+
|
|
430
621
|
// 2d. Section 3 carries the diagrams. Omission is legitimate under the
|
|
431
622
|
// template's conditions, so absence warns; a rendered section with no
|
|
432
623
|
// mermaid block is empty scaffolding and fails.
|
|
@@ -444,6 +635,8 @@ function main() {
|
|
|
444
635
|
);
|
|
445
636
|
}
|
|
446
637
|
|
|
638
|
+
mark("flow-chart");
|
|
639
|
+
|
|
447
640
|
// 2e. Locked 16: every Files-to-Add row is tagged. The decision says untagged
|
|
448
641
|
// rows fail the dispatch gate, so this is where that sentence becomes true.
|
|
449
642
|
const allLines = text.split("\n");
|
|
@@ -459,6 +652,8 @@ function main() {
|
|
|
459
652
|
}
|
|
460
653
|
}
|
|
461
654
|
|
|
655
|
+
mark("files-to-add-tags", filesBody ? undefined : "no Files-to-Add section");
|
|
656
|
+
|
|
462
657
|
// 2f. Locked 24: every Pass B concept-table cell carries its footnote.
|
|
463
658
|
const conceptBody = sectionBody(allLines, ["Kavram tablosu", "Concept table"], 3);
|
|
464
659
|
if (conceptBody) {
|
|
@@ -475,6 +670,8 @@ function main() {
|
|
|
475
670
|
}
|
|
476
671
|
}
|
|
477
672
|
|
|
673
|
+
mark("concept-table-evidence", conceptBody ? undefined : "no concept table");
|
|
674
|
+
|
|
478
675
|
// 2g. Locked 31 main clause: a rule with no test scenario fails the gate.
|
|
479
676
|
// The pre-existing occurrence-count heuristic below stays as the looser net.
|
|
480
677
|
if (mode === "full") {
|
|
@@ -496,6 +693,145 @@ function main() {
|
|
|
496
693
|
}
|
|
497
694
|
}
|
|
498
695
|
|
|
696
|
+
mark("rule-to-test", mode === "full" ? undefined : "lite mode");
|
|
697
|
+
|
|
698
|
+
// 2h. Redesign mode (Locked 37). Eight checks, live only when the front-matter
|
|
699
|
+
// opts in. The contract they enforce is analysis/redesign.md, which loads only
|
|
700
|
+
// on a redesign run; what is here is the enforcement, not the explanation.
|
|
701
|
+
const redesign = String(parsed?.fm?.redesign || "false").toLowerCase() === "true";
|
|
702
|
+
const cbBody = sectionBody(
|
|
703
|
+
allLines,
|
|
704
|
+
["Mevcut Davranış", "Mevcut Davranis", "Current Behaviour", "Current Behavior"],
|
|
705
|
+
3,
|
|
706
|
+
);
|
|
707
|
+
const epBody = sectionBody(
|
|
708
|
+
allLines,
|
|
709
|
+
["Endpoint Eşlemesi", "Endpoint Eslemesi", "Endpoint Mapping"],
|
|
710
|
+
3,
|
|
711
|
+
);
|
|
712
|
+
const dlBody = sectionBody(allLines, ["Fark Listesi", "Difference List"], 3);
|
|
713
|
+
if (redesign) {
|
|
714
|
+
// 1. The sections themselves. Without this every row check below passes over
|
|
715
|
+
// nothing, which is the specific defect that shows a gate green on an empty
|
|
716
|
+
// document - the same shape the corporate matrix check guards against.
|
|
717
|
+
for (const [body, name] of [
|
|
718
|
+
[cbBody, "4.5 Current Behaviour"],
|
|
719
|
+
[epBody, "4.6 Endpoint Mapping"],
|
|
720
|
+
[dlBody, "9.5 Difference List"],
|
|
721
|
+
]) {
|
|
722
|
+
if (!body) errors.push(`redesign: true but Section ${name} is missing (Locked 37)`);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const STATUSES = ["Moved", "Partial", "Missing", "New", "Out of scope"];
|
|
726
|
+
const CB_ID = /\bCB-[a-z0-9]+(?:-[a-z0-9]+)*-\d+/gi;
|
|
727
|
+
const cbRows = cbBody ? tableDataRows(cbBody) : [];
|
|
728
|
+
const dlRows = dlBody ? tableDataRows(dlBody) : [];
|
|
729
|
+
|
|
730
|
+
for (const row of cbRows) {
|
|
731
|
+
const cells = row.split("|").map((c) => c.trim());
|
|
732
|
+
const id = (row.match(CB_ID) || ["a row"])[0];
|
|
733
|
+
const evidence = cells[3] || "";
|
|
734
|
+
const certainty = (cells[4] || "").toLowerCase();
|
|
735
|
+
// 2. An unmarked guess. Prose can only ask an author to mark it; this finds
|
|
736
|
+
// the row that was not marked.
|
|
737
|
+
if (!/:\d+\b/.test(evidence) && !certainty.includes("uncertain")) {
|
|
738
|
+
errors.push(
|
|
739
|
+
`current-behaviour row ${id} cites no file:line and is not marked uncertain (Locked 37)`,
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
// 3. A rule that lives only in a shared module legitimately reads this way,
|
|
743
|
+
// so it warns rather than blocks - and Phase 4 runs --strict, so it blocks
|
|
744
|
+
// there, which is where a human is already looking.
|
|
745
|
+
if (/cross-cutting/i.test(evidence) && certainty.includes("confirmed")) {
|
|
746
|
+
warns.push(
|
|
747
|
+
`current-behaviour row ${id} is confirmed on cross-cutting evidence alone; name the file:line or mark it uncertain`,
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
for (const row of dlRows) {
|
|
753
|
+
const cells = row.split("|").map((c) => c.trim());
|
|
754
|
+
const id = (row.match(CB_ID) || ["a row"])[0];
|
|
755
|
+
const statusCell = cells[2] || "";
|
|
756
|
+
const status = STATUSES.find((v) => new RegExp(`\\b${v}\\b`, "i").test(statusCell));
|
|
757
|
+
// 4. A controlled vocabulary decaying into free text.
|
|
758
|
+
if (!status) {
|
|
759
|
+
errors.push(
|
|
760
|
+
`difference-list row ${id} has status "${statusCell}"; expected one of ${STATUSES.join(" / ")} (Locked 37)`,
|
|
761
|
+
);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
// 5. The reason the mode exists: a behaviour v2 drops with no decision.
|
|
765
|
+
if ((status === "Missing" || status === "Partial") && !/\bAS-\d{2,3}\b/.test(row)) {
|
|
766
|
+
errors.push(
|
|
767
|
+
`difference-list row ${id} is ${status} with no AS-NN open question (Locked 37)`,
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// 6. Both directions. A CB- id in the code and on nobody's difference list is
|
|
773
|
+
// the thing a redesign loses and production finds; an id on the list that no
|
|
774
|
+
// behaviour table defines is a row about nothing.
|
|
775
|
+
if (cbBody && dlBody) {
|
|
776
|
+
const defined = new Set(
|
|
777
|
+
cbRows.flatMap((r) => (r.match(CB_ID) || []).map((x) => x.toUpperCase())),
|
|
778
|
+
);
|
|
779
|
+
const listed = new Set(
|
|
780
|
+
dlRows.flatMap((r) => (r.match(CB_ID) || []).map((x) => x.toUpperCase())),
|
|
781
|
+
);
|
|
782
|
+
for (const id of defined) {
|
|
783
|
+
if (!listed.has(id))
|
|
784
|
+
errors.push(`${id} is in current behaviour but not in the difference list`);
|
|
785
|
+
}
|
|
786
|
+
for (const id of listed) {
|
|
787
|
+
if (!defined.has(id))
|
|
788
|
+
errors.push(`${id} is in the difference list but no behaviour defines it`);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// 7. A renderer that adds a column would silently disable the row checks
|
|
793
|
+
// above, so the shape is asserted before the rows are trusted.
|
|
794
|
+
if (epBody) {
|
|
795
|
+
const header = epBody.find((l) => /^\s*\|/.test(l) && /\bv1\b/i.test(l));
|
|
796
|
+
// GFM makes the trailing pipe optional, so a raw segment count reported a
|
|
797
|
+
// perfectly valid row as short. Strip the outer pipes on both sides first.
|
|
798
|
+
const cellCount = (line) =>
|
|
799
|
+
line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").length;
|
|
800
|
+
if (!header) {
|
|
801
|
+
errors.push("Section 4.6 has no v1/v2 header row (Locked 37)");
|
|
802
|
+
} else {
|
|
803
|
+
const cols = cellCount(header);
|
|
804
|
+
for (const row of tableDataRows(epBody)) {
|
|
805
|
+
if (cellCount(row) !== cols) {
|
|
806
|
+
errors.push(`endpoint-mapping row has ${cellCount(row)} cells, header has ${cols}`);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
mark("redesign-artifacts");
|
|
812
|
+
} else {
|
|
813
|
+
// 8. Drift: the sections are rendered but the front-matter does not claim the
|
|
814
|
+
// mode, so none of the seven checks above ran over them.
|
|
815
|
+
//
|
|
816
|
+
// The heading match alone is not enough to say that. sectionBody tests the
|
|
817
|
+
// keyword as a raw substring of the heading line - deliberately, since Locked
|
|
818
|
+
// 2 re-flows section numbers and the corporate profile numbers them
|
|
819
|
+
// differently - so an unrelated `### 4.9 Notes on the old Current Behaviour
|
|
820
|
+
// audit process` matched, and Phase 4 runs --strict, which turns this warning
|
|
821
|
+
// into a blocked document. The artefact itself is the honest signal: a
|
|
822
|
+
// rendered current-behaviour table always carries `CB-<slug>-NN` ids, and a
|
|
823
|
+
// paragraph that merely uses the words never does.
|
|
824
|
+
const looksRendered = /\bCB-[a-z0-9]+(?:-[a-z0-9]+)*-\d+/i.test(
|
|
825
|
+
[...(cbBody || []), ...(dlBody || [])].join("\n"),
|
|
826
|
+
);
|
|
827
|
+
if (looksRendered) {
|
|
828
|
+
warns.push(
|
|
829
|
+
"redesign sections are present but front-matter does not say redesign: true, so none of their checks ran",
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
mark("redesign-artifacts", "not a redesign");
|
|
833
|
+
}
|
|
834
|
+
|
|
499
835
|
// 3. Humanizer punctuation. Scope mirrors the humanizer pass exactly
|
|
500
836
|
// (analysis/render.md): front-matter, fenced code blocks, table rows and
|
|
501
837
|
// URLs are exempt there, so flagging them here would raise an ERROR that no
|
|
@@ -517,6 +853,8 @@ function main() {
|
|
|
517
853
|
}
|
|
518
854
|
}
|
|
519
855
|
|
|
856
|
+
mark("punctuation");
|
|
857
|
+
|
|
520
858
|
// 4. Placeholder whole-section bodies (Locked 2).
|
|
521
859
|
// Not a defect in the corporate profile: there N/A and EKLENECEK are how a
|
|
522
860
|
// backbone section reports "considered, nothing to state" (Locked 33), and the
|
|
@@ -527,6 +865,8 @@ function main() {
|
|
|
527
865
|
);
|
|
528
866
|
}
|
|
529
867
|
|
|
868
|
+
mark("placeholder-bodies", profile === "global" ? undefined : "corporate profile");
|
|
869
|
+
|
|
530
870
|
// 5. Traceability (Full mode only, Locked 31)
|
|
531
871
|
if (mode === "full") {
|
|
532
872
|
const idRe = /BR-[a-z0-9]+(?:-[a-z0-9]+)*-\d+/gi;
|
|
@@ -548,6 +888,8 @@ function main() {
|
|
|
548
888
|
}
|
|
549
889
|
}
|
|
550
890
|
|
|
891
|
+
mark("br-reference-count", mode === "full" ? undefined : "lite mode");
|
|
892
|
+
|
|
551
893
|
// 6. Manual test scenarios (Section 15.7): every MT row carries a BR id.
|
|
552
894
|
// A manual scenario nobody can trace back to a rule is a scenario nobody can
|
|
553
895
|
// tell is still correct when the rule changes.
|
|
@@ -559,6 +901,8 @@ function main() {
|
|
|
559
901
|
}
|
|
560
902
|
}
|
|
561
903
|
|
|
904
|
+
mark("manual-scenario-ids");
|
|
905
|
+
|
|
562
906
|
// 7. Variant coverage (Section 6.X): a row that lists an axis must say what
|
|
563
907
|
// the whole axis is AND which values this screen uses. Half a row cannot be
|
|
564
908
|
// audited - "used subset" means nothing without the set it is a subset of.
|
|
@@ -585,21 +929,41 @@ function main() {
|
|
|
585
929
|
}
|
|
586
930
|
}
|
|
587
931
|
|
|
932
|
+
mark("variant-coverage");
|
|
933
|
+
|
|
588
934
|
// Report
|
|
935
|
+
if (report) {
|
|
936
|
+
const width = Math.max(...checkRows.map((r) => r.id.length));
|
|
937
|
+
console.log(
|
|
938
|
+
`validate-analysis-doc: ${checkRows.length} check(s), mode=${mode}, profile=${profile}`,
|
|
939
|
+
);
|
|
940
|
+
// The provenance the reader needs to compare two reports. A timestamp cannot
|
|
941
|
+
// separate two runs on the same day; the digest says the evidence moved and
|
|
942
|
+
// base_commit says the repo did. Both are already in the front-matter, on
|
|
943
|
+
// the side only a machine reads - this is the human-readable copy.
|
|
944
|
+
const digest = parsed?.fm?.evidence_digest;
|
|
945
|
+
const commit = parsed?.fm?.base_commit;
|
|
946
|
+
if (digest || commit) {
|
|
947
|
+
console.log(` evidence_digest ${digest || "absent"} base_commit ${commit || "absent"}`);
|
|
948
|
+
}
|
|
949
|
+
for (const r of checkRows) console.log(` ${r.id.padEnd(width)} ${r.verdict}`);
|
|
950
|
+
console.log("");
|
|
951
|
+
}
|
|
589
952
|
for (const w of warns) console.error(`WARN: ${w}`);
|
|
590
953
|
for (const e of errors) console.error(`ERROR: ${e}`);
|
|
591
954
|
if (errors.length > 0) {
|
|
592
955
|
console.error(
|
|
593
956
|
`validate-analysis-doc: FAIL (${errors.length} error(s), ${warns.length} warning(s))`,
|
|
594
957
|
);
|
|
595
|
-
process.
|
|
958
|
+
process.exitCode = 1;
|
|
959
|
+
return;
|
|
596
960
|
}
|
|
597
961
|
if (strict && warns.length > 0) {
|
|
598
962
|
console.error(`validate-analysis-doc: FAIL under --strict (${warns.length} warning(s))`);
|
|
599
|
-
process.
|
|
963
|
+
process.exitCode = 1;
|
|
964
|
+
return;
|
|
600
965
|
}
|
|
601
966
|
console.log(`validate-analysis-doc: OK (mode=${mode}, ${warns.length} warning(s))`);
|
|
602
|
-
process.exit(0);
|
|
603
967
|
}
|
|
604
968
|
|
|
605
969
|
main();
|
|
@@ -55,7 +55,8 @@ async function main() {
|
|
|
55
55
|
parsed = JSON.parse(raw);
|
|
56
56
|
} catch (err) {
|
|
57
57
|
emit({ ok: false, code: 1, errors: [`invalid JSON: ${err.message}`] });
|
|
58
|
-
process.
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
return;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
const errors = [];
|
|
@@ -63,7 +64,8 @@ async function main() {
|
|
|
63
64
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
64
65
|
errors.push("root must be an object");
|
|
65
66
|
emit({ ok: false, code: 1, errors });
|
|
66
|
-
process.
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
return;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
// stack
|
|
@@ -124,11 +126,11 @@ async function main() {
|
|
|
124
126
|
|
|
125
127
|
if (errors.length > 0) {
|
|
126
128
|
emit({ ok: false, code: 1, errors });
|
|
127
|
-
process.
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
return;
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
emit({ ok: true, code: 0, errors: [] });
|
|
131
|
-
process.exit(0);
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
function emit(result) {
|
|
@@ -137,5 +139,5 @@ function emit(result) {
|
|
|
137
139
|
|
|
138
140
|
main().catch((err) => {
|
|
139
141
|
emit({ ok: false, code: 1, errors: [`unhandled: ${err.message}`] });
|
|
140
|
-
process.
|
|
142
|
+
process.exitCode = 1;
|
|
141
143
|
});
|
|
@@ -4,6 +4,7 @@ language: en
|
|
|
4
4
|
description: "List unassigned GitHub issues, pick one, auto-assign, and launch the multi-agent pipeline. Use when a GitHub issue should be picked up and started without knowing its number in advance."
|
|
5
5
|
user-invocable: true
|
|
6
6
|
argument-hint: "[autopilot] - optional: run the pipeline without confirmations"
|
|
7
|
+
not-for: jira
|
|
7
8
|
---
|
|
8
9
|
|
|
9
10
|
# multi-agent issue - GitHub Issue Picker
|
|
@@ -4,6 +4,7 @@ language: en
|
|
|
4
4
|
description: "Assess whether a GitHub issue is ready for multi-agent development: fetch it, grade scope / acceptance criteria / repro / design / API / stack readiness, then (after confirm) post the gaps as an issue comment. Read-only on code. Use when deciding whether a GitHub issue is specified well enough to hand to development."
|
|
5
5
|
user-invocable: true
|
|
6
6
|
argument-hint: "[#N | repo#N | GitHub issue URL] - optional; with no argument, pick from open issues"
|
|
7
|
+
not-for: review-jira
|
|
7
8
|
---
|
|
8
9
|
|
|
9
10
|
# multi-agent review-issue - GitHub issue readiness review
|