@mmerterden/multi-agent-pipeline 17.6.0 → 18.0.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 +127 -0
- package/README.md +43 -1
- package/README.tr.md +41 -0
- package/docs/adr/0011-dormant-ci.md +25 -1
- package/docs/server-readiness.md +188 -0
- package/index.js +16 -1
- package/install/_common.mjs +42 -17
- package/install/_dev-only-files.mjs +8 -0
- package/install/_unattended-profile.mjs +113 -0
- package/install/index.mjs +48 -0
- package/manifest.json +1049 -0
- package/package.json +5 -2
- package/pipeline/commands/multi-agent/status/SKILL.md +52 -21
- package/pipeline/lib/_jira-auth.sh +8 -0
- package/pipeline/lib/analysis-jira-write.sh +32 -0
- package/pipeline/lib/ask-choice.sh +13 -2
- package/pipeline/lib/autopilot-state.sh +8 -0
- package/pipeline/lib/fatal.mjs +129 -0
- package/pipeline/lib/figma-mcp-refresh.sh +18 -0
- package/pipeline/lib/figma-screenshot.sh +18 -0
- package/pipeline/lib/invoked-directly.mjs +43 -0
- package/pipeline/lib/jira-publish.sh +42 -0
- package/pipeline/lib/md2confluence-v3.py +47 -0
- package/pipeline/lib/outbound-gate.mjs +175 -0
- package/pipeline/lib/plan-todos.sh +27 -6
- package/pipeline/lib/post-pr-review.sh +77 -8
- package/pipeline/lib/repo-hygiene.sh +8 -3
- package/pipeline/lib/require-jq.sh +40 -0
- package/pipeline/lib/run-paths.sh +335 -0
- package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +70 -0
- package/pipeline/multi-agent-refs/features/cost-analysis.md +93 -0
- package/pipeline/multi-agent-refs/features/doctor.md +45 -0
- package/pipeline/multi-agent-refs/features/verify.md +83 -0
- package/pipeline/multi-agent-refs/phases/operations.md +13 -2
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
- package/pipeline/multi-agent-refs/unattended-contract.md +129 -0
- package/pipeline/scripts/_run-paths.mjs +372 -0
- package/pipeline/scripts/aggregate-metrics.mjs +64 -64
- package/pipeline/scripts/autopilot-arming.mjs +2 -1
- package/pipeline/scripts/autopilot-intake.mjs +2 -1
- package/pipeline/scripts/autopilot-runner.mjs +206 -2
- package/pipeline/scripts/build-references.mjs +2 -1
- package/pipeline/scripts/build-stack-plugins.mjs +10 -2
- package/pipeline/scripts/capture-evidence.sh +7 -2
- package/pipeline/scripts/classify-plan-safety.mjs +2 -1
- package/pipeline/scripts/cost-analyze.mjs +600 -0
- package/pipeline/scripts/cost-budget-check.mjs +4 -12
- package/pipeline/scripts/council-view.mjs +2 -1
- package/pipeline/scripts/crush-json.mjs +2 -1
- package/pipeline/scripts/diff-explain.mjs +6 -9
- package/pipeline/scripts/diff-risk-score.mjs +2 -1
- package/pipeline/scripts/doctor.mjs +138 -4
- package/pipeline/scripts/evidence-gate.mjs +9 -3
- package/pipeline/scripts/feedback-send.mjs +12 -2
- package/pipeline/scripts/gc-abandoned.sh +29 -13
- package/pipeline/scripts/gc-worktrees.sh +11 -4
- package/pipeline/scripts/github-ssh-setup.sh +64 -7
- package/pipeline/scripts/graph-mermaid.mjs +4 -2
- package/pipeline/scripts/keychain-save.sh +101 -30
- package/pipeline/scripts/learn-from-transcripts.mjs +2 -1
- package/pipeline/scripts/learning-curve.mjs +34 -29
- package/pipeline/scripts/make-manifest.mjs +199 -0
- package/pipeline/scripts/migrate-prefs.mjs +2 -1
- package/pipeline/scripts/migrate-state.mjs +94 -4
- package/pipeline/scripts/phase-banner.sh +6 -2
- package/pipeline/scripts/phase-tracker.sh +41 -3
- package/pipeline/scripts/plan-coverage-gate.mjs +6 -2
- package/pipeline/scripts/pre-commit-check.sh +7 -0
- package/pipeline/scripts/pre-push-check.sh +7 -0
- package/pipeline/scripts/purge.sh +23 -6
- package/pipeline/scripts/render-agent-log-cost.sh +9 -2
- package/pipeline/scripts/render-cost-summary.sh +9 -2
- package/pipeline/scripts/render-work-summary.sh +11 -4
- package/pipeline/scripts/review-file-filter.mjs +4 -2
- package/pipeline/scripts/review-scope.mjs +2 -1
- package/pipeline/scripts/routine-registry.mjs +2 -1
- package/pipeline/scripts/run-aggregator.mjs +13 -14
- package/pipeline/scripts/run-metrics.mjs +3 -1
- package/pipeline/scripts/runs-index.mjs +343 -0
- package/pipeline/scripts/scorecard-snapshot.mjs +178 -0
- package/pipeline/scripts/search-logs.sh +18 -0
- package/pipeline/scripts/test-gap-scan.mjs +2 -1
- package/pipeline/scripts/test-integrity-gate.mjs +2 -1
- package/pipeline/scripts/update-issue-progress.sh +56 -7
- package/pipeline/scripts/usage-report.mjs +12 -1
- package/pipeline/scripts/validate-analysis-doc.mjs +2 -1
- package/pipeline/scripts/validate-code-graph.mjs +6 -3
- package/pipeline/scripts/validate-complaint-doc.mjs +2 -1
- package/pipeline/scripts/validate-diff-risk.mjs +6 -3
- package/pipeline/scripts/validate-test-gap.mjs +6 -3
- package/pipeline/scripts/validate-triage.mjs +3 -1
- package/pipeline/scripts/verify-citations.mjs +4 -2
- package/pipeline/scripts/verify.mjs +327 -0
- package/pipeline/scripts/worktree-finalize.sh +13 -4
- package/pipeline/scripts/write-state.mjs +154 -15
- package/pipeline/skills/.skill-manifest.json +2 -2
- package/pipeline/skills/.skills-index.json +56 -1
- package/pipeline/skills/shared/README.md +8 -3
- package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +33 -9
- package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/package_app.sh +4 -1
- package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/setup_dev_signing.sh +4 -1
- package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/sign-and-notarize.sh +2 -1
- package/pipeline/skills/skills-index.md +6 -1
|
@@ -39,6 +39,7 @@ import { execFileSync } from "node:child_process";
|
|
|
39
39
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, renameSync, chmodSync } from "node:fs";
|
|
40
40
|
import { join, dirname } from "node:path";
|
|
41
41
|
import { homedir } from "node:os";
|
|
42
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
42
43
|
|
|
43
44
|
const ROOT = process.env.MA_AUTOPILOT_ROOT || join(homedir(), ".claude", "autopilot");
|
|
44
45
|
|
|
@@ -382,6 +383,6 @@ function main(argv) {
|
|
|
382
383
|
return 0;
|
|
383
384
|
}
|
|
384
385
|
|
|
385
|
-
if (import.meta.url
|
|
386
|
+
if (invokedDirectly(import.meta.url)) {
|
|
386
387
|
process.exit(main(process.argv));
|
|
387
388
|
}
|
|
@@ -51,10 +51,16 @@ import {
|
|
|
51
51
|
chmodSync,
|
|
52
52
|
readdirSync,
|
|
53
53
|
statSync,
|
|
54
|
+
openSync,
|
|
55
|
+
readSync,
|
|
56
|
+
closeSync,
|
|
57
|
+
truncateSync,
|
|
54
58
|
} from "node:fs";
|
|
55
59
|
import { join } from "node:path";
|
|
56
60
|
import { homedir } from "node:os";
|
|
57
61
|
import { randomUUID } from "node:crypto";
|
|
62
|
+
import { runMain } from "../lib/fatal.mjs";
|
|
63
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
58
64
|
|
|
59
65
|
const ROOT = process.env.MA_AUTOPILOT_ROOT || join(homedir(), ".claude", "autopilot");
|
|
60
66
|
// Siblings resolve from THIS file's own directory, not from ~/.claude/scripts.
|
|
@@ -76,6 +82,25 @@ const REGISTER_GRACE_MS = Number(process.env.MA_AP_GRACE_MS || 60000);
|
|
|
76
82
|
// A session in one of these is still working. Anything else - and a row with
|
|
77
83
|
// no status at all is not "anything else" - ends supervision.
|
|
78
84
|
const LIVE_STATUSES = new Set(["busy", "running", "waiting"]);
|
|
85
|
+
// launchd appends this process's stdout to runner.log forever. On a laptop that
|
|
86
|
+
// is a file nobody ever looks at; on a machine that ticks every few minutes for
|
|
87
|
+
// months it is the thing that fills the disk, and a full disk stops the runs it
|
|
88
|
+
// was logging.
|
|
89
|
+
const LOG_MAX_BYTES = Number(process.env.MA_AP_LOG_MAX_BYTES || 5 * 1024 * 1024);
|
|
90
|
+
// Consecutive failed attempts before the runner stops taking NEW work. The
|
|
91
|
+
// failure this guards against is a machine-level one - an expired token, a full
|
|
92
|
+
// disk, a `claude` that no longer launches - where every item fails the same
|
|
93
|
+
// way and the queue is consumed one worthless run at a time. Zero disables it.
|
|
94
|
+
const BREAKER_LIMIT = Number(process.env.MA_AP_BREAKER_LIMIT || 3);
|
|
95
|
+
// How long an open breaker waits before letting ONE attempt through. Long
|
|
96
|
+
// enough that a broken machine is not burning the queue (default 30 min, so at
|
|
97
|
+
// a 15-minute tick it is every other tick at most), short enough that a machine
|
|
98
|
+
// fixed at 3am is working again by morning without anyone touching it.
|
|
99
|
+
const BREAKER_COOLDOWN_SEC = Number(process.env.MA_AP_BREAKER_COOLDOWN_SEC || 1800);
|
|
100
|
+
// Outcomes that mean the attempt produced nothing. `needs-input` is NOT here:
|
|
101
|
+
// a run parked on a question did work and is waiting for a person, which is the
|
|
102
|
+
// system behaving correctly.
|
|
103
|
+
const FAILED_OUTCOMES = new Set(["launch-failed", "timed-out", "no-state", "failed", "unknown"]);
|
|
79
104
|
|
|
80
105
|
const log = (m) => process.stdout.write(`autopilot-runner: ${m}\n`);
|
|
81
106
|
|
|
@@ -120,6 +145,111 @@ function record(entry) {
|
|
|
120
145
|
chmodSync(p, 0o600);
|
|
121
146
|
}
|
|
122
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Keep runner.log bounded without breaking the writer.
|
|
150
|
+
*
|
|
151
|
+
* launchd holds the file open with O_APPEND, so RENAMING the log leaves that fd
|
|
152
|
+
* writing to the renamed inode and the new file stays empty forever - the
|
|
153
|
+
* rotation that looks right is the one that silently stops logging. Truncating
|
|
154
|
+
* the SAME inode is what works: the open fd keeps writing, at offset zero.
|
|
155
|
+
* The tail is copied aside first, because the last few hundred lines are the
|
|
156
|
+
* ones somebody is about to need.
|
|
157
|
+
*/
|
|
158
|
+
function rotateLog() {
|
|
159
|
+
const p = join(ROOT, "runner.log");
|
|
160
|
+
let size;
|
|
161
|
+
try {
|
|
162
|
+
size = statSync(p).size;
|
|
163
|
+
} catch {
|
|
164
|
+
// No log yet (a runner that has never been started by launchd), or it is
|
|
165
|
+
// unreadable. Nothing to rotate either way.
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (size <= LOG_MAX_BYTES) return;
|
|
169
|
+
try {
|
|
170
|
+
const keep = Math.min(size, 256 * 1024);
|
|
171
|
+
const fd = openSync(p, "r");
|
|
172
|
+
const buf = Buffer.alloc(keep);
|
|
173
|
+
readSync(fd, buf, 0, keep, size - keep);
|
|
174
|
+
closeSync(fd);
|
|
175
|
+
writeFileSync(join(ROOT, "runner.log.1"), buf, { mode: 0o600 });
|
|
176
|
+
// Truncate in place. Not unlink, not rename.
|
|
177
|
+
truncateSync(p, 0);
|
|
178
|
+
log(
|
|
179
|
+
`runner.log reached ${Math.round(size / 1048576)}MB - tail kept in runner.log.1, log truncated`,
|
|
180
|
+
);
|
|
181
|
+
} catch {
|
|
182
|
+
// A log that cannot be rotated is not a reason to skip the tick.
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* One line per tick, machine-readable, next to the human log. The human log
|
|
188
|
+
* answers "what happened just now"; this answers "how has it been behaving for
|
|
189
|
+
* a week", which is the question a server actually raises and the one prose
|
|
190
|
+
* cannot be asked.
|
|
191
|
+
*/
|
|
192
|
+
function tick(entry) {
|
|
193
|
+
try {
|
|
194
|
+
ensureRoot();
|
|
195
|
+
const p = join(ROOT, "ticks.jsonl");
|
|
196
|
+
appendFileSync(p, JSON.stringify({ at: new Date().toISOString(), ...entry }) + "\n", {
|
|
197
|
+
mode: 0o600,
|
|
198
|
+
});
|
|
199
|
+
} catch {
|
|
200
|
+
// Telemetry never fails a tick.
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* How many attempts in a row produced nothing.
|
|
206
|
+
*
|
|
207
|
+
* Read from attempted.jsonl rather than kept as a counter, because a counter is
|
|
208
|
+
* a second piece of state that can disagree with the ledger - and the ledger is
|
|
209
|
+
* what a person reads when they ask why the runner stopped.
|
|
210
|
+
*
|
|
211
|
+
* @returns {{count:number, last:string|null}}
|
|
212
|
+
*/
|
|
213
|
+
export function consecutiveFailures(lines) {
|
|
214
|
+
let count = 0;
|
|
215
|
+
let last = null;
|
|
216
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
217
|
+
const row = lines[i];
|
|
218
|
+
if (!row || typeof row.outcome !== "string") continue;
|
|
219
|
+
// A blocked item never ran: arming refused it, which says nothing about
|
|
220
|
+
// whether a run would have worked.
|
|
221
|
+
if (row.outcome.startsWith("blocked-")) continue;
|
|
222
|
+
if (FAILED_OUTCOMES.has(row.outcome)) {
|
|
223
|
+
count++;
|
|
224
|
+
if (!last) last = row.outcome;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
return { count, last };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function readAttempted() {
|
|
233
|
+
const p = join(ROOT, "attempted.jsonl");
|
|
234
|
+
if (!existsSync(p)) return [];
|
|
235
|
+
try {
|
|
236
|
+
return readFileSync(p, "utf-8")
|
|
237
|
+
.split("\n")
|
|
238
|
+
.filter(Boolean)
|
|
239
|
+
.slice(-50)
|
|
240
|
+
.map((l) => {
|
|
241
|
+
try {
|
|
242
|
+
return JSON.parse(l);
|
|
243
|
+
} catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
.filter(Boolean);
|
|
248
|
+
} catch {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
123
253
|
/** Seconds since the epoch at which this machine booted. */
|
|
124
254
|
export function bootTime() {
|
|
125
255
|
const out = run("sysctl", ["-n", "kern.boottime"]);
|
|
@@ -446,6 +576,61 @@ function main() {
|
|
|
446
576
|
return 0;
|
|
447
577
|
}
|
|
448
578
|
|
|
579
|
+
rotateLog();
|
|
580
|
+
|
|
581
|
+
// ---- 1b. BREAKER -------------------------------------------------------
|
|
582
|
+
// Three failures in a row is not three unlucky items, it is one broken
|
|
583
|
+
// machine: an expired token, a `claude` that no longer launches, a full disk.
|
|
584
|
+
// Left alone the runner eats the whole queue at a few minutes an item and
|
|
585
|
+
// records a failure for each, which destroys the evidence of WHICH item was
|
|
586
|
+
// first and leaves nothing to resume. Stopping keeps the queue intact.
|
|
587
|
+
const attempts = readAttempted();
|
|
588
|
+
const breaker = consecutiveFailures(attempts);
|
|
589
|
+
if (BREAKER_LIMIT > 0 && breaker.count >= BREAKER_LIMIT) {
|
|
590
|
+
// HALF-OPEN, not latched. The first version of this returned here on every
|
|
591
|
+
// tick, and the only writer of attempted.jsonl is downstream of this
|
|
592
|
+
// return - so once it opened, no new attempt could ever be recorded, the
|
|
593
|
+
// consecutive count could never fall, and the runner was stopped for good.
|
|
594
|
+
// The log line said "until an attempt succeeds" and the feature doc said
|
|
595
|
+
// "one successful attempt clears it": both described a state the code made
|
|
596
|
+
// unreachable.
|
|
597
|
+
//
|
|
598
|
+
// A breaker that cannot re-close is not a breaker, it is an off switch with
|
|
599
|
+
// a misleading label. So after a cooldown one probe is let through: if the
|
|
600
|
+
// machine recovered it succeeds and the count resets on its own; if it did
|
|
601
|
+
// not, that probe fails, becomes the new most recent attempt, and the
|
|
602
|
+
// breaker closes again for another cooldown. One wasted run per cooldown is
|
|
603
|
+
// the price of not needing a human to notice.
|
|
604
|
+
const lastAt = attempts.length ? Number(attempts[attempts.length - 1].at || 0) : 0;
|
|
605
|
+
const sinceSec = lastAt ? Math.floor(Date.now() / 1000) - lastAt : Infinity;
|
|
606
|
+
const probeDue = sinceSec >= BREAKER_COOLDOWN_SEC;
|
|
607
|
+
if (!probeDue) {
|
|
608
|
+
const why = `${breaker.count} attempts in a row produced nothing (last: ${breaker.last})`;
|
|
609
|
+
const waitMin = Math.ceil((BREAKER_COOLDOWN_SEC - sinceSec) / 60);
|
|
610
|
+
log(`circuit breaker open - ${why}`);
|
|
611
|
+
log(
|
|
612
|
+
` one probe run is allowed in ${waitMin} min; check the token, disk and \`claude\` binary`,
|
|
613
|
+
);
|
|
614
|
+
log(" to override for one tick: MA_AP_BREAKER_LIMIT=0");
|
|
615
|
+
if (!DRY) {
|
|
616
|
+
writeState("queue.json", {
|
|
617
|
+
...readJson(join(ROOT, "queue.json"), { queued: [], running: [] }),
|
|
618
|
+
blockedReason: `circuit breaker: ${why} - probe in ${waitMin} min`,
|
|
619
|
+
});
|
|
620
|
+
refreshStatus();
|
|
621
|
+
}
|
|
622
|
+
tick({
|
|
623
|
+
action: "breaker-open",
|
|
624
|
+
failures: breaker.count,
|
|
625
|
+
lastOutcome: breaker.last,
|
|
626
|
+
probeInSec: BREAKER_COOLDOWN_SEC - sinceSec,
|
|
627
|
+
});
|
|
628
|
+
return 0;
|
|
629
|
+
}
|
|
630
|
+
log(`circuit breaker half-open - ${breaker.count} failures, letting one probe run through`);
|
|
631
|
+
tick({ action: "breaker-probe", failures: breaker.count, lastOutcome: breaker.last });
|
|
632
|
+
}
|
|
633
|
+
|
|
449
634
|
// ---- 2. INTAKE ---------------------------------------------------------
|
|
450
635
|
const intake = join(SCRIPTS, "autopilot-intake.mjs");
|
|
451
636
|
if (existsSync(intake)) run(process.execPath, [intake]);
|
|
@@ -455,6 +640,7 @@ function main() {
|
|
|
455
640
|
const slots = Number(config.slots ?? 1);
|
|
456
641
|
if (running.length >= slots) {
|
|
457
642
|
log(`${running.length}/${slots} slots in use - nothing to take`);
|
|
643
|
+
tick({ action: "slots-full", running: running.length, slots });
|
|
458
644
|
return 0;
|
|
459
645
|
}
|
|
460
646
|
|
|
@@ -466,6 +652,11 @@ function main() {
|
|
|
466
652
|
const next = (queue.queued || []).find((i) => !busyRepos.has(i.repo));
|
|
467
653
|
if (!next) {
|
|
468
654
|
log(queue.emptyReason || "every queued item belongs to a repo already in flight");
|
|
655
|
+
tick({
|
|
656
|
+
action: "nothing-to-take",
|
|
657
|
+
queued: (queue.queued || []).length,
|
|
658
|
+
running: running.length,
|
|
659
|
+
});
|
|
469
660
|
return 0;
|
|
470
661
|
}
|
|
471
662
|
|
|
@@ -593,9 +784,22 @@ function main() {
|
|
|
593
784
|
log(`${next.id}: claim kept for the next tick (${supervised.reason})`);
|
|
594
785
|
}
|
|
595
786
|
log(`${next.id}: ${outcome}${prUrl ? ` ${prUrl}` : ""} (${supervised.waitedSec}s)`);
|
|
787
|
+
tick({
|
|
788
|
+
action: "ran",
|
|
789
|
+
source: next.source,
|
|
790
|
+
id: next.id,
|
|
791
|
+
taskId,
|
|
792
|
+
outcome,
|
|
793
|
+
reason: supervised.reason,
|
|
794
|
+
waitedSec: supervised.waitedSec,
|
|
795
|
+
prOpened: Boolean(prUrl),
|
|
796
|
+
consecutiveFailuresBefore: breaker.count,
|
|
797
|
+
});
|
|
596
798
|
return 0;
|
|
597
799
|
}
|
|
598
800
|
|
|
599
|
-
if (import.meta.url
|
|
600
|
-
|
|
801
|
+
if (invokedDirectly(import.meta.url)) {
|
|
802
|
+
runMain("autopilot-runner", () => {
|
|
803
|
+
process.exit(main());
|
|
804
|
+
});
|
|
601
805
|
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
// Exit codes: 0 ok, 1 coverage failure, 2 usage / parse error.
|
|
23
23
|
|
|
24
24
|
import { readFileSync } from "node:fs";
|
|
25
|
+
import { runMain } from "../lib/fatal.mjs";
|
|
25
26
|
|
|
26
27
|
const HEADERS = {
|
|
27
28
|
tr: ["Tür", "Kaynak", "URL / Yol", "Sürüm / Ref", "Rol", "Erişim", "Notlar"],
|
|
@@ -385,4 +386,4 @@ function main() {
|
|
|
385
386
|
process.stdout.write(`${renderTable(rows, lang)}\n`);
|
|
386
387
|
}
|
|
387
388
|
|
|
388
|
-
main
|
|
389
|
+
runMain("build-references", main);
|
|
@@ -33,7 +33,8 @@ import {
|
|
|
33
33
|
mkdirSync,
|
|
34
34
|
statSync,
|
|
35
35
|
} from "node:fs";
|
|
36
|
-
import { join } from "node:path";
|
|
36
|
+
import { dirname, join } from "node:path";
|
|
37
|
+
import { fileURLToPath } from "node:url";
|
|
37
38
|
import { spawnSync } from "node:child_process";
|
|
38
39
|
import { createHash } from "node:crypto";
|
|
39
40
|
|
|
@@ -65,7 +66,14 @@ if (!HOME) {
|
|
|
65
66
|
process.exit(2);
|
|
66
67
|
}
|
|
67
68
|
const PLUGINS_REPO = getArg("--plugins-repo", join(HOME, "multi-agent-plugins"));
|
|
68
|
-
|
|
69
|
+
// The pipeline root is THIS file's repo, found by walking up from the script,
|
|
70
|
+
// not a guess at where the checkout lives. It defaulted to
|
|
71
|
+
// $HOME/multi-agent-pipeline, which is true on the maintainer's machine and
|
|
72
|
+
// nowhere else: on a CI runner the checkout is under the workspace directory,
|
|
73
|
+
// so --check-routing reported "source not found" and three tests failed on
|
|
74
|
+
// every clean host while passing locally. That is the whole class of bug CI
|
|
75
|
+
// exists to catch, and it survived because CI was asleep.
|
|
76
|
+
const PIPE_ROOT = getArg("--pipeline", join(dirname(fileURLToPath(import.meta.url)), "..", ".."));
|
|
69
77
|
const EXTERNAL = join(PIPE_ROOT, "pipeline/skills/shared/external");
|
|
70
78
|
const DRY = args.includes("--dry-run");
|
|
71
79
|
|
|
@@ -207,7 +207,10 @@ case "$MODE" in
|
|
|
207
207
|
# stale video from yesterday's run attached as today's evidence is worse
|
|
208
208
|
# than no video, because nobody re-checks an artefact that is present.
|
|
209
209
|
SRC=""
|
|
210
|
-
for cand in $(find
|
|
210
|
+
# `while read`, not `for cand in $(find ...)`: word splitting turns one
|
|
211
|
+
# path with a space into two candidates, both of which fail `[ -f ]`, and
|
|
212
|
+
# the evidence goes missing without a word said.
|
|
213
|
+
while IFS= read -r cand; do
|
|
211
214
|
[ -f "$cand" ] || continue
|
|
212
215
|
MT=$(date -r "$cand" +%s 2>/dev/null || echo 0)
|
|
213
216
|
[ "$MT" -ge "$SINCE" ] 2>/dev/null || continue
|
|
@@ -215,7 +218,9 @@ case "$MODE" in
|
|
|
215
218
|
PREV=$(date -r "$SRC" +%s 2>/dev/null || echo 0)
|
|
216
219
|
[ "$MT" -gt "$PREV" ] 2>/dev/null && SRC="$cand"
|
|
217
220
|
fi
|
|
218
|
-
done
|
|
221
|
+
done <<EOF
|
|
222
|
+
$(find test-results cypress/videos -type f \( -name '*.webm' -o -name '*.mp4' \) 2>/dev/null)
|
|
223
|
+
EOF
|
|
219
224
|
[ -n "$SRC" ] || { echo "capture-evidence: the suite recorded no video after the marker (is video enabled in the project's runner config?)" >&2; exit 4; }
|
|
220
225
|
case "$SRC" in
|
|
221
226
|
*.webm)
|
|
@@ -197,5 +197,6 @@ if (recommendPause) {
|
|
|
197
197
|
summary = `Low-risk plan - autopilot safe`;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
// See run-metrics.mjs: exiting here would truncate this write when stdout is a
|
|
201
|
+
// pipe, and 0 is the default code anyway.
|
|
200
202
|
process.stdout.write(JSON.stringify({ score, recommendPause, reasons, summary }, null, 2) + "\n");
|
|
201
|
-
process.exit(0);
|