muse-crew 0.4.5 → 0.4.7
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/lib/AGENTS.md +5 -4
- package/lib/merge-lock.sh +149 -68
- package/lib/orphan-sweep.sh +38 -34
- package/lib/publish-npm.sh +141 -105
- package/lib/test-merge-lock.sh +77 -34
- package/lib/test-orphan-sweep.sh +19 -5
- package/lib/test-publish-skip.sh +192 -0
- package/lib/worktree-lifecycle.sh +46 -8
- package/package.json +1 -1
- package/workflows/bugfix.js +144 -36
- package/workflows/chore.js +141 -35
- package/workflows/standard.js +144 -36
package/workflows/standard.js
CHANGED
|
@@ -509,6 +509,12 @@ if (missingInitialPins.length > 0) {
|
|
|
509
509
|
}
|
|
510
510
|
log("Lifecycle scripts pinned to " + RUN_LIB);
|
|
511
511
|
|
|
512
|
+
// Merge-lock holder identity (bug 2fc8f52f): the opaque task+run identity
|
|
513
|
+
// minted at this run's first claim (never a PID — short-lived agent PIDs
|
|
514
|
+
// made every concurrent acquire take the stale path). Set exactly once on
|
|
515
|
+
// the first claim; stable for the rest of the run's lifetime.
|
|
516
|
+
let lockHolder = taskId;
|
|
517
|
+
|
|
512
518
|
while (i < STEPS.length) {
|
|
513
519
|
const step = STEPS[i];
|
|
514
520
|
const isFirstClaim = (i === startStepIndex && totalReworkCount === 0);
|
|
@@ -692,6 +698,13 @@ while (i < STEPS.length) {
|
|
|
692
698
|
activeSessionId = claimResult.session_id;
|
|
693
699
|
}
|
|
694
700
|
|
|
701
|
+
// Mint the merge-lock holder identity once: task+session of this run's
|
|
702
|
+
// first claim. The task ID alone cannot distinguish two runs of the same
|
|
703
|
+
// task (re-dispatch after park), and a PID is not a valid holder at all.
|
|
704
|
+
if (lockHolder === taskId && activeSessionId) {
|
|
705
|
+
lockHolder = taskId + "/" + activeSessionId;
|
|
706
|
+
}
|
|
707
|
+
|
|
695
708
|
// ── Capture: baseline evidence for experiential tasks ─────────────
|
|
696
709
|
// Hazel's QA capture pass runs right after Triage, before Map, for tasks
|
|
697
710
|
// Sage flagged experiential. The capture itself is parent-driven (the
|
|
@@ -860,11 +873,11 @@ while (i < STEPS.length) {
|
|
|
860
873
|
|
|
861
874
|
} else if (step.name === "Integrate") {
|
|
862
875
|
instructions = "Merge the approved task branch into main.\n\n" +
|
|
863
|
-
"Run: "+ LIFECYCLE_ENV + " integrate " + taskId + " \"merge: " + safeTitle + "\"\n\n" +
|
|
876
|
+
"Run: "+ LIFECYCLE_ENV + "WORKFLOW_RUN_ID=" + lockHolder + " integrate " + taskId + " \"merge: " + safeTitle + "\"\n\n" +
|
|
864
877
|
"Read the output:\n" +
|
|
865
878
|
"- If it contains MERGED, integration succeeded. Report the merged commit hash.\n" +
|
|
866
879
|
"- If it contains MERGED_EMPTY, the branch had no commits ahead of main (a runtime-state deliverable, declared by Build as repo_diff: none). Integration succeeded vacuously: the merge lock was NOT taken and there is no new commit. Report 'merged empty: no repo changes — deliverable was runtime state', then end your report with exactly this line: VERDICT: PASS. SKIP STEP 2 (push): there is no new commit to push.\n" +
|
|
867
|
-
"- If it contains LOCK_HELD, another task holds the merge lock (mid Integrate/Publish). Report 'merge lock held', then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
880
|
+
"- If it contains LOCK_HELD, another task holds the merge lock (mid Integrate/Publish) and the 10-minute bounded backoff is exhausted. Report 'merge lock held after bounded backoff', then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
868
881
|
"- If it contains CONFLICT, the plain merge failed — the merge was aborted, main is clean, and your task still holds the merge lock. Do NOT fail yet. Resolve it:\n" +
|
|
869
882
|
"RESOLUTION:\n" +
|
|
870
883
|
"R1. Refresh the merge lock FIRST (a long resolution must not silently lose the lock to the orphan sweep): "+ LIFECYCLE_ENV + " refresh-lock " + taskId + ". Create a scratch worktree WITH A NEW BRANCH (main is already checked out in the repo checkout, so git forbids checking it out a second time): cd " + REPO_PATH + " && git worktree add -b resolve/" + taskId + " /tmp/crew-resolve-" + taskId + " main. Reproduce the conflict in the scratch worktree: cd /tmp/crew-resolve-" + taskId + " && git merge " + TASK_BRANCH + ". This reproduces the exact conflict (main has not moved — the lock was held throughout). The task branch " + TASK_BRANCH + " is never modified.\n" +
|
|
@@ -893,13 +906,17 @@ while (i < STEPS.length) {
|
|
|
893
906
|
instructions = "Publish the npm package by running exactly ONE command — the deterministic publish script.\n" +
|
|
894
907
|
"The release decision is already made and recorded: release: yes, version_bump: " + publishTarget.scope +
|
|
895
908
|
", target version " + publishTarget.target + " (computed as " + publishTarget.base + " + " + publishTarget.scope +
|
|
896
|
-
" → " + publishTarget.target + " by the workflow, not by you). There is no decision to make
|
|
909
|
+
" → " + publishTarget.target + " by the workflow, not by you). There is no decision to make; the script itself skips gracefully when no merge lock is held.\n\n" +
|
|
897
910
|
"Run exactly this command and no other publish-related commands:\n" +
|
|
898
911
|
"TASK_ID=" + taskId + " REPO_PATH=" + REPO_PATH + " PKG=muse-crew TARGET_VERSION=" + publishTarget.target +
|
|
899
912
|
" CREW_HOME=" + crewHome + " LIFECYCLE=" + LIFECYCLE + " RELEASE_SCRIPT=" + RELEASE_SCRIPT +
|
|
900
913
|
" bash " + PUBLISH_NPM + "\n\n" +
|
|
901
914
|
"Do NOT run npm, npm pack, npm publish, or the python publish script yourself. Do NOT compare local and registry versions. Do NOT decide whether to publish.\n\n" +
|
|
902
915
|
"If the command exits nonzero, put the script's PUBLISH_FAILED line in your report, then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
916
|
+
"If the script's output contains PUBLISH_SKIPPED=no-lock-held, the publish was skipped gracefully: Integrate reported MERGED_EMPTY (no commits ahead of main), so no merge lock was taken and there is nothing to ship. Paste the marker block verbatim into your report, then end your report with exactly these three lines, in this order — lowercase, no trailing period, do not rephrase:\n" +
|
|
917
|
+
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
918
|
+
"skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)\n" +
|
|
919
|
+
"VERDICT: PASS\n\n" +
|
|
903
920
|
"If it exits zero, paste the script's COMPLETE marker block verbatim into your report, then end your report with exactly these three lines, in this order — lowercase, no trailing period, do not rephrase:\n" +
|
|
904
921
|
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
905
922
|
"published: muse-crew@" + publishTarget.target + "\n" +
|
|
@@ -918,20 +935,34 @@ while (i < STEPS.length) {
|
|
|
918
935
|
// getprovenance-vs-HEAD verification below stays as the final gate.
|
|
919
936
|
var artifactPublish = null;
|
|
920
937
|
var publishLockRefreshed = false;
|
|
938
|
+
var publishSkippedNoLock = false;
|
|
921
939
|
try {
|
|
922
|
-
// STEP 0 (mechanical):
|
|
923
|
-
//
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
940
|
+
// STEP 0 (mechanical): read the merge-lock state explicitly — never
|
|
941
|
+
// infer it from prose. An empty-diff Integrate (MERGED_EMPTY)
|
|
942
|
+
// intentionally takes no lock: nothing merged, nothing to ship. When
|
|
943
|
+
// no lock is held, Publish skips the publish path gracefully instead
|
|
944
|
+
// of failing closed on refresh-lock (park 2026-09-11).
|
|
945
|
+
var lockState = await agent(
|
|
946
|
+
"Run: "+ LIFECYCLE_ENV + " lock-status " + taskId + "\n" +
|
|
947
|
+
"If the output is exactly UNLOCKED, return JSON { \"state\": \"UNLOCKED\" } and run nothing else.\n" +
|
|
948
|
+
"Otherwise the lock is held: run "+ LIFECYCLE_ENV + " refresh-lock " + taskId + " (a long build must not silently lose the lock to the orphan sweep), then return JSON { \"state\": \"LOCKED\", \"refreshed\": <true if the refresh exited zero, false otherwise>, \"output\": \"<trimmed refresh stdout>\" } and nothing else.",
|
|
949
|
+
{ key: attemptKey("publish-lock-refresh-" + taskId, totalReworkCount), label: "Reading merge lock state for publish",
|
|
950
|
+
schema: { type: "object", properties: { state: { type: "string" }, refreshed: { type: "boolean" }, output: { type: "string" } }, required: ["state"] } }
|
|
929
951
|
);
|
|
930
|
-
if (
|
|
931
|
-
|
|
952
|
+
if ((lockState.state || "").trim() === "UNLOCKED") {
|
|
953
|
+
publishSkippedNoLock = true;
|
|
954
|
+
log("Publish skipped for task " + taskId + ": no merge lock held (empty-diff Integrate) — nothing to ship");
|
|
955
|
+
} else {
|
|
956
|
+
if (!lockState.refreshed) {
|
|
957
|
+
return await parkTask("Publish lock refresh failed: " + (lockState.output || "no output") + ". Fail-closed — lock state unknown.");
|
|
958
|
+
}
|
|
959
|
+
publishLockRefreshed = true;
|
|
932
960
|
}
|
|
933
|
-
|
|
934
|
-
//
|
|
961
|
+
// STEP 1 (mechanical): trigger the rebuild with one narrow call.
|
|
962
|
+
// Skipped entirely when no lock was held — nothing merged, nothing
|
|
963
|
+
// to ship.
|
|
964
|
+
if (!publishSkippedNoLock) {
|
|
965
|
+
// (below) the rebuild trigger, bounded poll, and provenance stamp
|
|
935
966
|
// agent only makes the artifact_edit call and reports whether it was
|
|
936
967
|
// accepted — no prose claim to trust. If the artifact tool namespace
|
|
937
968
|
// is missing from this child it reports honestly and the workflow
|
|
@@ -952,14 +983,46 @@ while (i < STEPS.length) {
|
|
|
952
983
|
}
|
|
953
984
|
var publishFailure = null;
|
|
954
985
|
if (rebuildTrigger.edit_started) {
|
|
955
|
-
// STEP 1b (mechanical): bounded poll for build completion
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
986
|
+
// STEP 1b (mechanical): bounded poll for build completion, chunked so
|
|
987
|
+
// the merge-lock lease is refreshed before it can expire. The 600s
|
|
988
|
+
// lease is shorter than the worst-case 10-minute build poll, so the
|
|
989
|
+
// poll runs in three chunks (7 checks x 30s ~= 3.5 min each) with a
|
|
990
|
+
// holder-only lease refresh between chunks. If a refresh fails, the
|
|
991
|
+
// lock was lost: stop the run and park the task — never continue to
|
|
992
|
+
// a provenance stamp or version assignment without holding the lock.
|
|
993
|
+
var buildPoll = null;
|
|
994
|
+
for (var chunk = 1; chunk <= 3; chunk++) {
|
|
995
|
+
if (chunk > 1) {
|
|
996
|
+
var refreshPoll = await agent(
|
|
997
|
+
"Refresh the merge lock for task " + taskId + ".\n" +
|
|
998
|
+
"Run: " + LIFECYCLE_ENV + " refresh-lock " + taskId + "\n" +
|
|
999
|
+
"If the output contains REFRESHED, return JSON { \"held\": true } and nothing else. Otherwise return JSON { \"held\": false, \"output\": \"<verbatim output>\" } and nothing else.",
|
|
1000
|
+
{ key: attemptKey("publish-lock-refresh-poll-" + taskId + "-c" + chunk, totalReworkCount), label: "Refreshing merge lock during build poll",
|
|
1001
|
+
schema: { type: "object", properties: { held: { type: "boolean" }, output: { type: "string" } }, required: ["held"] },
|
|
1002
|
+
timeoutMs: 60000 }
|
|
1003
|
+
);
|
|
1004
|
+
if (!refreshPoll || refreshPoll.held !== true) {
|
|
1005
|
+
return await parkTask("Merge-lock lease lost during the artifact build poll (refresh before chunk " + chunk + " of 3 failed: " + ((refreshPoll && refreshPoll.output) || "no output") + "). Fail-closed: stopping before any provenance stamp or version assignment.");
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
// Chunk 1 keeps the original stable key; later chunks use -c<N>
|
|
1009
|
+
// suffixed keys. All are attemptKey-scoped so rework passes stay
|
|
1010
|
+
// disjoint (replay-key contract).
|
|
1011
|
+
var pollKey = (chunk === 1)
|
|
1012
|
+
? attemptKey("publish-artifact-poll-" + taskId, totalReworkCount)
|
|
1013
|
+
: attemptKey("publish-artifact-poll-" + taskId + "-c" + chunk, totalReworkCount);
|
|
1014
|
+
buildPoll = await agent(
|
|
1015
|
+
"Poll artifact_status for slug \"" + PUBLISH_SLUG + "\" until no build is running. Check every 30 seconds, up to 7 checks (3.5 minutes max).\n" +
|
|
1016
|
+
"Return JSON { \"build_done\": <true if no build is running within budget, false on timeout>, \"status\": \"<final status or timeout note>\" } and nothing else.",
|
|
1017
|
+
{ key: pollKey, label: "Waiting for artifact build to complete (chunk " + chunk + " of 3)",
|
|
1018
|
+
schema: { type: "object", properties: { build_done: { type: "boolean" }, status: { type: "string" } }, required: ["build_done"] },
|
|
1019
|
+
timeoutMs: 270000 }
|
|
1020
|
+
);
|
|
1021
|
+
if (buildPoll && buildPoll.build_done) { break; }
|
|
1022
|
+
}
|
|
1023
|
+
if (!buildPoll || !buildPoll.build_done) {
|
|
1024
|
+
buildPoll = { build_done: false, status: (buildPoll && buildPoll.status) || "build still running after the 10.5-minute bounded poll" };
|
|
1025
|
+
}
|
|
963
1026
|
if (buildPoll.build_done) {
|
|
964
1027
|
// STEP 1c (mechanical): stamp provenance from workflow-computed values.
|
|
965
1028
|
var provStamp = await agent(
|
|
@@ -983,25 +1046,34 @@ while (i < STEPS.length) {
|
|
|
983
1046
|
} else {
|
|
984
1047
|
publishFailure = "Artifact rebuild trigger failed: " + (rebuildTrigger.error || "artifact_edit not accepted") + ". The publish did not land.";
|
|
985
1048
|
}
|
|
986
|
-
//
|
|
1049
|
+
} // end: publishSkippedNoLock — no rebuild, no stamp, nothing to ship
|
|
1050
|
+
// STEP 2 (mechanical, always — skip path included): post-deploy
|
|
987
1051
|
// commits builder leftovers if any, removes the worktree, and releases
|
|
988
|
-
// the merge lock — even when the publish
|
|
989
|
-
// or failed publish can never leave the
|
|
1052
|
+
// the merge lock (forgiving when none is held) — even when the publish
|
|
1053
|
+
// itself failed, so a skipped or failed publish can never leave the
|
|
1054
|
+
// lock held (canary b5efd1b1).
|
|
990
1055
|
var postDeploy = await agent(
|
|
991
1056
|
"Run: "+ LIFECYCLE_ENV + " post-deploy " + taskId + "\n" +
|
|
992
1057
|
"Return JSON { \"deployed\": <true if the output contains DEPLOYED, false otherwise>, \"output\": \"<trimmed output>\" } and nothing else.",
|
|
993
1058
|
{ key: attemptKey("publish-postdeploy-" + taskId, totalReworkCount), label: "Finalizing publish (post-deploy)",
|
|
994
1059
|
schema: { type: "object", properties: { deployed: { type: "boolean" }, output: { type: "string" } }, required: ["deployed"] } }
|
|
995
1060
|
);
|
|
996
|
-
if (
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1061
|
+
if (publishSkippedNoLock) {
|
|
1062
|
+
if (!postDeploy.deployed) {
|
|
1063
|
+
return await parkTask("Post-deploy failed after a skipped publish: " + (postDeploy.output || "no output") + ". Nothing was published; worktree cleanup and lock state unknown — human attention needed.");
|
|
1064
|
+
}
|
|
1065
|
+
log("Publish skipped cleanly for task " + taskId + " (no lock held) — post-deploy finalized cleanup");
|
|
1066
|
+
} else {
|
|
1067
|
+
if (publishFailure) {
|
|
1068
|
+
return await parkTask(publishFailure + (postDeploy.deployed
|
|
1069
|
+
? " Post-deploy finalized cleanup."
|
|
1070
|
+
: " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
|
|
1071
|
+
}
|
|
1072
|
+
if (!postDeploy.deployed) {
|
|
1073
|
+
return await parkTask("Post-deploy failed after a stamped publish: " + (postDeploy.output || "no output") + ". The publish landed but worktree cleanup and lock release are unknown — human attention needed.");
|
|
1074
|
+
}
|
|
1075
|
+
log("Deterministic artifact publish completed for task " + taskId + ": provenance at " + artifactPublish.source_commit);
|
|
1003
1076
|
}
|
|
1004
|
-
log("Deterministic artifact publish completed for task " + taskId + ": provenance at " + artifactPublish.source_commit);
|
|
1005
1077
|
} catch (pubErr) {
|
|
1006
1078
|
// Best-effort cleanup: if the lock was refreshed, try to release it
|
|
1007
1079
|
// before parking so the failure cannot wedge the next run.
|
|
@@ -1022,6 +1094,12 @@ while (i < STEPS.length) {
|
|
|
1022
1094
|
// The work agent no longer performs the publish — it reports on the
|
|
1023
1095
|
// mechanical outcome above. It must not rebuild or re-stamp: a second
|
|
1024
1096
|
// artifact_edit would trigger a duplicate build.
|
|
1097
|
+
if (publishSkippedNoLock) {
|
|
1098
|
+
instructions = "Publish was skipped deterministically by the workflow before your step — do NOT call artifact_edit, artifact_status, setprovenance, or post-deploy yourself; doing so would disturb the finalized state.\n\n" +
|
|
1099
|
+
"Integrate reported MERGED_EMPTY (the task branch had no commits ahead of main), so no merge lock was taken and there is nothing to ship. The workflow finalized cleanup via post-deploy.\n\n" +
|
|
1100
|
+
"Write plain prose describing the skip, then on its own line: VERDICT: PASS\n" +
|
|
1101
|
+
"The VERDICT line must be the last line of your report.";
|
|
1102
|
+
} else {
|
|
1025
1103
|
instructions = "Publish the merged code to the live artifact.\n\n" +
|
|
1026
1104
|
"The publish was performed deterministically by the workflow before your step — do NOT call artifact_edit, artifact_status, setprovenance, or post-deploy yourself; doing so would trigger a duplicate build or disturb the finalized state. You perform no publish actions.\n\n" +
|
|
1027
1105
|
"For the change summary, run: cd " + REPO_PATH + " && git log -1 --stat\n\n" +
|
|
@@ -1034,6 +1112,7 @@ while (i < STEPS.length) {
|
|
|
1034
1112
|
"The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
|
|
1035
1113
|
"Write plain prose describing what was published, then on its own line: VERDICT: PASS\n" +
|
|
1036
1114
|
"The VERDICT line must be the last line of your report.";
|
|
1115
|
+
}
|
|
1037
1116
|
} else if (PUBLISH_TYPE === "vercel") {
|
|
1038
1117
|
// vercel publish is not yet implemented — block without inventing behavior
|
|
1039
1118
|
instructions = "The project's publish target is \"vercel\", which is not yet implemented.\n" +
|
|
@@ -1045,7 +1124,9 @@ while (i < STEPS.length) {
|
|
|
1045
1124
|
// declared release: yes, QA verifies the registry actually moved. A silent
|
|
1046
1125
|
// publish skip becomes a loud QA failure with evidence, not a pass.
|
|
1047
1126
|
var npmPublishCheck = (PUBLISH_TYPE === "npm" && releaseDecision && releaseDecision.release === "yes")
|
|
1048
|
-
? "NPM PUBLISH CHECK: the accepted Build report declared release: yes, so this run's Publish phase must have published. Find this task's recorded Publish result: call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getstate\", args: { \"events_limit\": 1 }, then find the session for this task_id with step \"Publish\" (status completed) in the returned sessions array and read its session notes (the Publish agent's summary — event history does NOT carry it).\n" +
|
|
1127
|
+
? "NPM PUBLISH CHECK: the accepted Build report declared release: yes, so this run's Publish phase must have published — UNLESS it was skipped deterministically on an empty-diff Integrate. Find this task's recorded Publish result: call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getstate\", args: { \"events_limit\": 1 }, then find the session for this task_id with step \"Publish\" (status completed) in the returned sessions array and read its session notes (the Publish agent's summary — event history does NOT carry it).\n" +
|
|
1128
|
+
"CHECK THE SKIP PATH FIRST: if the notes contain a line matching skipped: no-lock-held, Publish was skipped deterministically — Integrate reported MERGED_EMPTY (no commits ahead of main), so no merge lock was taken and there was nothing to ship. Verify the notes contain that skip-marker line and do NOT contain a line matching published: muse-crew@. Do NOT run npm view and do NOT demand registry movement — nothing was supposed to ship. Report 'npm publish check: Publish skipped deterministically (empty-diff Integrate — nothing to ship)' and PASS this check.\n" +
|
|
1129
|
+
"Only when the notes contain no skip marker must the publish have landed — run the full verification below.\n" +
|
|
1049
1130
|
"Extract the line matching TARGET_VERSION=<new-version> computed as <base> + <scope> → <new-version> (the workflow appends it to the Publish notes, so it is always present). If the line is missing, report 'npm publish verification failed: Publish notes did not carry the computed target version', then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
1050
1131
|
"Verify the bump SCOPE: the <scope> in that line MUST equal the accepted version_bump scope \"" + releaseDecision.version_bump + "\" — if it differs, report the mismatch, then end your report with exactly this line: VERDICT: FAIL. Verify the ARITHMETIC: <base> + <scope> must equal <new-version> (patch increments the last segment only, e.g. 0.3.0 + patch → 0.3.1; minor increments the middle and resets the last to 0; major increments the first and resets the rest to 0) — if the math is wrong, report it, then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
1051
1132
|
"Extract the published version from the notes line matching published: muse-crew@<version>. It MUST equal <new-version> from the TARGET_VERSION line. Then run: npm view muse-crew version. The registry version MUST equal <new-version>. If any of these checks fails, report 'npm publish verification failed: [details]', then end your report with exactly this line: VERDICT: FAIL.\n"
|
|
@@ -1260,8 +1341,21 @@ while (i < STEPS.length) {
|
|
|
1260
1341
|
const status = passed ? "completed" : (step.name === "Integrate" || step.name === "Publish" ? "failed" : "rejected");
|
|
1261
1342
|
|
|
1262
1343
|
// Deterministic publish verification: the agent cannot self-certify a publish.
|
|
1344
|
+
// Skip-aware (park 2026-09-11): when the deterministic publish script found
|
|
1345
|
+
// no merge lock held (empty-diff Integrate), it skips the publish path
|
|
1346
|
+
// gracefully and emits the machine-readable PUBLISH_SKIPPED=no-lock-held
|
|
1347
|
+
// marker. Verification is then vacuous — nothing was shipped, and the
|
|
1348
|
+
// registry must NOT have moved. The marker is script-emitted explicit state
|
|
1349
|
+
// (pasted verbatim per the Publish agent instructions), not agent prose; a
|
|
1350
|
+
// report without the marker still runs the full verification fail-closed.
|
|
1263
1351
|
var publishVerified = false;
|
|
1352
|
+
var npmPublishSkipped = false;
|
|
1264
1353
|
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget) {
|
|
1354
|
+
if (/^PUBLISH_SKIPPED=no-lock-held$/m.test(workerText || "")) {
|
|
1355
|
+
npmPublishSkipped = true;
|
|
1356
|
+
log("Publish skipped for task " + taskId + " (no merge lock held — empty-diff Integrate): publish verification vacuous, nothing was shipped");
|
|
1357
|
+
}
|
|
1358
|
+
if (!npmPublishSkipped) {
|
|
1265
1359
|
try {
|
|
1266
1360
|
var verifyResult = await agent(
|
|
1267
1361
|
"Run: npm view muse-crew version 2>/dev/null. Return JSON { \"registry_version\": \"<output trimmed>\" } and nothing else.",
|
|
@@ -1278,6 +1372,7 @@ while (i < STEPS.length) {
|
|
|
1278
1372
|
} catch (e) {
|
|
1279
1373
|
return await parkTask("Publish verification failed: could not read the registry version (" + (e && e.message ? e.message : e) + "). Fail-closed.");
|
|
1280
1374
|
}
|
|
1375
|
+
} // end: !npmPublishSkipped — a skipped publish has nothing to verify
|
|
1281
1376
|
}
|
|
1282
1377
|
|
|
1283
1378
|
// Artifact publish verification: the worker cannot self-certify a deploy.
|
|
@@ -1285,6 +1380,14 @@ while (i < STEPS.length) {
|
|
|
1285
1380
|
// integrated commit. A stale or missing provenance means the publish did not
|
|
1286
1381
|
// land — fail closed, do not trust the worker's prose.
|
|
1287
1382
|
if (step.name === "Publish" && PUBLISH_TYPE === "artifact" && PUBLISH_SLUG) {
|
|
1383
|
+
// Skip-aware (park 2026-09-11): an empty-diff Integrate takes no merge
|
|
1384
|
+
// lock, and the deterministic publish path skips rebuild/stamp entirely —
|
|
1385
|
+
// there is no new provenance to compare against HEAD, so verification is
|
|
1386
|
+
// vacuous. publishSkippedNoLock is workflow-computed state from the
|
|
1387
|
+
// explicit lock-status read in STEP 0, not agent prose.
|
|
1388
|
+
if (publishSkippedNoLock) {
|
|
1389
|
+
log("Publish skipped for task " + taskId + " (no merge lock held — empty-diff Integrate): artifact verification vacuous, nothing was shipped");
|
|
1390
|
+
} else {
|
|
1288
1391
|
try {
|
|
1289
1392
|
var headResult = await agent(
|
|
1290
1393
|
"Run: cd " + REPO_PATH + " && git rev-parse HEAD. Return JSON { \"head\": \"<output trimmed>\" } and nothing else.",
|
|
@@ -1308,6 +1411,7 @@ while (i < STEPS.length) {
|
|
|
1308
1411
|
} catch (e) {
|
|
1309
1412
|
return await parkTask("Publish verification failed: could not read artifact provenance (" + (e && e.message ? e.message : e) + "). Fail-closed.");
|
|
1310
1413
|
}
|
|
1414
|
+
} // end: !publishSkippedNoLock — a skipped publish has nothing to verify
|
|
1311
1415
|
}
|
|
1312
1416
|
|
|
1313
1417
|
// Session notes. Machine-readable marker lines are extracted from the full
|
|
@@ -1316,9 +1420,13 @@ while (i < STEPS.length) {
|
|
|
1316
1420
|
// reading TARGET_VERSION=) depend on them.
|
|
1317
1421
|
let summary;
|
|
1318
1422
|
var workerMarkers = extractMarkerLines(workerText);
|
|
1319
|
-
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget && publishVerified) {
|
|
1423
|
+
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget && (publishVerified || npmPublishSkipped)) {
|
|
1424
|
+
// The skip path records the computed target and the explicit skip — never
|
|
1425
|
+
// "published:", which would be a false claim (nothing was shipped).
|
|
1320
1426
|
const markerLines = "TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
1321
|
-
|
|
1427
|
+
(npmPublishSkipped
|
|
1428
|
+
? "skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)"
|
|
1429
|
+
: "published: muse-crew@" + publishTarget.target);
|
|
1322
1430
|
summary = (stepResult.summary || "Step completed").slice(0, 2000 - markerLines.length - workerMarkers.length - 2) + "\n" + markerLines + (workerMarkers ? "\n" + workerMarkers : "");
|
|
1323
1431
|
} else {
|
|
1324
1432
|
summary = (stepResult.summary || "Step completed").slice(0, 2000 - workerMarkers.length - 1) + (workerMarkers ? "\n" + workerMarkers : "");
|