@mmerterden/multi-agent-pipeline 20.8.2 → 20.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/docs/facts.json +1 -1
  3. package/install/_common.mjs +49 -0
  4. package/install/_dev-only-files.mjs +1 -0
  5. package/install/catalog-history.json +1 -0
  6. package/install/claude.mjs +13 -7
  7. package/manifest.json +40 -30
  8. package/package.json +3 -2
  9. package/pipeline/lib/claude-md-links.mjs +328 -0
  10. package/pipeline/lib/owned-path-gate.mjs +699 -0
  11. package/pipeline/lib/repo-profile-derive.mjs +1771 -0
  12. package/pipeline/lib/repo-profile.mjs +780 -0
  13. package/pipeline/lib/stack-detect.sh +59 -19
  14. package/pipeline/lib/unattended.mjs +17 -0
  15. package/pipeline/multi-agent-refs/features/repo-profile.md +96 -0
  16. package/pipeline/multi-agent-refs/features/review-decision.md +18 -13
  17. package/pipeline/multi-agent-refs/features/stack-skill-routing.md +179 -33
  18. package/pipeline/multi-agent-refs/outside-the-pipeline.md +33 -11
  19. package/pipeline/multi-agent-refs/phases/phase-1-plan.md +26 -12
  20. package/pipeline/multi-agent-refs/phases/phase-2-dev.md +24 -13
  21. package/pipeline/multi-agent-refs/phases/phase-3-review.md +16 -4
  22. package/pipeline/multi-agent-refs/phases/phase-4-commit.md +1 -1
  23. package/pipeline/multi-agent-refs/phases/phase-5-report.md +8 -0
  24. package/pipeline/rules/outside-the-pipeline.md +6 -1
  25. package/pipeline/schemas/agent-state.schema.json +66 -2
  26. package/pipeline/schemas/phases.json +4 -4
  27. package/pipeline/schemas/repo-profile.schema.json +1107 -0
  28. package/pipeline/schemas/token-budget.json +4 -4
  29. package/pipeline/scripts/agent-guard.py +30 -0
  30. package/pipeline/scripts/owned-path-gate.mjs +205 -0
  31. package/pipeline/scripts/pre-commit-check.sh +151 -1
  32. package/pipeline/scripts/repo-profile.mjs +244 -0
  33. package/pipeline/scripts/review-decision-gate.mjs +42 -18
  34. package/pipeline/scripts/skill-candidates.mjs +882 -0
  35. package/pipeline/scripts/unattended_policy.py +90 -0
  36. package/pipeline/scripts/usage-report.mjs +36 -6
  37. package/pipeline/skills/.skill-manifest.json +1 -1
@@ -13,8 +13,11 @@
13
13
  * failing-test its `verification` (Step 3.7, verify-by-test) is
14
14
  * `confirmed` and `evidencePath` is a non-empty log inside the
15
15
  * checkout that shows a failing test and no pass;
16
- * test-integrity it is the finding test-integrity-gate.mjs produced for that
17
- * file, which is a command's output and not a model's.
16
+ * test-integrity it is the finding test-integrity-gate.mjs produced: the
17
+ * same file and the same tag or fingerprint, a command's
18
+ * output and not a model's;
19
+ * owned-path it is the finding owned-path-gate.mjs produced, matched
20
+ * the same way, for the same reason.
18
21
  *
19
22
  * Anything else is lowered to `important`, stays in `accepted[]` (triage judged
20
23
  * it real and in scope, so the rework still fixes it) and is listed with its
@@ -40,14 +43,15 @@
40
43
  *
41
44
  * Usage:
42
45
  * review-decision-gate.mjs <triage.json> [--state <agent-state.json>] [--iteration <n>]
43
- * [--source <reviewer-output.json>]... [--integrity <test-integrity.json>]
46
+ * [--source <reviewer-output.json>]... [--integrity <gate-findings.json>]...
44
47
  * [--repo <dir>] [--dry-run] [--json]
45
48
  * review-decision-gate.mjs --rebuttal-allowed [--state <agent-state.json>] [--json]
46
49
  *
47
50
  * Exit (gates active): 0 decided · 1 a rebuttal round ran, or --rebuttal-allowed refused ·
48
51
  * 2 usage · 3 unreadable input, or an accepted blocker with no reviewer record (an empty
49
- * `reviewers: []` is no record). A missing or empty --integrity file is no findings,
50
- * and a missing or empty --source is an audit that did not run.
52
+ * `reviewers: []` is no record, and a declared --integrity file that does not exist). An
53
+ * empty --integrity file is no findings, and a missing or empty --source is an audit
54
+ * that did not run.
51
55
  * Attended: 0, and 2 on a usage error.
52
56
  *
53
57
  * @module pipeline/scripts/review-decision-gate
@@ -171,6 +175,9 @@ export function rebuttalRecorded(reviewers) {
171
175
  );
172
176
  }
173
177
 
178
+ /** Tags of findings a deterministic gate produced, and the basis each one records. */
179
+ const GATE_BASIS = { test_integrity: "test-integrity", owned_path: "owned-path" };
180
+
174
181
  /**
175
182
  * Apply the decision rule. Pure: the input is not mutated.
176
183
  *
@@ -181,7 +188,7 @@ export function decide({ triage, reviewers, integrity = [], root }) {
181
188
  const next = structuredClone(triage);
182
189
  const sources = (Array.isArray(reviewers) ? reviewers : []).map(blockingOf);
183
190
  const gateFindings = (Array.isArray(integrity) ? integrity : []).filter(
184
- (f) => f && f.tag === "test_integrity",
191
+ (f) => f && GATE_BASIS[f.tag],
185
192
  );
186
193
  const kept = [];
187
194
  const downgraded = [];
@@ -198,8 +205,13 @@ export function decide({ triage, reviewers, integrity = [], root }) {
198
205
  kept.push({ ...id, basis: "corroborated", sources: count });
199
206
  continue;
200
207
  }
201
- if (gateFindings.some((g) => normalizeFile(g.file) === normalizeFile(f.file))) {
202
- kept.push({ ...id, basis: "test-integrity", sources: count });
208
+ const gate = gateFindings.find(
209
+ (g) =>
210
+ normalizeFile(g.file) === normalizeFile(f.file) &&
211
+ (f.tag === g.tag || fingerprintOf(f) === fingerprintOf(g)),
212
+ );
213
+ if (gate) {
214
+ kept.push({ ...id, basis: GATE_BASIS[gate.tag], sources: count });
203
215
  continue;
204
216
  }
205
217
  const v = f.verification;
@@ -236,38 +248,50 @@ function readJson(p, what) {
236
248
  }
237
249
 
238
250
  /**
239
- * Step 1.76's findings. No flag, no file or an empty file is the gate's own
240
- * "no findings" output; a file with content that is not JSON is unreadable.
251
+ * A deterministic gate's findings (Step 1.76 test-integrity, the owned-path
252
+ * gate). An empty file is the gate's own "no findings" output. Both gates
253
+ * always write their file, so a declared file that does not exist means the
254
+ * gate failed before writing, and that is an error rather than no findings;
255
+ * so is content that is not JSON.
241
256
  */
242
257
  function readIntegrity(p) {
243
- if (!p || !existsSync(p)) return [];
258
+ if (!existsSync(p)) {
259
+ throw new Error(`gate findings ${p} do not exist: the gate did not run or failed`);
260
+ }
244
261
  let body;
245
262
  try {
246
263
  body = readFileSync(p, "utf8");
247
264
  } catch (err) {
248
- throw new Error(`cannot read test-integrity output ${p}: ${err.message}`, { cause: err });
265
+ throw new Error(`cannot read gate findings ${p}: ${err.message}`, { cause: err });
249
266
  }
250
267
  if (!body.trim()) return [];
251
268
  try {
252
269
  const doc = JSON.parse(body);
253
270
  return Array.isArray(doc?.findings) ? doc.findings : [];
254
271
  } catch (err) {
255
- throw new Error(`cannot read test-integrity output ${p}: ${err.message}`, { cause: err });
272
+ throw new Error(`cannot read gate findings ${p}: ${err.message}`, { cause: err });
256
273
  }
257
274
  }
258
275
 
259
276
  function parse(argv) {
260
- const opts = { source: [], json: false, dryRun: false, rebuttalAllowed: false, triage: null };
261
- const single = new Set(["--state", "--iteration", "--integrity", "--repo"]);
277
+ const opts = {
278
+ source: [],
279
+ integrity: [],
280
+ json: false,
281
+ dryRun: false,
282
+ rebuttalAllowed: false,
283
+ triage: null,
284
+ };
285
+ const single = new Set(["--state", "--iteration", "--repo"]);
262
286
  for (let i = 0; i < argv.length; i++) {
263
287
  const a = argv[i];
264
288
  if (a === "--json") opts.json = true;
265
289
  else if (a === "--dry-run") opts.dryRun = true;
266
290
  else if (a === "--rebuttal-allowed") opts.rebuttalAllowed = true;
267
- else if (a === "--source" || single.has(a)) {
291
+ else if (a === "--source" || a === "--integrity" || single.has(a)) {
268
292
  const v = argv[++i];
269
293
  if (v === undefined) throw new Error(`${a} needs a value`);
270
- if (a === "--source") opts.source.push(v);
294
+ if (a === "--source" || a === "--integrity") opts[a.slice(2)].push(v);
271
295
  else opts[a.slice(2)] = v;
272
296
  } else if (a.startsWith("--")) throw new Error(`unexpected argument ${a}`);
273
297
  else if (opts.triage) throw new Error(`one triage file only, got ${a} too`);
@@ -341,7 +365,7 @@ async function main(argv) {
341
365
  throw new Error(`${opts.triage} is not a triage output (no accepted[])`);
342
366
  }
343
367
  const reviewers = reviewerRecord(opts, state);
344
- const integrity = readIntegrity(opts.integrity);
368
+ const integrity = opts.integrity.flatMap(readIntegrity);
345
369
  if (rebuttalRecorded(reviewers)) {
346
370
  result = {
347
371
  gate: GATE,