muse-crew 0.6.2 → 0.6.3
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/API.md +9 -0
- package/lib/AGENTS.md +1 -1
- package/lib/test-verify-merge.sh +75 -0
- package/lib/worktree-lifecycle.sh +38 -1
- package/package.json +1 -1
- package/workflows/bugfix.js +41 -1
- package/workflows/chore.js +41 -1
- package/workflows/standard.js +41 -1
package/API.md
CHANGED
|
@@ -294,6 +294,15 @@ Artifact Publish passes only when both are true:
|
|
|
294
294
|
|
|
295
295
|
Verification fails closed. If the provenance is missing, unreadable, or points at a different commit than the integrated HEAD, the workflow parks the task — it does not trust the worker's prose. Narrative remains useful but cannot independently establish deployment truth.
|
|
296
296
|
|
|
297
|
+
### Integrate merge verification
|
|
298
|
+
|
|
299
|
+
Integrate passes only when both are true:
|
|
300
|
+
|
|
301
|
+
1. The worker declares success.
|
|
302
|
+
2. Mechanical verification confirms the task branch tip is an ancestor of `main`: the workflow runs the lifecycle script's `verify-merge` command, which resolves the branch through the crew registry (never by reconstructing `task/<id>`), and matches the `VERIFIED:` marker on the script's own stdout with a regex — never by reading agent prose.
|
|
303
|
+
|
|
304
|
+
A genuine empty-diff Integrate (no commits ahead of `main` — a runtime-state deliverable) verifies vacuously: the branch tip is then an ancestor of `main`. Verification failure is an operational step failure, not a park: the dispatcher retries Integrate under its consecutive-failure cap, and the retry finds the commits still on the branch and performs the real merge.
|
|
305
|
+
|
|
297
306
|
---
|
|
298
307
|
|
|
299
308
|
## Not part of this API
|
package/lib/AGENTS.md
CHANGED
|
@@ -5,7 +5,7 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
|
|
|
5
5
|
- `build-registry.js` — deterministic extractor that generates `workflows/registry.json` (workflow step registry) from the workflow files' `meta` blocks at release time; invoked by `crew-release.sh` deploy
|
|
6
6
|
- `crew-release.sh` — immutable release manager: deploy, rollback, prune
|
|
7
7
|
- `merge-lock.sh` — serialized merge lock for concurrent agents: time-based holder lease (bug 2fc8f52f — an unexpired lease is held regardless of process liveness; only an expired lease may be broken). Lock file is key=value: task_id, opaque holder identity (never a PID), acquired_at epoch, lease_seconds (default 600, override via MERGE_LOCK_LEASE_SECONDS). acquire/refresh/release/status/force-release; holder-only refresh and release; every op appends to $CREW_HOME/.merge-lock.log
|
|
8
|
-
- `worktree-lifecycle.sh` — the worktree lifecycle seam: prepare/cleanup/inspect/integrate/status/post-deploy/refresh-lock/lock-status over git worktrees (`.worktrees/<id>`, branch `task/<id>`). The crew registry (`<repo>/.worktrees/.registry/<id>`) is the source of truth for task→branch/path — never reconstruct it from git state. Prepare fails closed on dirty `main`; cleanup is forgiving. `lock-status` reports the merge-lock state explicitly (`UNLOCKED`, or `LOCKED by <holder> since <ts> (pid <pid>)`, always exit 0) so Publish can distinguish an empty-diff Integrate (no lock taken) from a refresh failure.
|
|
8
|
+
- `worktree-lifecycle.sh` — the worktree lifecycle seam: prepare/cleanup/inspect/integrate/verify-merge/status/post-deploy/refresh-lock/lock-status over git worktrees (`.worktrees/<id>`, branch `task/<id>`). The crew registry (`<repo>/.worktrees/.registry/<id>`) is the source of truth for task→branch/path — never reconstruct it from git state. Prepare fails closed on dirty `main`; cleanup is forgiving. `lock-status` reports the merge-lock state explicitly (`UNLOCKED`, or `LOCKED by <holder> since <ts> (pid <pid>)`, always exit 0) so Publish can distinguish an empty-diff Integrate (no lock taken) from a refresh failure.
|
|
9
9
|
- `test-worktree-backend.sh` — regression tests for the lifecycle script (validate, prepare/reuse, inspect, status, cleanup, idempotent cleanup, dirty-main preflight) on a scratch repo
|
|
10
10
|
- `test-version-write.sh` — regression tests for the escape-preserving step-8 version write in publish-npm.sh (fixture: current package.json with the \\u2014 escape; extracts the shipped block by anchor)
|
|
11
11
|
- `test-publish-verify.sh` — regression tests for the retry-tolerant step-12 verification in publish-npm.sh (canary 5a027278): extracts the shipped block by anchor and runs it against a fake npm whose read replica lags (non-zero exits, then the old version, then the target) — requires convergence on success, fail-closed `PUBLISH_FAILED=verify` on exhaustion, and `--prefer-online` on every read
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# test-verify-merge.sh — regression tests for the verify-merge lifecycle
|
|
3
|
+
# command (bug b1b1f919: Integrate completed on empty merge while approved
|
|
4
|
+
# commits were still stranded on the task branch).
|
|
5
|
+
#
|
|
6
|
+
# Fixture: a scratch repo with main + prepared task branches.
|
|
7
|
+
# 1. missing branch → ERROR, non-zero
|
|
8
|
+
# 2. empty-diff branch (no commits ahead of main) → VERIFIED, exit 0
|
|
9
|
+
# 3. branch with commits ahead, not merged → NOT_MERGED, non-zero
|
|
10
|
+
# 4. after merging the branch → VERIFIED, exit 0
|
|
11
|
+
# 5. registry-divergent branch name → resolves via registry, not via
|
|
12
|
+
# reconstructed "task/<id>" (verifying the wrong branch is the same
|
|
13
|
+
# bug in different clothes)
|
|
14
|
+
# 6. invalid task id → rejected
|
|
15
|
+
set -u
|
|
16
|
+
|
|
17
|
+
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
18
|
+
LIFECYCLE="$REPO_DIR/lib/worktree-lifecycle.sh"
|
|
19
|
+
|
|
20
|
+
fail() { echo "FAIL: $1"; exit 1; }
|
|
21
|
+
|
|
22
|
+
T=/tmp/crew-verify-merge-test
|
|
23
|
+
rm -rf "$T"
|
|
24
|
+
mkdir -p "$T"
|
|
25
|
+
cd "$T"
|
|
26
|
+
git init -q -b main .
|
|
27
|
+
git config user.email test@test.t
|
|
28
|
+
git config user.name test
|
|
29
|
+
echo x > f.txt
|
|
30
|
+
echo ".worktrees/" > .gitignore
|
|
31
|
+
git add -A
|
|
32
|
+
git commit -qm init
|
|
33
|
+
|
|
34
|
+
export CREW_REPO="$T"
|
|
35
|
+
tid="verifytest1"
|
|
36
|
+
|
|
37
|
+
# 1. missing branch → ERROR, non-zero
|
|
38
|
+
out=$(bash "$LIFECYCLE" verify-merge "$tid" 2>&1) && fail "verify-merge on missing branch exited 0"
|
|
39
|
+
echo "$out" | grep -q '^ERROR: branch task/verifytest1 does not exist' || fail "missing branch: expected ERROR, got: $out"
|
|
40
|
+
|
|
41
|
+
# 2. prepare (empty diff) → VERIFIED, exit 0
|
|
42
|
+
out=$(bash "$LIFECYCLE" prepare "$tid") || fail "prepare exited non-zero: $out"
|
|
43
|
+
out=$(bash "$LIFECYCLE" verify-merge "$tid") || fail "verify-merge on empty-diff branch exited non-zero: $out"
|
|
44
|
+
echo "$out" | grep -q '^VERIFIED:' || fail "empty-diff: expected VERIFIED, got: $out"
|
|
45
|
+
|
|
46
|
+
# 3. commit on the branch, not merged → NOT_MERGED, non-zero
|
|
47
|
+
echo change >> "$T/.worktrees/$tid/f.txt"
|
|
48
|
+
(cd "$T/.worktrees/$tid" && git commit -qam work)
|
|
49
|
+
out=$(bash "$LIFECYCLE" verify-merge "$tid" 2>&1) && fail "verify-merge on unmerged branch exited 0"
|
|
50
|
+
echo "$out" | grep -q '^NOT_MERGED:' || fail "unmerged: expected NOT_MERGED, got: $out"
|
|
51
|
+
echo "$out" | grep -q '1 commit(s) still ahead of main' || fail "unmerged: expected ahead count, got: $out"
|
|
52
|
+
|
|
53
|
+
# 4. merge the branch → VERIFIED, exit 0
|
|
54
|
+
(cd "$T" && git merge -q --no-ff "task/$tid" -m "merge: $tid")
|
|
55
|
+
out=$(bash "$LIFECYCLE" verify-merge "$tid") || fail "verify-merge after merge exited non-zero: $out"
|
|
56
|
+
echo "$out" | grep -q '^VERIFIED:' || fail "merged: expected VERIFIED, got: $out"
|
|
57
|
+
|
|
58
|
+
# 5. registry-divergent branch name → resolve_branch wins over reconstruction
|
|
59
|
+
tid2="verifytest2"
|
|
60
|
+
out=$(bash "$LIFECYCLE" prepare "$tid2") || fail "prepare 2 exited non-zero: $out"
|
|
61
|
+
(cd "$T" && git branch -m "task/$tid2" "custom/$tid2")
|
|
62
|
+
sed -i "s|^branch=task/$tid2\$|branch=custom/$tid2|" "$T/.worktrees/.registry/$tid2"
|
|
63
|
+
echo change2 >> "$T/.worktrees/$tid2/f.txt"
|
|
64
|
+
(cd "$T/.worktrees/$tid2" && git commit -qam work2)
|
|
65
|
+
out=$(bash "$LIFECYCLE" verify-merge "$tid2" 2>&1) && fail "verify-merge on divergent unmerged branch exited 0"
|
|
66
|
+
echo "$out" | grep -q '^NOT_MERGED: custom/verifytest2 ' || fail "divergent: expected NOT_MERGED on custom/verifytest2, got: $out"
|
|
67
|
+
|
|
68
|
+
# 6. invalid task id rejected
|
|
69
|
+
bash "$LIFECYCLE" verify-merge "bad id!" >/dev/null 2>&1 && fail "verify-merge accepted a bad id"
|
|
70
|
+
|
|
71
|
+
bash "$LIFECYCLE" cleanup "$tid" >/dev/null 2>&1 || true
|
|
72
|
+
(cd "$T" && git branch -D "custom/$tid2" >/dev/null 2>&1) || true
|
|
73
|
+
rm -rf "$T/.worktrees/$tid2" "$T/.worktrees/.registry/$tid2"
|
|
74
|
+
rm -rf "$T"
|
|
75
|
+
echo "verify-merge: OK"
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
# prepare <task_id> — create worktree + branch, register ownership
|
|
16
16
|
# inspect <task_id> — diff against main for review
|
|
17
17
|
# integrate <task_id> <commit_msg> — merge lock → merge into main → report commit
|
|
18
|
+
# verify-merge <task_id> — confirm the task branch tip is an ancestor of main (post-step merge check)
|
|
18
19
|
# post-deploy <task_id> — commit builder artifacts, cleanup, release lock
|
|
19
20
|
# cleanup <task_id> — remove worktree + branch (no lock)
|
|
20
21
|
# status <task_id> — report worktree state
|
|
@@ -250,6 +251,41 @@ cmd_integrate() {
|
|
|
250
251
|
echo "LOCK: held by $task_id — release after deploy with post-deploy"
|
|
251
252
|
}
|
|
252
253
|
|
|
254
|
+
cmd_verify_merge() {
|
|
255
|
+
local task_id="$1"
|
|
256
|
+
validate_task_id "$task_id"
|
|
257
|
+
|
|
258
|
+
# Resolve through the registry — never reconstruct "task/<id>" here. The
|
|
259
|
+
# workflow's TASK_BRANCH constant and the registry can diverge, and
|
|
260
|
+
# verifying the wrong branch is the same bug in different clothes
|
|
261
|
+
# (b1b1f919).
|
|
262
|
+
local branch
|
|
263
|
+
branch=$(resolve_branch "$task_id")
|
|
264
|
+
|
|
265
|
+
cd "$REPO"
|
|
266
|
+
|
|
267
|
+
if ! git rev-parse --verify "$branch" >/dev/null 2>&1; then
|
|
268
|
+
echo "ERROR: branch $branch does not exist"
|
|
269
|
+
return 1
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
local tip
|
|
273
|
+
tip=$(git rev-parse "$branch")
|
|
274
|
+
|
|
275
|
+
# The merge landed iff the task branch tip is an ancestor of main. A
|
|
276
|
+
# genuine empty-diff Integrate (MERGED_EMPTY: no commits ahead of main)
|
|
277
|
+
# verifies vacuously — the tip is then main itself or behind it.
|
|
278
|
+
if git merge-base --is-ancestor "$branch" main 2>/dev/null; then
|
|
279
|
+
echo "VERIFIED: $branch tip $tip is an ancestor of main — merge landed"
|
|
280
|
+
return 0
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
local ahead
|
|
284
|
+
ahead=$(git rev-list --count "main..$branch")
|
|
285
|
+
echo "NOT_MERGED: $branch tip $tip is not an ancestor of main — $ahead commit(s) still ahead of main"
|
|
286
|
+
return 1
|
|
287
|
+
}
|
|
288
|
+
|
|
253
289
|
cmd_post_deploy() {
|
|
254
290
|
local task_id="$1"
|
|
255
291
|
validate_task_id "$task_id"
|
|
@@ -388,6 +424,7 @@ case "$cmd" in
|
|
|
388
424
|
prepare) cmd_prepare "${1:?task_id required}" ;;
|
|
389
425
|
inspect) cmd_inspect "${1:?task_id required}" ;;
|
|
390
426
|
integrate) cmd_integrate "${1:?task_id required}" "${2:-}" ;;
|
|
427
|
+
verify-merge) cmd_verify_merge "${1:?task_id required}" ;;
|
|
391
428
|
post-deploy) cmd_post_deploy "${1:?task_id required}" ;;
|
|
392
429
|
cleanup) cmd_cleanup "${1:?task_id required}" ;;
|
|
393
430
|
status) cmd_status "${1:?task_id required}" ;;
|
|
@@ -396,7 +433,7 @@ case "$cmd" in
|
|
|
396
433
|
sweep) cmd_sweep "${1:-report}" ;;
|
|
397
434
|
*)
|
|
398
435
|
echo "Usage: worktree-lifecycle.sh <command> [args]"
|
|
399
|
-
echo "Commands: validate, prepare, inspect, integrate, post-deploy, cleanup, status, refresh-lock, lock-status, sweep"
|
|
436
|
+
echo "Commands: validate, prepare, inspect, integrate, verify-merge, post-deploy, cleanup, status, refresh-lock, lock-status, sweep"
|
|
400
437
|
exit 1
|
|
401
438
|
;;
|
|
402
439
|
esac
|
package/package.json
CHANGED
package/workflows/bugfix.js
CHANGED
|
@@ -1342,7 +1342,47 @@ while (i < STEPS.length) {
|
|
|
1342
1342
|
passed: verdictPassed !== null ? verdictPassed : true,
|
|
1343
1343
|
summary: workerText
|
|
1344
1344
|
};
|
|
1345
|
-
|
|
1345
|
+
let passed = stepResult.passed === true;
|
|
1346
|
+
|
|
1347
|
+
// Deterministic integrate verification: the agent cannot self-certify a
|
|
1348
|
+
// merge. After the Integrate agent claims success, the workflow confirms
|
|
1349
|
+
// mechanically that the task branch tip is an ancestor of main via the
|
|
1350
|
+
// lifecycle script's verify-merge command (which resolves the branch
|
|
1351
|
+
// through the crew registry — never by reconstructing "task/"+taskId — so
|
|
1352
|
+
// the check cannot verify the wrong branch). The VERIFIED marker is matched
|
|
1353
|
+
// by regex on the script's own stdout; agent prose is never read. This
|
|
1354
|
+
// closes the hole where an agent reported "merged empty" while approved
|
|
1355
|
+
// commits were still stranded on the task branch (bug b1b1f919). A genuine
|
|
1356
|
+
// empty-diff Integrate (MERGED_EMPTY: no commits ahead of main) verifies
|
|
1357
|
+
// vacuously — the tip is then an ancestor of main. Verification failure is
|
|
1358
|
+
// an operational step failure, not a park: the dispatcher retries Integrate
|
|
1359
|
+
// under its consecutive-failure cap, and the retry finds the commits still
|
|
1360
|
+
// on the branch and performs the real merge — self-healing.
|
|
1361
|
+
if (step.name === "Integrate" && passed) {
|
|
1362
|
+
var integrateVerifyOut = "";
|
|
1363
|
+
try {
|
|
1364
|
+
var integrateVerifyResult = await agent(
|
|
1365
|
+
"Run: " + LIFECYCLE_ENV + LIFECYCLE + " verify-merge " + taskId + " 2>&1 || true\n" +
|
|
1366
|
+
"Return JSON { \"output\": \"<the command's full stdout, trimmed>\" } and nothing else.",
|
|
1367
|
+
{ key: attemptKey("verify-merge-" + taskId, totalReworkCount), label: "Verifying merge landed",
|
|
1368
|
+
schema: { type: "object", properties: { output: { type: "string" } }, required: ["output"] } }
|
|
1369
|
+
);
|
|
1370
|
+
integrateVerifyOut = (integrateVerifyResult.output || "").trim();
|
|
1371
|
+
} catch (e) {
|
|
1372
|
+
integrateVerifyOut = "";
|
|
1373
|
+
}
|
|
1374
|
+
if (/^VERIFIED:/m.test(integrateVerifyOut)) {
|
|
1375
|
+
log("Integrate verified for task " + taskId + ": task branch tip is an ancestor of main");
|
|
1376
|
+
} else {
|
|
1377
|
+
var integrateVerifyReason = integrateVerifyOut
|
|
1378
|
+
? integrateVerifyOut.split("\n")[0].slice(0, 200)
|
|
1379
|
+
: "verify-merge produced no usable output";
|
|
1380
|
+
log("Integrate verification failed for task " + taskId + ": " + integrateVerifyReason + " — marking failed for retry");
|
|
1381
|
+
stepResult.summary = (stepResult.summary || "") + "\nintegrate_verify: FAILED — " + integrateVerifyReason;
|
|
1382
|
+
passed = false;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1346
1386
|
// "rejected" is an explicit phase verdict routed through rework (Review/QA,
|
|
1347
1387
|
// and the Build/Reproduce reports that feed them). Integrate/Publish work
|
|
1348
1388
|
// that did not finish is operational — "failed", retryable under the
|
package/workflows/chore.js
CHANGED
|
@@ -1218,7 +1218,47 @@ while (i < STEPS.length) {
|
|
|
1218
1218
|
passed: verdictPassed !== null ? verdictPassed : true,
|
|
1219
1219
|
summary: workerText
|
|
1220
1220
|
};
|
|
1221
|
-
|
|
1221
|
+
let passed = stepResult.passed === true;
|
|
1222
|
+
|
|
1223
|
+
// Deterministic integrate verification: the agent cannot self-certify a
|
|
1224
|
+
// merge. After the Integrate agent claims success, the workflow confirms
|
|
1225
|
+
// mechanically that the task branch tip is an ancestor of main via the
|
|
1226
|
+
// lifecycle script's verify-merge command (which resolves the branch
|
|
1227
|
+
// through the crew registry — never by reconstructing "task/"+taskId — so
|
|
1228
|
+
// the check cannot verify the wrong branch). The VERIFIED marker is matched
|
|
1229
|
+
// by regex on the script's own stdout; agent prose is never read. This
|
|
1230
|
+
// closes the hole where an agent reported "merged empty" while approved
|
|
1231
|
+
// commits were still stranded on the task branch (bug b1b1f919). A genuine
|
|
1232
|
+
// empty-diff Integrate (MERGED_EMPTY: no commits ahead of main) verifies
|
|
1233
|
+
// vacuously — the tip is then an ancestor of main. Verification failure is
|
|
1234
|
+
// an operational step failure, not a park: the dispatcher retries Integrate
|
|
1235
|
+
// under its consecutive-failure cap, and the retry finds the commits still
|
|
1236
|
+
// on the branch and performs the real merge — self-healing.
|
|
1237
|
+
if (step.name === "Integrate" && passed) {
|
|
1238
|
+
var integrateVerifyOut = "";
|
|
1239
|
+
try {
|
|
1240
|
+
var integrateVerifyResult = await agent(
|
|
1241
|
+
"Run: " + LIFECYCLE_ENV + LIFECYCLE + " verify-merge " + taskId + " 2>&1 || true\n" +
|
|
1242
|
+
"Return JSON { \"output\": \"<the command's full stdout, trimmed>\" } and nothing else.",
|
|
1243
|
+
{ key: attemptKey("verify-merge-" + taskId, reworkCount), label: "Verifying merge landed",
|
|
1244
|
+
schema: { type: "object", properties: { output: { type: "string" } }, required: ["output"] } }
|
|
1245
|
+
);
|
|
1246
|
+
integrateVerifyOut = (integrateVerifyResult.output || "").trim();
|
|
1247
|
+
} catch (e) {
|
|
1248
|
+
integrateVerifyOut = "";
|
|
1249
|
+
}
|
|
1250
|
+
if (/^VERIFIED:/m.test(integrateVerifyOut)) {
|
|
1251
|
+
log("Integrate verified for task " + taskId + ": task branch tip is an ancestor of main");
|
|
1252
|
+
} else {
|
|
1253
|
+
var integrateVerifyReason = integrateVerifyOut
|
|
1254
|
+
? integrateVerifyOut.split("\n")[0].slice(0, 200)
|
|
1255
|
+
: "verify-merge produced no usable output";
|
|
1256
|
+
log("Integrate verification failed for task " + taskId + ": " + integrateVerifyReason + " — marking failed for retry");
|
|
1257
|
+
stepResult.summary = (stepResult.summary || "") + "\nintegrate_verify: FAILED — " + integrateVerifyReason;
|
|
1258
|
+
passed = false;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1222
1262
|
// "rejected" is an explicit phase verdict routed through rework (Review).
|
|
1223
1263
|
// Integrate/Publish work that did not finish is operational — "failed",
|
|
1224
1264
|
// retryable under the dispatcher's consecutive-failure cap.
|
package/workflows/standard.js
CHANGED
|
@@ -1352,7 +1352,47 @@ while (i < STEPS.length) {
|
|
|
1352
1352
|
passed: verdictPassed !== null ? verdictPassed : true,
|
|
1353
1353
|
summary: workerText
|
|
1354
1354
|
};
|
|
1355
|
-
|
|
1355
|
+
let passed = stepResult.passed === true;
|
|
1356
|
+
|
|
1357
|
+
// Deterministic integrate verification: the agent cannot self-certify a
|
|
1358
|
+
// merge. After the Integrate agent claims success, the workflow confirms
|
|
1359
|
+
// mechanically that the task branch tip is an ancestor of main via the
|
|
1360
|
+
// lifecycle script's verify-merge command (which resolves the branch
|
|
1361
|
+
// through the crew registry — never by reconstructing "task/"+taskId — so
|
|
1362
|
+
// the check cannot verify the wrong branch). The VERIFIED marker is matched
|
|
1363
|
+
// by regex on the script's own stdout; agent prose is never read. This
|
|
1364
|
+
// closes the hole where an agent reported "merged empty" while approved
|
|
1365
|
+
// commits were still stranded on the task branch (bug b1b1f919). A genuine
|
|
1366
|
+
// empty-diff Integrate (MERGED_EMPTY: no commits ahead of main) verifies
|
|
1367
|
+
// vacuously — the tip is then an ancestor of main. Verification failure is
|
|
1368
|
+
// an operational step failure, not a park: the dispatcher retries Integrate
|
|
1369
|
+
// under its consecutive-failure cap, and the retry finds the commits still
|
|
1370
|
+
// on the branch and performs the real merge — self-healing.
|
|
1371
|
+
if (step.name === "Integrate" && passed) {
|
|
1372
|
+
var integrateVerifyOut = "";
|
|
1373
|
+
try {
|
|
1374
|
+
var integrateVerifyResult = await agent(
|
|
1375
|
+
"Run: " + LIFECYCLE_ENV + LIFECYCLE + " verify-merge " + taskId + " 2>&1 || true\n" +
|
|
1376
|
+
"Return JSON { \"output\": \"<the command's full stdout, trimmed>\" } and nothing else.",
|
|
1377
|
+
{ key: attemptKey("verify-merge-" + taskId, totalReworkCount), label: "Verifying merge landed",
|
|
1378
|
+
schema: { type: "object", properties: { output: { type: "string" } }, required: ["output"] } }
|
|
1379
|
+
);
|
|
1380
|
+
integrateVerifyOut = (integrateVerifyResult.output || "").trim();
|
|
1381
|
+
} catch (e) {
|
|
1382
|
+
integrateVerifyOut = "";
|
|
1383
|
+
}
|
|
1384
|
+
if (/^VERIFIED:/m.test(integrateVerifyOut)) {
|
|
1385
|
+
log("Integrate verified for task " + taskId + ": task branch tip is an ancestor of main");
|
|
1386
|
+
} else {
|
|
1387
|
+
var integrateVerifyReason = integrateVerifyOut
|
|
1388
|
+
? integrateVerifyOut.split("\n")[0].slice(0, 200)
|
|
1389
|
+
: "verify-merge produced no usable output";
|
|
1390
|
+
log("Integrate verification failed for task " + taskId + ": " + integrateVerifyReason + " — marking failed for retry");
|
|
1391
|
+
stepResult.summary = (stepResult.summary || "") + "\nintegrate_verify: FAILED — " + integrateVerifyReason;
|
|
1392
|
+
passed = false;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1356
1396
|
// "rejected" is an explicit phase verdict routed through rework (Review/QA,
|
|
1357
1397
|
// and the Build/Reproduce reports that feed them). Integrate/Publish work
|
|
1358
1398
|
// that did not finish is operational — "failed", retryable under the
|