@mmerterden/multi-agent-pipeline 20.23.1 → 20.24.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 +30 -1
- package/README.md +7 -6
- package/README.tr.md +6 -5
- package/docs/architecture.md +3 -3
- package/docs/ecosystem.md +5 -5
- package/docs/facts.json +3 -3
- package/install/catalog-history.json +1 -1
- package/install/templates/claude-hooks.json +14 -1
- package/manifest.json +77 -56
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +11 -2
- package/pipeline/commands/multi-agent/postmortem/SKILL.md +78 -0
- package/pipeline/commands/multi-agent/setup/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
- package/pipeline/lib/analysis-jira-write.sh +105 -22
- package/pipeline/lib/decisions.mjs +130 -0
- package/pipeline/lib/plan-todos.sh +156 -33
- package/pipeline/lib/task-graph.mjs +446 -0
- package/pipeline/lib/tdd-evidence.mjs +579 -0
- package/pipeline/multi-agent-refs/analysis-template-corporate.md +2 -0
- package/pipeline/multi-agent-refs/analysis-template.md +4 -2
- package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -4
- package/pipeline/multi-agent-refs/features/analysis-jira.md +51 -3
- package/pipeline/multi-agent-refs/features/autopilot-decisions.md +34 -0
- package/pipeline/multi-agent-refs/features/model-fallback.md +4 -2
- package/pipeline/multi-agent-refs/features/mutation-probe.md +58 -0
- package/pipeline/multi-agent-refs/features/plan-checks.md +67 -0
- package/pipeline/multi-agent-refs/features/plan-todos.md +40 -1
- package/pipeline/multi-agent-refs/features/rework-escalation.md +49 -0
- package/pipeline/multi-agent-refs/features/scope-check.md +1 -0
- package/pipeline/multi-agent-refs/generate-issue.md +1 -1
- package/pipeline/multi-agent-refs/help/en.md +1 -0
- package/pipeline/multi-agent-refs/help/tr.md +1 -0
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -3
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +5 -7
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +13 -11
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +3 -3
- package/pipeline/multi-agent-refs/picker-contract.md +3 -2
- package/pipeline/multi-agent-refs/response-shape.md +65 -0
- package/pipeline/rules/response-shape.md +18 -0
- package/pipeline/schemas/agent-state.schema.json +31 -1
- package/pipeline/schemas/plan-todos.schema.json +101 -0
- package/pipeline/schemas/planning-output.schema.json +46 -0
- package/pipeline/schemas/prefs.schema.json +117 -1
- package/pipeline/scripts/_jsonl.mjs +151 -0
- package/pipeline/scripts/_plan-overlap.mjs +231 -0
- package/pipeline/scripts/_redact.mjs +124 -10
- package/pipeline/scripts/analysis-story-body.mjs +26 -10
- package/pipeline/scripts/analysis-story-tree.mjs +210 -9
- package/pipeline/scripts/autopilot-publish.mjs +12 -1
- package/pipeline/scripts/cost-analyze.mjs +6 -24
- package/pipeline/scripts/decisions-log.mjs +118 -0
- package/pipeline/scripts/edit-syntax-gate.mjs +240 -0
- package/pipeline/scripts/edit-syntax-gate.sh +29 -0
- package/pipeline/scripts/find-polluter.mjs +281 -0
- package/pipeline/scripts/gate-ledger.mjs +46 -13
- package/pipeline/scripts/mutation-probe.mjs +484 -0
- package/pipeline/scripts/plan-complexity.mjs +259 -0
- package/pipeline/scripts/review-decision-gate.mjs +33 -1
- package/pipeline/scripts/rework-escalation.mjs +264 -0
- package/pipeline/scripts/test-strength.mjs +5 -3
- package/pipeline/scripts/transcript-index.mjs +950 -0
- package/pipeline/scripts/validate-planning.mjs +48 -6
- package/pipeline/skills/.skill-manifest.json +11 -7
- package/pipeline/skills/.skills-index.json +12 -1
- package/pipeline/skills/shared/README.md +4 -3
- package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +11 -2
- package/pipeline/skills/shared/core/multi-agent-postmortem/SKILL.md +66 -0
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +15 -0
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +13 -0
- package/pipeline/skills/shared/external/debugging-strategies/SKILL.md +8 -0
- package/pipeline/skills/skills-index.md +2 -1
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
*
|
|
51
51
|
* Usage:
|
|
52
52
|
* analysis-story-tree.mjs <analysis.md> [--json] [--brief] [--prefs FILE] [--page-url URL]
|
|
53
|
+
* analysis-story-tree.mjs --save-subtasks "<type>[,<type>...]"|none [--prefs FILE]
|
|
53
54
|
*
|
|
54
55
|
* Exit: 0 planned, 2 nothing to plan, 3 usage, 4 the document is not issue-ready.
|
|
55
56
|
*/
|
|
@@ -60,6 +61,8 @@ import { createHash } from "node:crypto";
|
|
|
60
61
|
import { homedir } from "node:os";
|
|
61
62
|
import { dirname, join } from "node:path";
|
|
62
63
|
import { hostCommand } from "../lib/user-facing.mjs";
|
|
64
|
+
import { lockTimeoutFrom, updateJsonFileSync } from "../lib/json-file-lock.mjs";
|
|
65
|
+
import { analyzeGraph, cyclicComponentOf } from "../lib/task-graph.mjs";
|
|
63
66
|
import { resolveConstitution } from "./constitution.mjs";
|
|
64
67
|
import { gateContext, recordQuietly } from "./gate-ledger.mjs";
|
|
65
68
|
import {
|
|
@@ -81,7 +84,11 @@ const PREFS_PATH =
|
|
|
81
84
|
: join(homedir(), ".claude", "multi-agent-preferences.json");
|
|
82
85
|
const pageUrlFlag = argv.indexOf("--page-url");
|
|
83
86
|
const PAGE_URL = pageUrlFlag !== -1 ? argv[pageUrlFlag + 1] : null;
|
|
84
|
-
const
|
|
87
|
+
const saveFlag = argv.indexOf("--save-subtasks");
|
|
88
|
+
const SAVE_SUBTASKS = saveFlag !== -1 ? (argv[saveFlag + 1] ?? "") : null;
|
|
89
|
+
const valueIdx = new Set(
|
|
90
|
+
[prefsFlag, pageUrlFlag, saveFlag].filter((i) => i !== -1).map((i) => i + 1),
|
|
91
|
+
);
|
|
85
92
|
const file = argv.find((a, i) => !a.startsWith("--") && !valueIdx.has(i));
|
|
86
93
|
|
|
87
94
|
function die(msg, code) {
|
|
@@ -162,6 +169,9 @@ export function identityLabel(prefix, docId, sourceIds) {
|
|
|
162
169
|
* issueTree.epicLinkFieldId the epic-link field, when not discovered
|
|
163
170
|
* issueTree.cloneByChannel one linked story per channel in channelComponents
|
|
164
171
|
* issueTree.cloneLinkType the link type between a story and its clones
|
|
172
|
+
* issueTree.storyPanels story section panel colours, or false for none
|
|
173
|
+
* issueTree.subtaskRolesDeclined the person chose no sub-tasks; do not ask again
|
|
174
|
+
* issueTree.dependencyLinkType the link type written for a story dependency
|
|
165
175
|
*/
|
|
166
176
|
const ISSUE_TREE_SETTINGS = [
|
|
167
177
|
"issueTree.enabled",
|
|
@@ -177,6 +187,9 @@ const ISSUE_TREE_SETTINGS = [
|
|
|
177
187
|
"issueTree.epicLinkFieldId",
|
|
178
188
|
"issueTree.cloneByChannel",
|
|
179
189
|
"issueTree.cloneLinkType",
|
|
190
|
+
"issueTree.storyPanels",
|
|
191
|
+
"issueTree.subtaskRolesDeclined",
|
|
192
|
+
"issueTree.dependencyLinkType",
|
|
180
193
|
];
|
|
181
194
|
|
|
182
195
|
function readPrefs() {
|
|
@@ -206,6 +219,100 @@ export function subtaskTypeFor(setting, role) {
|
|
|
206
219
|
return null;
|
|
207
220
|
}
|
|
208
221
|
|
|
222
|
+
const DEPENDS_ON = /\bdepends on:([^|\n]*)/gi;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The `Depends on: <ids>` clauses of a document's requirement rows.
|
|
226
|
+
*
|
|
227
|
+
* A clause belongs to the table row it sits in, in any cell, so a trailing
|
|
228
|
+
* column under any header works and so does a note appended to a cell. The
|
|
229
|
+
* row's subject is the first requirement id left once the clause is removed.
|
|
230
|
+
* Clauses outside a table row are not read: without a row there is no subject.
|
|
231
|
+
*
|
|
232
|
+
* `stripped` is the text with every read clause removed. Ids are defined by
|
|
233
|
+
* where they appear, so a reference left in place would define the id it
|
|
234
|
+
* points at, and a dependency on a requirement nobody wrote would plan a story
|
|
235
|
+
* for it instead of being reported.
|
|
236
|
+
*
|
|
237
|
+
* @returns {{deps: Map<string, Set<string>>, stripped: string}}
|
|
238
|
+
*/
|
|
239
|
+
export function dependencyClauses(text, corporate) {
|
|
240
|
+
const atom = corporate ? ATOM_CORP : ATOM_GLOBAL;
|
|
241
|
+
const deps = new Map();
|
|
242
|
+
const lines = text.split("\n");
|
|
243
|
+
for (const [i, line] of lines.entries()) {
|
|
244
|
+
if (!/^\s*\|/.test(line) || !/\bdepends on:/i.test(line)) continue;
|
|
245
|
+
const refs = [];
|
|
246
|
+
const rest = line.replace(DEPENDS_ON, (clause, ids) => {
|
|
247
|
+
const found = ids.match(atom);
|
|
248
|
+
if (!found) return clause;
|
|
249
|
+
refs.push(...found.map((x) => x.toUpperCase()));
|
|
250
|
+
return "";
|
|
251
|
+
});
|
|
252
|
+
const subject = (rest.match(atom) || [])[0]?.toUpperCase();
|
|
253
|
+
if (!subject || !refs.length) continue;
|
|
254
|
+
lines[i] = rest;
|
|
255
|
+
for (const r of refs) {
|
|
256
|
+
if (r === subject) continue;
|
|
257
|
+
if (!deps.has(subject)) deps.set(subject, new Set());
|
|
258
|
+
deps.get(subject).add(r);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return { deps, stripped: lines.join("\n") };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Requirement dependencies, read as story-to-story edges and checked as a
|
|
266
|
+
* graph. A story depends on another when one of its source ids depends on a
|
|
267
|
+
* source id of the other; two ids of one story give no edge. A dependency on
|
|
268
|
+
* an id no story covers, and a cycle between stories, are problems: the plan
|
|
269
|
+
* reports them and is not `ok`. An edge whose two stories sit in one strongly
|
|
270
|
+
* connected component carries `onCycle: true`; such an edge has no order to
|
|
271
|
+
* record, so it is never linked and never counted as a write.
|
|
272
|
+
*/
|
|
273
|
+
function storyDependencies(stories, reqDeps) {
|
|
274
|
+
const coverers = new Map();
|
|
275
|
+
for (const s of stories)
|
|
276
|
+
for (const id of s.sourceIds) {
|
|
277
|
+
if (!coverers.has(id)) coverers.set(id, []);
|
|
278
|
+
coverers.get(id).push(s);
|
|
279
|
+
}
|
|
280
|
+
const edges = new Map();
|
|
281
|
+
const missing = [];
|
|
282
|
+
for (const s of stories) {
|
|
283
|
+
for (const id of s.sourceIds) {
|
|
284
|
+
for (const dep of reqDeps.get(id) || []) {
|
|
285
|
+
const targets = coverers.get(dep);
|
|
286
|
+
if (!targets) {
|
|
287
|
+
missing.push({ id, dependsOn: dep });
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
for (const t of targets) {
|
|
291
|
+
if (t === s) continue;
|
|
292
|
+
const key = `${s.label}>${t.label}`;
|
|
293
|
+
if (!edges.has(key))
|
|
294
|
+
edges.set(key, { dependent: s.label, prerequisite: t.label, via: [] });
|
|
295
|
+
const via = `${dep} -> ${id}`;
|
|
296
|
+
if (!edges.get(key).via.includes(via)) edges.get(key).via.push(via);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
for (const s of stories) {
|
|
302
|
+
const on = [...edges.values()].filter((e) => e.dependent === s.label);
|
|
303
|
+
if (on.length) s.dependsOn = on.map((e) => e.prerequisite);
|
|
304
|
+
}
|
|
305
|
+
const graph = stories.map((s) => ({ id: s.label, deps: s.dependsOn || [], status: "pending" }));
|
|
306
|
+
const { cycles } = analyzeGraph(graph);
|
|
307
|
+
const component = cyclicComponentOf(graph);
|
|
308
|
+
for (const e of edges.values()) {
|
|
309
|
+
const c = component.get(e.dependent);
|
|
310
|
+
if (c !== undefined && c === component.get(e.prerequisite)) e.onCycle = true;
|
|
311
|
+
}
|
|
312
|
+
const unique = [...new Map(missing.map((m) => [`${m.id}>${m.dependsOn}`, m])).values()];
|
|
313
|
+
return { edges: [...edges.values()], cycles, missing: unique };
|
|
314
|
+
}
|
|
315
|
+
|
|
209
316
|
/** Every open placeholder that makes a document unfit to become work. */
|
|
210
317
|
export function openItems(text) {
|
|
211
318
|
const found = [];
|
|
@@ -258,10 +365,11 @@ export function coverageOf(defined, nodes, corporate = false) {
|
|
|
258
365
|
};
|
|
259
366
|
}
|
|
260
367
|
|
|
261
|
-
export function plan(
|
|
262
|
-
const fm = frontMatter(
|
|
368
|
+
export function plan(source, prefs, { pageUrl = null } = {}) {
|
|
369
|
+
const fm = frontMatter(source);
|
|
263
370
|
const profile = (fm.profile || "global").toLowerCase();
|
|
264
371
|
const corporate = profile === "corporate";
|
|
372
|
+
const { deps: reqDeps, stripped: text } = dependencyClauses(source, corporate);
|
|
265
373
|
const secs = sections(text);
|
|
266
374
|
|
|
267
375
|
const atomRe = corporate ? ATOM_CORP : ATOM_GLOBAL;
|
|
@@ -388,6 +496,7 @@ export function plan(text, prefs, { pageUrl = null } = {}) {
|
|
|
388
496
|
profile,
|
|
389
497
|
pageUrl: pageUrl || fm.confluence_url || null,
|
|
390
498
|
redesign: String(fm.redesign || "").toLowerCase() === "true",
|
|
499
|
+
panels: prefs.storyPanels,
|
|
391
500
|
};
|
|
392
501
|
for (const story of stories) {
|
|
393
502
|
const b = buildStoryBody(story, bodyCtx);
|
|
@@ -412,6 +521,30 @@ export function plan(text, prefs, { pageUrl = null } = {}) {
|
|
|
412
521
|
}
|
|
413
522
|
|
|
414
523
|
const coverage = coverageOf(defined, stories, corporate);
|
|
524
|
+
const dependencies = reqDeps.size ? storyDependencies(stories, reqDeps) : null;
|
|
525
|
+
if (dependencies) {
|
|
526
|
+
const titleOf = new Map(stories.map((s) => [s.label, s.group]));
|
|
527
|
+
const linked = new Set(dependencies.edges.map((e) => `${e.dependent}>${e.prerequisite}`));
|
|
528
|
+
const isLoop = (c) => c.every((l, i) => linked.has(`${l}>${c[(i + 1) % c.length]}`));
|
|
529
|
+
const problems = [
|
|
530
|
+
...dependencies.cycles.map((c) =>
|
|
531
|
+
isLoop(c)
|
|
532
|
+
? `dependency cycle: ${[c[0], ...[...c].reverse()].map((l) => titleOf.get(l)).join(" -> ")}`
|
|
533
|
+
: `dependency cycle among: ${c.map((l) => titleOf.get(l)).join(", ")}`,
|
|
534
|
+
),
|
|
535
|
+
...dependencies.missing.map(
|
|
536
|
+
(m) => `${m.id} depends on ${m.dependsOn}, which no story covers`,
|
|
537
|
+
),
|
|
538
|
+
];
|
|
539
|
+
if (problems.length) {
|
|
540
|
+
coverage.dependencyProblems = problems;
|
|
541
|
+
if (coverage.verdict === "ok") coverage.verdict = "incomplete";
|
|
542
|
+
if (coverage.verdict === "incomplete" && !coverage.reason) {
|
|
543
|
+
coverage.reason = `${problems.length} dependency problem(s): ${problems.join("; ")}`;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const linkType = prefs.dependencyLinkType ?? "Blocks";
|
|
415
548
|
|
|
416
549
|
return {
|
|
417
550
|
document: {
|
|
@@ -434,12 +567,17 @@ export function plan(text, prefs, { pageUrl = null } = {}) {
|
|
|
434
567
|
epicLinkFieldId: prefs.epicLinkFieldId ?? null,
|
|
435
568
|
cloneByChannel: prefs.cloneByChannel === true,
|
|
436
569
|
cloneLinkType: prefs.cloneLinkType ?? "Relates",
|
|
570
|
+
storyPanels: prefs.storyPanels ?? null,
|
|
571
|
+
subtaskRolesDeclined: prefs.subtaskRolesDeclined === true,
|
|
572
|
+
...(dependencies || prefs.dependencyLinkType !== undefined
|
|
573
|
+
? { dependencyLinkType: linkType }
|
|
574
|
+
: {}),
|
|
437
575
|
},
|
|
438
576
|
nodes: stories,
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
0,
|
|
442
|
-
|
|
577
|
+
...(dependencies ? { dependencies } : {}),
|
|
578
|
+
writeCount:
|
|
579
|
+
stories.reduce((n, s) => n + 1 + s.subtasks.length + 2 * (s.clones?.length || 0), 0) +
|
|
580
|
+
(dependencies?.edges.filter((e) => !e.onCycle).length || 0),
|
|
443
581
|
};
|
|
444
582
|
}
|
|
445
583
|
|
|
@@ -448,7 +586,9 @@ export function plan(text, prefs, { pageUrl = null } = {}) {
|
|
|
448
586
|
*
|
|
449
587
|
* @returns {object} a spec-consistency-gate result
|
|
450
588
|
*/
|
|
451
|
-
function consistencyOf(
|
|
589
|
+
function consistencyOf(source, nodes) {
|
|
590
|
+
const corporate = (frontMatter(source).profile || "").toLowerCase() === "corporate";
|
|
591
|
+
const text = dependencyClauses(source, corporate).stripped;
|
|
452
592
|
const constitution = resolveConstitution({
|
|
453
593
|
repo: dirname(file),
|
|
454
594
|
feature: frontMatter(text).feature || null,
|
|
@@ -464,7 +604,50 @@ function consistencyOf(text, nodes) {
|
|
|
464
604
|
});
|
|
465
605
|
}
|
|
466
606
|
|
|
607
|
+
/**
|
|
608
|
+
* Save the sub-tasks a person chose for every story: each chosen site type
|
|
609
|
+
* becomes a role named after it and mapped to it, or "none" records that the
|
|
610
|
+
* question was answered with no sub-tasks.
|
|
611
|
+
*/
|
|
612
|
+
function saveSubtasks(choice) {
|
|
613
|
+
const names = String(choice)
|
|
614
|
+
.split(",")
|
|
615
|
+
.map((n) => n.trim())
|
|
616
|
+
.filter(Boolean);
|
|
617
|
+
const none = names.length === 1 && names[0].toLowerCase() === "none";
|
|
618
|
+
if (!names.length || names.some((n) => n.length > 100 || /[\n{}]/.test(n)))
|
|
619
|
+
return die("analysis-story-tree: --save-subtasks takes type names or none", 3);
|
|
620
|
+
try {
|
|
621
|
+
updateJsonFileSync(
|
|
622
|
+
PREFS_PATH,
|
|
623
|
+
(j) => {
|
|
624
|
+
if (!j || typeof j !== "object" || Array.isArray(j)) return undefined;
|
|
625
|
+
j.global = j.global || {};
|
|
626
|
+
const t = { ...(j.global.issueTree || {}) };
|
|
627
|
+
if (none) {
|
|
628
|
+
t.subtaskRoles = [];
|
|
629
|
+
t.subtaskRolesDeclined = true;
|
|
630
|
+
} else {
|
|
631
|
+
t.subtaskRoles = names;
|
|
632
|
+
t.subtaskIssueType = Object.fromEntries(names.map((n) => [n, n]));
|
|
633
|
+
delete t.subtaskRolesDeclined;
|
|
634
|
+
}
|
|
635
|
+
j.global.issueTree = t;
|
|
636
|
+
return j;
|
|
637
|
+
},
|
|
638
|
+
{ mode: 0o600, followSymlink: true, timeoutMs: lockTimeoutFrom(process.env) },
|
|
639
|
+
);
|
|
640
|
+
} catch (e) {
|
|
641
|
+
return die(`analysis-story-tree: could not write ${PREFS_PATH}: ${e.message}`, 3);
|
|
642
|
+
}
|
|
643
|
+
process.stdout.write(
|
|
644
|
+
none ? "issueTree: no sub-tasks\n" : `issueTree: sub-tasks ${names.join(", ")}\n`,
|
|
645
|
+
);
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
|
|
467
649
|
async function main() {
|
|
650
|
+
if (SAVE_SUBTASKS !== null) return saveSubtasks(SAVE_SUBTASKS);
|
|
468
651
|
if (!file)
|
|
469
652
|
return die(
|
|
470
653
|
"usage: analysis-story-tree.mjs <analysis.md> [--json] [--brief] [--prefs FILE] [--page-url URL]",
|
|
@@ -550,12 +733,18 @@ async function main() {
|
|
|
550
733
|
);
|
|
551
734
|
if (c.uncovered.length) process.stdout.write(` not in any story: ${c.uncovered.join(", ")}\n`);
|
|
552
735
|
if (c.invented.length) process.stdout.write(` cited but undefined: ${c.invented.join(", ")}\n`);
|
|
736
|
+
for (const p of c.dependencyProblems || []) process.stdout.write(` ${p}\n`);
|
|
553
737
|
const k = result.consistency;
|
|
554
738
|
process.stdout.write(
|
|
555
739
|
`consistency: ${k.verdict} (${k.advisory ? "advisory, " : ""}${consistencyDetail(k)})\n`,
|
|
556
740
|
);
|
|
557
741
|
for (const line of describe(k)) process.stdout.write(`${line}\n`);
|
|
558
|
-
|
|
742
|
+
const linkCount =
|
|
743
|
+
result.nodes.reduce((n, s) => n + (s.clones?.length || 0), 0) +
|
|
744
|
+
(result.dependencies?.edges.filter((e) => !e.onCycle).length || 0);
|
|
745
|
+
process.stdout.write(
|
|
746
|
+
`\n${result.writeCount - linkCount} issue(s) and ${linkCount} link(s) would be created:\n`,
|
|
747
|
+
);
|
|
559
748
|
for (const s of result.nodes) {
|
|
560
749
|
process.stdout.write(` ${result.prefsUsed.storyIssueType} ${s.title}\n`);
|
|
561
750
|
process.stdout.write(
|
|
@@ -579,6 +768,18 @@ async function main() {
|
|
|
579
768
|
);
|
|
580
769
|
}
|
|
581
770
|
}
|
|
771
|
+
const d = result.dependencies;
|
|
772
|
+
if (d) {
|
|
773
|
+
const group = new Map(result.nodes.map((n) => [n.label, n.group]));
|
|
774
|
+
process.stdout.write(
|
|
775
|
+
`\ndependencies: ${d.edges.length} ${result.prefsUsed.dependencyLinkType} link(s), the prerequisite first:\n`,
|
|
776
|
+
);
|
|
777
|
+
for (const e of d.edges) {
|
|
778
|
+
process.stdout.write(
|
|
779
|
+
` ${group.get(e.prerequisite)} -> ${group.get(e.dependent)} (${e.via.join(", ")}) [${e.prerequisite} -> ${e.dependent}]${e.onCycle ? " not linked: on a dependency cycle" : ""}\n`,
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
582
783
|
// Every field's provenance, so a wrong setting is visible here rather than in
|
|
583
784
|
// Jira after the writes have happened.
|
|
584
785
|
process.stdout.write("\nfrom prefs.global.issueTree:\n");
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
* 1. The request, the state and the test log are read as bounded regular
|
|
14
14
|
* files (lib/regular-file.mjs): a FIFO, a device or an oversized file is
|
|
15
15
|
* refused, never read. The request is read once and validated in process
|
|
16
|
-
* (pr-request.mjs validatePrRequest) on those same bytes.
|
|
16
|
+
* (pr-request.mjs validatePrRequest) on those same bytes. The state's
|
|
17
|
+
* `decisions[]` is appended to the body as one redacted section
|
|
18
|
+
* (lib/decisions.mjs), so the PR lists what the run chose unasked.
|
|
17
19
|
* 2. The repository comes from the runner's own config (the queue item's
|
|
18
20
|
* `nameWithOwner`), never from the request. A request naming another
|
|
19
21
|
* repository is refused, not redirected. The base is the configured
|
|
@@ -104,6 +106,7 @@ import {
|
|
|
104
106
|
import { tmpdir } from "node:os";
|
|
105
107
|
import { dirname, isAbsolute, join } from "node:path";
|
|
106
108
|
|
|
109
|
+
import { appendDecisionsSection } from "../lib/decisions.mjs";
|
|
107
110
|
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
108
111
|
import { scan } from "../lib/outbound-gate.mjs";
|
|
109
112
|
import { prRequestPath, prRequestsDir, prVerdictPath } from "../lib/pr-request-location.mjs";
|
|
@@ -123,6 +126,7 @@ const GIT_TIMEOUT_MS = 60 * 1000;
|
|
|
123
126
|
const NET_TIMEOUT_MS = 5 * 60 * 1000;
|
|
124
127
|
// A valid request is at most a 256-character title and a 65536-character body.
|
|
125
128
|
const REQUEST_MAX_BYTES = 1024 * 1024;
|
|
129
|
+
const BODY_MAX_CHARS = 65536;
|
|
126
130
|
const STATE_MAX_BYTES = 16 * 1024 * 1024;
|
|
127
131
|
const LOG_MAX_BYTES = 64 * 1024 * 1024;
|
|
128
132
|
const VERIFY_TIMEOUT_SEC = 30 * 60;
|
|
@@ -1268,6 +1272,13 @@ export async function publish({ sessionId, statePath, item, config, env = proces
|
|
|
1268
1272
|
}
|
|
1269
1273
|
|
|
1270
1274
|
req = readRequest(reqPath);
|
|
1275
|
+
// What the run decided in place of a person goes into the PR it opens. The
|
|
1276
|
+
// section is redacted when rendered and passes the outbound gate below with
|
|
1277
|
+
// the rest of the body.
|
|
1278
|
+
req = {
|
|
1279
|
+
...req,
|
|
1280
|
+
body: appendDecisionsSection(req.body, state.decisions, { maxChars: BODY_MAX_CHARS }),
|
|
1281
|
+
};
|
|
1271
1282
|
|
|
1272
1283
|
const repoCfg = (config?.repos || []).find((r) => r && r.nameWithOwner === repo);
|
|
1273
1284
|
if (!repo || !repoCfg)
|
|
@@ -46,20 +46,12 @@
|
|
|
46
46
|
* 2 usage error
|
|
47
47
|
*/
|
|
48
48
|
|
|
49
|
-
import {
|
|
50
|
-
createReadStream,
|
|
51
|
-
existsSync,
|
|
52
|
-
mkdirSync,
|
|
53
|
-
readdirSync,
|
|
54
|
-
readFileSync,
|
|
55
|
-
statSync,
|
|
56
|
-
writeFileSync,
|
|
57
|
-
} from "node:fs";
|
|
58
|
-
import { createInterface } from "node:readline";
|
|
49
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
59
50
|
import { homedir } from "node:os";
|
|
60
51
|
import { dirname, join } from "node:path";
|
|
61
52
|
import { fileURLToPath } from "node:url";
|
|
62
53
|
import { costUsd } from "./_cost.mjs";
|
|
54
|
+
import { readJsonl } from "./_jsonl.mjs";
|
|
63
55
|
import { runMain } from "../lib/fatal.mjs";
|
|
64
56
|
import { messages, outputLanguage } from "../lib/user-facing.mjs";
|
|
65
57
|
|
|
@@ -263,20 +255,10 @@ async function readSeries({ days }) {
|
|
|
263
255
|
const unpricedModels = new Set();
|
|
264
256
|
|
|
265
257
|
for (const f of files) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
for await (const line of rl) {
|
|
273
|
-
if (!line || line.charCodeAt(0) !== 123) continue;
|
|
274
|
-
let d;
|
|
275
|
-
try {
|
|
276
|
-
d = JSON.parse(line);
|
|
277
|
-
} catch {
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
258
|
+
// Uncapped: a cost total that silently stops at a byte cap under-reports.
|
|
259
|
+
for await (const row of readJsonl(f.path, { maxBytes: Infinity, maxLineBytes: Infinity })) {
|
|
260
|
+
const d = row.record;
|
|
261
|
+
if (!d) continue;
|
|
280
262
|
const msg = d.message;
|
|
281
263
|
const u = msg?.usage;
|
|
282
264
|
if (!u) continue;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* decisions-log.mjs - append to and render `state.decisions[]`, the list of
|
|
4
|
+
* choices an autopilot run made where an attended run would have asked.
|
|
5
|
+
*
|
|
6
|
+
* Writers: a picker resolved without asking (picker-contract.md, autopilot),
|
|
7
|
+
* a split consensus taken on the triage verdict, a model fallback taken
|
|
8
|
+
* without the date-gate question, a rework dispatched one rung up
|
|
9
|
+
* (rework-escalation.mjs). gate-ledger.mjs park and review-decision-gate.mjs
|
|
10
|
+
* write their own entries through lib/decisions.mjs.
|
|
11
|
+
*
|
|
12
|
+
* `add` writes only while the quality gates are active (lib/unattended.mjs
|
|
13
|
+
* gatesActive): in an attended run the person answered, so there is nothing to
|
|
14
|
+
* report back. Writes take the write-state lock through gate-ledger.mjs
|
|
15
|
+
* updateState.
|
|
16
|
+
*
|
|
17
|
+
* `render` prints the markdown section Phase 5 puts in its report, redacted,
|
|
18
|
+
* and nothing at all for a run with no entries.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* decisions-log.mjs add --what <text> --why <text> --cost-if-wrong <text> --phase <0-5>
|
|
22
|
+
* [--state <path>] [--repo <dir>]
|
|
23
|
+
* decisions-log.mjs render [--state <path>] [--repo <dir>]
|
|
24
|
+
*
|
|
25
|
+
* Exit: 0 written, skipped (attended) or rendered · 2 usage · 3 no state or I/O error
|
|
26
|
+
*
|
|
27
|
+
* @module pipeline/scripts/decisions-log
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { writeFileSync } from "node:fs";
|
|
31
|
+
|
|
32
|
+
import { decisionEntry, renderDecisions, withDecision } from "../lib/decisions.mjs";
|
|
33
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
34
|
+
import { readRegularJson } from "../lib/regular-file.mjs";
|
|
35
|
+
import { gateContext, resolveStatePath, updateState } from "./gate-ledger.mjs";
|
|
36
|
+
|
|
37
|
+
class UsageError extends Error {}
|
|
38
|
+
|
|
39
|
+
function parse(argv) {
|
|
40
|
+
const opts = {};
|
|
41
|
+
for (let i = 1; i < argv.length; i++) {
|
|
42
|
+
const a = argv[i];
|
|
43
|
+
if (!a.startsWith("--")) throw new UsageError(`unexpected argument ${a}`);
|
|
44
|
+
const v = argv[++i];
|
|
45
|
+
if (v === undefined) throw new UsageError(`${a} needs a value`);
|
|
46
|
+
opts[a.slice(2)] = v;
|
|
47
|
+
}
|
|
48
|
+
return opts;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Append one entry when the gates are active. Resolves to whether it wrote.
|
|
53
|
+
*
|
|
54
|
+
* @param {{what: string, why: string, costIfWrong: string, phase: number}} input
|
|
55
|
+
* @param {{statePath?: string|null, repo?: string|null, env?: Record<string,string|undefined>}} [opts]
|
|
56
|
+
*/
|
|
57
|
+
export async function addDecision(
|
|
58
|
+
input,
|
|
59
|
+
{ statePath = null, repo = null, env = process.env } = {},
|
|
60
|
+
) {
|
|
61
|
+
const entry = decisionEntry(input);
|
|
62
|
+
const ctx = gateContext({ statePath, repo, env });
|
|
63
|
+
if (!ctx.active || !ctx.statePath) return { written: false, entry };
|
|
64
|
+
await updateState(ctx.statePath, (s) => withDecision(s, entry));
|
|
65
|
+
return { written: true, entry, statePath: ctx.statePath };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function main(argv) {
|
|
69
|
+
const mode = argv[0];
|
|
70
|
+
const opts = parse(argv);
|
|
71
|
+
if (mode === "add") {
|
|
72
|
+
let input;
|
|
73
|
+
try {
|
|
74
|
+
input = decisionEntry({
|
|
75
|
+
what: opts.what,
|
|
76
|
+
why: opts.why,
|
|
77
|
+
costIfWrong: opts["cost-if-wrong"],
|
|
78
|
+
phase: opts.phase,
|
|
79
|
+
});
|
|
80
|
+
} catch (err) {
|
|
81
|
+
throw new UsageError(err.message);
|
|
82
|
+
}
|
|
83
|
+
await addDecision(input, { statePath: opts.state || null, repo: opts.repo || process.cwd() });
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
if (mode === "render") {
|
|
87
|
+
const path = resolveStatePath({ statePath: opts.state, repo: opts.repo || process.cwd() });
|
|
88
|
+
if (!path) {
|
|
89
|
+
process.stderr.write("decisions-log: no run state found\n");
|
|
90
|
+
return 3;
|
|
91
|
+
}
|
|
92
|
+
const state = readRegularJson(path, null);
|
|
93
|
+
if (!state || typeof state !== "object") {
|
|
94
|
+
process.stderr.write(`decisions-log: cannot read ${path}\n`);
|
|
95
|
+
return 3;
|
|
96
|
+
}
|
|
97
|
+
const md = renderDecisions(state.decisions);
|
|
98
|
+
if (md) writeFileSync(1, md);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
throw new UsageError("mode must be add or render");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (invokedDirectly(import.meta.url)) {
|
|
105
|
+
main(process.argv.slice(2)).then(
|
|
106
|
+
(code) => process.exit(code),
|
|
107
|
+
(err) => {
|
|
108
|
+
process.stderr.write(`decisions-log: ${err.message}\n`);
|
|
109
|
+
if (err instanceof UsageError) {
|
|
110
|
+
process.stderr.write(
|
|
111
|
+
"usage: decisions-log.mjs add --what t --why t --cost-if-wrong t --phase n [--state p] | render [--state p]\n",
|
|
112
|
+
);
|
|
113
|
+
process.exit(2);
|
|
114
|
+
}
|
|
115
|
+
process.exit(3);
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
}
|