muse-crew 0.4.6 → 0.5.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.
@@ -916,13 +916,17 @@ while (i < STEPS.length) {
916
916
  instructions = "Publish the npm package by running exactly ONE command — the deterministic publish script.\n" +
917
917
  "The release decision is already made and recorded: release: yes, version_bump: " + publishTarget.scope +
918
918
  ", target version " + publishTarget.target + " (computed as " + publishTarget.base + " + " + publishTarget.scope +
919
- " → " + publishTarget.target + " by the workflow, not by you). There is no decision to make and no skip path.\n\n" +
919
+ " → " + 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" +
920
920
  "Run exactly this command and no other publish-related commands:\n" +
921
921
  "TASK_ID=" + taskId + " REPO_PATH=" + REPO_PATH + " PKG=muse-crew TARGET_VERSION=" + publishTarget.target +
922
922
  " CREW_HOME=" + crewHome + " LIFECYCLE=" + LIFECYCLE + " RELEASE_SCRIPT=" + RELEASE_SCRIPT +
923
923
  " bash " + PUBLISH_NPM + "\n\n" +
924
924
  "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" +
925
925
  "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" +
926
+ "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" +
927
+ "TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
928
+ "skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)\n" +
929
+ "VERDICT: PASS\n\n" +
926
930
  "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" +
927
931
  "TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
928
932
  "published: muse-crew@" + publishTarget.target + "\n" +
@@ -941,20 +945,34 @@ while (i < STEPS.length) {
941
945
  // getprovenance-vs-HEAD verification below stays as the final gate.
942
946
  var artifactPublish = null;
943
947
  var publishLockRefreshed = false;
948
+ var publishSkippedNoLock = false;
944
949
  try {
945
- // STEP 0 (mechanical): refresh the merge lock so a long build cannot
946
- // go stale mid-publish.
947
- var lockRefresh = await agent(
948
- "Run: "+ LIFECYCLE_ENV + " refresh-lock " + taskId + "\n" +
949
- "Return JSON { \"refreshed\": <true if the command exited zero, false otherwise>, \"output\": \"<trimmed stdout>\" } and nothing else.",
950
- { key: attemptKey("publish-lock-refresh-" + taskId, totalReworkCount), label: "Refreshing merge lock for publish",
951
- schema: { type: "object", properties: { refreshed: { type: "boolean" }, output: { type: "string" } }, required: ["refreshed"] } }
950
+ // STEP 0 (mechanical): read the merge-lock state explicitly never
951
+ // infer it from prose. An empty-diff Integrate (MERGED_EMPTY)
952
+ // intentionally takes no lock: nothing merged, nothing to ship. When
953
+ // no lock is held, Publish skips the publish path gracefully instead
954
+ // of failing closed on refresh-lock (park 2026-09-11).
955
+ var lockState = await agent(
956
+ "Run: "+ LIFECYCLE_ENV + " lock-status " + taskId + "\n" +
957
+ "If the output is exactly UNLOCKED, return JSON { \"state\": \"UNLOCKED\" } and run nothing else.\n" +
958
+ "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.",
959
+ { key: attemptKey("publish-lock-refresh-" + taskId, totalReworkCount), label: "Reading merge lock state for publish",
960
+ schema: { type: "object", properties: { state: { type: "string" }, refreshed: { type: "boolean" }, output: { type: "string" } }, required: ["state"] } }
952
961
  );
953
- if (!lockRefresh.refreshed) {
954
- return await parkTask("Publish lock refresh failed: " + (lockRefresh.output || "no output") + ". Fail-closed — lock state unknown.");
962
+ if ((lockState.state || "").trim() === "UNLOCKED") {
963
+ publishSkippedNoLock = true;
964
+ log("Publish skipped for task " + taskId + ": no merge lock held (empty-diff Integrate) — nothing to ship");
965
+ } else {
966
+ if (!lockState.refreshed) {
967
+ return await parkTask("Publish lock refresh failed: " + (lockState.output || "no output") + ". Fail-closed — lock state unknown.");
968
+ }
969
+ publishLockRefreshed = true;
955
970
  }
956
- publishLockRefreshed = true;
957
- // STEP 1 (mechanical): trigger the rebuild with one narrow call. The
971
+ // STEP 1 (mechanical): trigger the rebuild with one narrow call.
972
+ // Skipped entirely when no lock was held nothing merged, nothing
973
+ // to ship.
974
+ if (!publishSkippedNoLock) {
975
+ // (below) the rebuild trigger, bounded poll, and provenance stamp
958
976
  // agent only makes the artifact_edit call and reports whether it was
959
977
  // accepted — no prose claim to trust. If the artifact tool namespace
960
978
  // is missing from this child it reports honestly and the workflow
@@ -1038,25 +1056,34 @@ while (i < STEPS.length) {
1038
1056
  } else {
1039
1057
  publishFailure = "Artifact rebuild trigger failed: " + (rebuildTrigger.error || "artifact_edit not accepted") + ". The publish did not land.";
1040
1058
  }
1041
- // STEP 2 (mechanical, always once the lock was refreshed): post-deploy
1059
+ } // end: publishSkippedNoLock no rebuild, no stamp, nothing to ship
1060
+ // STEP 2 (mechanical, always — skip path included): post-deploy
1042
1061
  // commits builder leftovers if any, removes the worktree, and releases
1043
- // the merge lock — even when the publish itself failed, so a skipped
1044
- // or failed publish can never leave the lock held (canary b5efd1b1).
1062
+ // the merge lock (forgiving when none is held) — even when the publish
1063
+ // itself failed, so a skipped or failed publish can never leave the
1064
+ // lock held (canary b5efd1b1).
1045
1065
  var postDeploy = await agent(
1046
1066
  "Run: "+ LIFECYCLE_ENV + " post-deploy " + taskId + "\n" +
1047
1067
  "Return JSON { \"deployed\": <true if the output contains DEPLOYED, false otherwise>, \"output\": \"<trimmed output>\" } and nothing else.",
1048
1068
  { key: attemptKey("publish-postdeploy-" + taskId, totalReworkCount), label: "Finalizing publish (post-deploy)",
1049
1069
  schema: { type: "object", properties: { deployed: { type: "boolean" }, output: { type: "string" } }, required: ["deployed"] } }
1050
1070
  );
1051
- if (publishFailure) {
1052
- return await parkTask(publishFailure + (postDeploy.deployed
1053
- ? " Post-deploy finalized cleanup."
1054
- : " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
1055
- }
1056
- if (!postDeploy.deployed) {
1057
- 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.");
1071
+ if (publishSkippedNoLock) {
1072
+ if (!postDeploy.deployed) {
1073
+ 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.");
1074
+ }
1075
+ log("Publish skipped cleanly for task " + taskId + " (no lock held) — post-deploy finalized cleanup");
1076
+ } else {
1077
+ if (publishFailure) {
1078
+ return await parkTask(publishFailure + (postDeploy.deployed
1079
+ ? " Post-deploy finalized cleanup."
1080
+ : " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
1081
+ }
1082
+ if (!postDeploy.deployed) {
1083
+ 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.");
1084
+ }
1085
+ log("Deterministic artifact publish completed for task " + taskId + ": provenance at " + artifactPublish.source_commit);
1058
1086
  }
1059
- log("Deterministic artifact publish completed for task " + taskId + ": provenance at " + artifactPublish.source_commit);
1060
1087
  } catch (pubErr) {
1061
1088
  // Best-effort cleanup: if the lock was refreshed, try to release it
1062
1089
  // before parking so the failure cannot wedge the next run.
@@ -1077,6 +1104,12 @@ while (i < STEPS.length) {
1077
1104
  // The work agent no longer performs the publish — it reports on the
1078
1105
  // mechanical outcome above. It must not rebuild or re-stamp: a second
1079
1106
  // artifact_edit would trigger a duplicate build.
1107
+ if (publishSkippedNoLock) {
1108
+ 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" +
1109
+ "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" +
1110
+ "Write plain prose describing the skip, then on its own line: VERDICT: PASS\n" +
1111
+ "The VERDICT line must be the last line of your report.";
1112
+ } else {
1080
1113
  instructions = "Publish the merged code to the live artifact.\n\n" +
1081
1114
  "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" +
1082
1115
  "For the change summary, run: cd " + REPO_PATH + " && git log -1 --stat\n\n" +
@@ -1089,6 +1122,7 @@ while (i < STEPS.length) {
1089
1122
  "The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
1090
1123
  "Write plain prose describing what was published, then on its own line: VERDICT: PASS\n" +
1091
1124
  "The VERDICT line must be the last line of your report.";
1125
+ }
1092
1126
  } else if (PUBLISH_TYPE === "vercel") {
1093
1127
  // vercel publish is not yet implemented — block without inventing behavior
1094
1128
  instructions = "The project's publish target is \"vercel\", which is not yet implemented.\n" +
@@ -1100,7 +1134,9 @@ while (i < STEPS.length) {
1100
1134
  // declared release: yes, QA verifies the registry actually moved. A silent
1101
1135
  // publish skip becomes a loud QA failure with evidence, not a pass.
1102
1136
  var npmPublishCheck = (PUBLISH_TYPE === "npm" && releaseDecision && releaseDecision.release === "yes")
1103
- ? "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" +
1137
+ ? "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" +
1138
+ "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" +
1139
+ "Only when the notes contain no skip marker must the publish have landed — run the full verification below.\n" +
1104
1140
  "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" +
1105
1141
  "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" +
1106
1142
  "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"
@@ -1295,8 +1331,21 @@ while (i < STEPS.length) {
1295
1331
  const status = passed ? "completed" : (step.name === "Integrate" || step.name === "Publish" ? "failed" : "rejected");
1296
1332
 
1297
1333
  // Deterministic publish verification: the agent cannot self-certify a publish.
1334
+ // Skip-aware (park 2026-09-11): when the deterministic publish script found
1335
+ // no merge lock held (empty-diff Integrate), it skips the publish path
1336
+ // gracefully and emits the machine-readable PUBLISH_SKIPPED=no-lock-held
1337
+ // marker. Verification is then vacuous — nothing was shipped, and the
1338
+ // registry must NOT have moved. The marker is script-emitted explicit state
1339
+ // (pasted verbatim per the Publish agent instructions), not agent prose; a
1340
+ // report without the marker still runs the full verification fail-closed.
1298
1341
  var publishVerified = false;
1342
+ var npmPublishSkipped = false;
1299
1343
  if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget) {
1344
+ if (/^PUBLISH_SKIPPED=no-lock-held$/m.test(workerText || "")) {
1345
+ npmPublishSkipped = true;
1346
+ log("Publish skipped for task " + taskId + " (no merge lock held — empty-diff Integrate): publish verification vacuous, nothing was shipped");
1347
+ }
1348
+ if (!npmPublishSkipped) {
1300
1349
  try {
1301
1350
  var verifyResult = await agent(
1302
1351
  "Run: npm view muse-crew version 2>/dev/null. Return JSON { \"registry_version\": \"<output trimmed>\" } and nothing else.",
@@ -1313,6 +1362,7 @@ while (i < STEPS.length) {
1313
1362
  } catch (e) {
1314
1363
  return await parkTask("Publish verification failed: could not read the registry version (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1315
1364
  }
1365
+ } // end: !npmPublishSkipped — a skipped publish has nothing to verify
1316
1366
  }
1317
1367
 
1318
1368
  // Artifact publish verification: the worker cannot self-certify a deploy.
@@ -1320,6 +1370,14 @@ while (i < STEPS.length) {
1320
1370
  // integrated commit. A stale or missing provenance means the publish did not
1321
1371
  // land — fail closed, do not trust the worker's prose.
1322
1372
  if (step.name === "Publish" && PUBLISH_TYPE === "artifact" && PUBLISH_SLUG) {
1373
+ // Skip-aware (park 2026-09-11): an empty-diff Integrate takes no merge
1374
+ // lock, and the deterministic publish path skips rebuild/stamp entirely —
1375
+ // there is no new provenance to compare against HEAD, so verification is
1376
+ // vacuous. publishSkippedNoLock is workflow-computed state from the
1377
+ // explicit lock-status read in STEP 0, not agent prose.
1378
+ if (publishSkippedNoLock) {
1379
+ log("Publish skipped for task " + taskId + " (no merge lock held — empty-diff Integrate): artifact verification vacuous, nothing was shipped");
1380
+ } else {
1323
1381
  try {
1324
1382
  var headResult = await agent(
1325
1383
  "Run: cd " + REPO_PATH + " && git rev-parse HEAD. Return JSON { \"head\": \"<output trimmed>\" } and nothing else.",
@@ -1343,6 +1401,7 @@ while (i < STEPS.length) {
1343
1401
  } catch (e) {
1344
1402
  return await parkTask("Publish verification failed: could not read artifact provenance (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1345
1403
  }
1404
+ } // end: !publishSkippedNoLock — a skipped publish has nothing to verify
1346
1405
  }
1347
1406
 
1348
1407
  // Session notes. Machine-readable marker lines are extracted from the full
@@ -1351,9 +1410,13 @@ while (i < STEPS.length) {
1351
1410
  // reading TARGET_VERSION=) depend on them.
1352
1411
  let summary;
1353
1412
  var workerMarkers = extractMarkerLines(workerText);
1354
- if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget && publishVerified) {
1413
+ if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget && (publishVerified || npmPublishSkipped)) {
1414
+ // The skip path records the computed target and the explicit skip — never
1415
+ // "published:", which would be a false claim (nothing was shipped).
1355
1416
  const markerLines = "TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
1356
- "published: muse-crew@" + publishTarget.target;
1417
+ (npmPublishSkipped
1418
+ ? "skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)"
1419
+ : "published: muse-crew@" + publishTarget.target);
1357
1420
  summary = (stepResult.summary || "Step completed").slice(0, 2000 - markerLines.length - workerMarkers.length - 2) + "\n" + markerLines + (workerMarkers ? "\n" + workerMarkers : "");
1358
1421
  } else {
1359
1422
  summary = (stepResult.summary || "Step completed").slice(0, 2000 - workerMarkers.length - 1) + (workerMarkers ? "\n" + workerMarkers : "");
@@ -853,13 +853,17 @@ while (i < STEPS.length) {
853
853
  instructions = "Publish the npm package by running exactly ONE command — the deterministic publish script.\n" +
854
854
  "The release decision is already made and recorded: release: yes, version_bump: " + publishTarget.scope +
855
855
  ", target version " + publishTarget.target + " (computed as " + publishTarget.base + " + " + publishTarget.scope +
856
- " → " + publishTarget.target + " by the workflow, not by you). There is no decision to make and no skip path.\n\n" +
856
+ " → " + 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" +
857
857
  "Run exactly this command and no other publish-related commands:\n" +
858
858
  "TASK_ID=" + taskId + " REPO_PATH=" + REPO_PATH + " PKG=muse-crew TARGET_VERSION=" + publishTarget.target +
859
859
  " CREW_HOME=" + crewHome + " LIFECYCLE=" + LIFECYCLE + " RELEASE_SCRIPT=" + RELEASE_SCRIPT +
860
860
  " bash " + PUBLISH_NPM + "\n\n" +
861
861
  "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" +
862
862
  "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" +
863
+ "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" +
864
+ "TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
865
+ "skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)\n" +
866
+ "VERDICT: PASS\n\n" +
863
867
  "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" +
864
868
  "TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
865
869
  "published: muse-crew@" + publishTarget.target + "\n" +
@@ -878,20 +882,34 @@ while (i < STEPS.length) {
878
882
  // getprovenance-vs-HEAD verification below stays as the final gate.
879
883
  var artifactPublish = null;
880
884
  var publishLockRefreshed = false;
885
+ var publishSkippedNoLock = false;
881
886
  try {
882
- // STEP 0 (mechanical): refresh the merge lock so a long build cannot
883
- // go stale mid-publish.
884
- var lockRefresh = await agent(
885
- "Run: "+ LIFECYCLE_ENV + " refresh-lock " + taskId + "\n" +
886
- "Return JSON { \"refreshed\": <true if the command exited zero, false otherwise>, \"output\": \"<trimmed stdout>\" } and nothing else.",
887
- { key: attemptKey("publish-lock-refresh-" + taskId, reworkCount), label: "Refreshing merge lock for publish",
888
- schema: { type: "object", properties: { refreshed: { type: "boolean" }, output: { type: "string" } }, required: ["refreshed"] } }
887
+ // STEP 0 (mechanical): read the merge-lock state explicitly never
888
+ // infer it from prose. An empty-diff Integrate (MERGED_EMPTY)
889
+ // intentionally takes no lock: nothing merged, nothing to ship. When
890
+ // no lock is held, Publish skips the publish path gracefully instead
891
+ // of failing closed on refresh-lock (park 2026-09-11).
892
+ var lockState = await agent(
893
+ "Run: "+ LIFECYCLE_ENV + " lock-status " + taskId + "\n" +
894
+ "If the output is exactly UNLOCKED, return JSON { \"state\": \"UNLOCKED\" } and run nothing else.\n" +
895
+ "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.",
896
+ { key: attemptKey("publish-lock-refresh-" + taskId, reworkCount), label: "Reading merge lock state for publish",
897
+ schema: { type: "object", properties: { state: { type: "string" }, refreshed: { type: "boolean" }, output: { type: "string" } }, required: ["state"] } }
889
898
  );
890
- if (!lockRefresh.refreshed) {
891
- return await parkTask("Publish lock refresh failed: " + (lockRefresh.output || "no output") + ". Fail-closed — lock state unknown.");
899
+ if ((lockState.state || "").trim() === "UNLOCKED") {
900
+ publishSkippedNoLock = true;
901
+ log("Publish skipped for task " + taskId + ": no merge lock held (empty-diff Integrate) — nothing to ship");
902
+ } else {
903
+ if (!lockState.refreshed) {
904
+ return await parkTask("Publish lock refresh failed: " + (lockState.output || "no output") + ". Fail-closed — lock state unknown.");
905
+ }
906
+ publishLockRefreshed = true;
892
907
  }
893
- publishLockRefreshed = true;
894
- // STEP 1 (mechanical): trigger the rebuild with one narrow call. The
908
+ // STEP 1 (mechanical): trigger the rebuild with one narrow call.
909
+ // Skipped entirely when no lock was held nothing merged, nothing
910
+ // to ship.
911
+ if (!publishSkippedNoLock) {
912
+ // (below) the rebuild trigger, bounded poll, and provenance stamp
895
913
  // agent only makes the artifact_edit call and reports whether it was
896
914
  // accepted — no prose claim to trust. If the artifact tool namespace
897
915
  // is missing from this child it reports honestly and the workflow
@@ -975,25 +993,34 @@ while (i < STEPS.length) {
975
993
  } else {
976
994
  publishFailure = "Artifact rebuild trigger failed: " + (rebuildTrigger.error || "artifact_edit not accepted") + ". The publish did not land.";
977
995
  }
978
- // STEP 2 (mechanical, always once the lock was refreshed): post-deploy
996
+ } // end: publishSkippedNoLock no rebuild, no stamp, nothing to ship
997
+ // STEP 2 (mechanical, always — skip path included): post-deploy
979
998
  // commits builder leftovers if any, removes the worktree, and releases
980
- // the merge lock — even when the publish itself failed, so a skipped
981
- // or failed publish can never leave the lock held (canary b5efd1b1).
999
+ // the merge lock (forgiving when none is held) — even when the publish
1000
+ // itself failed, so a skipped or failed publish can never leave the
1001
+ // lock held (canary b5efd1b1).
982
1002
  var postDeploy = await agent(
983
1003
  "Run: "+ LIFECYCLE_ENV + " post-deploy " + taskId + "\n" +
984
1004
  "Return JSON { \"deployed\": <true if the output contains DEPLOYED, false otherwise>, \"output\": \"<trimmed output>\" } and nothing else.",
985
1005
  { key: attemptKey("publish-postdeploy-" + taskId, reworkCount), label: "Finalizing publish (post-deploy)",
986
1006
  schema: { type: "object", properties: { deployed: { type: "boolean" }, output: { type: "string" } }, required: ["deployed"] } }
987
1007
  );
988
- if (publishFailure) {
989
- return await parkTask(publishFailure + (postDeploy.deployed
990
- ? " Post-deploy finalized cleanup."
991
- : " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
992
- }
993
- if (!postDeploy.deployed) {
994
- 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.");
1008
+ if (publishSkippedNoLock) {
1009
+ if (!postDeploy.deployed) {
1010
+ 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.");
1011
+ }
1012
+ log("Publish skipped cleanly for task " + taskId + " (no lock held) — post-deploy finalized cleanup");
1013
+ } else {
1014
+ if (publishFailure) {
1015
+ return await parkTask(publishFailure + (postDeploy.deployed
1016
+ ? " Post-deploy finalized cleanup."
1017
+ : " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
1018
+ }
1019
+ if (!postDeploy.deployed) {
1020
+ 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.");
1021
+ }
1022
+ log("Deterministic artifact publish completed for task " + taskId + ": provenance at " + artifactPublish.source_commit);
995
1023
  }
996
- log("Deterministic artifact publish completed for task " + taskId + ": provenance at " + artifactPublish.source_commit);
997
1024
  } catch (pubErr) {
998
1025
  // Best-effort cleanup: if the lock was refreshed, try to release it
999
1026
  // before parking so the failure cannot wedge the next run.
@@ -1014,6 +1041,12 @@ while (i < STEPS.length) {
1014
1041
  // The work agent no longer performs the publish — it reports on the
1015
1042
  // mechanical outcome above. It must not rebuild or re-stamp: a second
1016
1043
  // artifact_edit would trigger a duplicate build.
1044
+ if (publishSkippedNoLock) {
1045
+ 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" +
1046
+ "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" +
1047
+ "Write plain prose describing the skip, then on its own line: VERDICT: PASS\n" +
1048
+ "The VERDICT line must be the last line of your report.";
1049
+ } else {
1017
1050
  instructions = "Publish the merged code to the live artifact.\n\n" +
1018
1051
  "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" +
1019
1052
  "For the change summary, run: cd " + REPO_PATH + " && git log -1 --stat\n\n" +
@@ -1026,6 +1059,7 @@ while (i < STEPS.length) {
1026
1059
  "The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
1027
1060
  "Write plain prose describing what was published, then on its own line: VERDICT: PASS\n" +
1028
1061
  "The VERDICT line must be the last line of your report.";
1062
+ }
1029
1063
  } else if (PUBLISH_TYPE === "vercel") {
1030
1064
  // vercel publish is not yet implemented — block without inventing behavior
1031
1065
  instructions = "The project's publish target is \"vercel\", which is not yet implemented.\n" +
@@ -1172,8 +1206,21 @@ while (i < STEPS.length) {
1172
1206
  const status = passed ? "completed" : (step.name === "Integrate" || step.name === "Publish" ? "failed" : "rejected");
1173
1207
 
1174
1208
  // Deterministic publish verification: the agent cannot self-certify a publish.
1209
+ // Skip-aware (park 2026-09-11): when the deterministic publish script found
1210
+ // no merge lock held (empty-diff Integrate), it skips the publish path
1211
+ // gracefully and emits the machine-readable PUBLISH_SKIPPED=no-lock-held
1212
+ // marker. Verification is then vacuous — nothing was shipped, and the
1213
+ // registry must NOT have moved. The marker is script-emitted explicit state
1214
+ // (pasted verbatim per the Publish agent instructions), not agent prose; a
1215
+ // report without the marker still runs the full verification fail-closed.
1175
1216
  var publishVerified = false;
1217
+ var npmPublishSkipped = false;
1176
1218
  if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget) {
1219
+ if (/^PUBLISH_SKIPPED=no-lock-held$/m.test(workerText || "")) {
1220
+ npmPublishSkipped = true;
1221
+ log("Publish skipped for task " + taskId + " (no merge lock held — empty-diff Integrate): publish verification vacuous, nothing was shipped");
1222
+ }
1223
+ if (!npmPublishSkipped) {
1177
1224
  try {
1178
1225
  var verifyResult = await agent(
1179
1226
  "Run: npm view muse-crew version 2>/dev/null. Return JSON { \"registry_version\": \"<output trimmed>\" } and nothing else.",
@@ -1190,6 +1237,7 @@ while (i < STEPS.length) {
1190
1237
  } catch (e) {
1191
1238
  return await parkTask("Publish verification failed: could not read the registry version (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1192
1239
  }
1240
+ } // end: !npmPublishSkipped — a skipped publish has nothing to verify
1193
1241
  }
1194
1242
 
1195
1243
  // Artifact publish verification: the worker cannot self-certify a deploy.
@@ -1197,6 +1245,14 @@ while (i < STEPS.length) {
1197
1245
  // integrated commit. A stale or missing provenance means the publish did not
1198
1246
  // land — fail closed, do not trust the worker's prose.
1199
1247
  if (step.name === "Publish" && PUBLISH_TYPE === "artifact" && PUBLISH_SLUG) {
1248
+ // Skip-aware (park 2026-09-11): an empty-diff Integrate takes no merge
1249
+ // lock, and the deterministic publish path skips rebuild/stamp entirely —
1250
+ // there is no new provenance to compare against HEAD, so verification is
1251
+ // vacuous. publishSkippedNoLock is workflow-computed state from the
1252
+ // explicit lock-status read in STEP 0, not agent prose.
1253
+ if (publishSkippedNoLock) {
1254
+ log("Publish skipped for task " + taskId + " (no merge lock held — empty-diff Integrate): artifact verification vacuous, nothing was shipped");
1255
+ } else {
1200
1256
  try {
1201
1257
  var headResult = await agent(
1202
1258
  "Run: cd " + REPO_PATH + " && git rev-parse HEAD. Return JSON { \"head\": \"<output trimmed>\" } and nothing else.",
@@ -1220,6 +1276,7 @@ while (i < STEPS.length) {
1220
1276
  } catch (e) {
1221
1277
  return await parkTask("Publish verification failed: could not read artifact provenance (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1222
1278
  }
1279
+ } // end: !publishSkippedNoLock — a skipped publish has nothing to verify
1223
1280
  }
1224
1281
 
1225
1282
  // Session notes. Machine-readable marker lines are extracted from the full
@@ -1228,9 +1285,13 @@ while (i < STEPS.length) {
1228
1285
  // reading TARGET_VERSION=) depend on them.
1229
1286
  let summary;
1230
1287
  var workerMarkers = extractMarkerLines(workerText);
1231
- if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget && publishVerified) {
1288
+ if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget && (publishVerified || npmPublishSkipped)) {
1289
+ // The skip path records the computed target and the explicit skip — never
1290
+ // "published:", which would be a false claim (nothing was shipped).
1232
1291
  const markerLines = "TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
1233
- "published: muse-crew@" + publishTarget.target;
1292
+ (npmPublishSkipped
1293
+ ? "skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)"
1294
+ : "published: muse-crew@" + publishTarget.target);
1234
1295
  summary = (stepResult.summary || "Step completed").slice(0, 2000 - markerLines.length - workerMarkers.length - 2) + "\n" + markerLines + (workerMarkers ? "\n" + workerMarkers : "");
1235
1296
  } else {
1236
1297
  summary = (stepResult.summary || "Step completed").slice(0, 2000 - workerMarkers.length - 1) + (workerMarkers ? "\n" + workerMarkers : "");
@@ -17,7 +17,7 @@ const crewHome = inputs.crewHome;
17
17
  const dashboardSlug = inputs.dashboardSlug;
18
18
  const dashboardName = inputs.dashboardName || "Muse Crew";
19
19
  const cronId = inputs.cronId || "crew-poll";
20
- const sweepCronId = inputs.sweepCronId || "crew-sweep";
20
+ const sweepCronId = inputs.sweepCronId || "crew-orphan-sweep";
21
21
 
22
22
  if (!crewRepoPath) throw new Error("crewRepoPath is required — path to muse-crew (git checkout or npm install)");
23
23
  if (!crewHome) throw new Error("crewHome is required — e.g. ~/.crew");
@@ -198,35 +198,30 @@ phase("sweep-cron");
198
198
  var sweepResult;
199
199
  try {
200
200
  sweepResult = await agent(
201
- "Check if the sweep cron exists and create it if not.\n\n" +
201
+ "Check if the orphan sweep cron exists and create it if not.\n\n" +
202
202
  "Cron id: " + sweepCronId + "\n" +
203
+ "Template: " + crewRepoPath + "/seed/cron-sweep-body-template.md\n" +
203
204
  "crewHome: " + crewHome + "\n" +
204
205
  "Dashboard slug: " + dashboardSlug + "\n\n" +
205
206
  "Steps:\n" +
206
207
  "1. Call cron_list and look for a job with id '" + sweepCronId + "'\n" +
207
- "2. If it exists, return { existed: true }\n" +
208
- "3. If not found, call cron_add with:\n" +
209
- " - id: '" + sweepCronId + "'\n" +
210
- " - title: 'Muse Crew orphan sweep'\n" +
211
- " - enabled: true\n" +
212
- " - mode: 'task'\n" +
213
- " - schedule: { kind: 'interval', every: '30m' }\n" +
214
- " - owner: 'space:" + dashboardSlug + "'\n" +
215
- " - timeout_secs: 120\n" +
216
- " - body: the body text below\n" +
217
- " Return { existed: false }\n\n" +
218
- "Body text for the cron:\n" +
219
- "---\n" +
220
- "## Muse Crew Orphan Sweep\n\n" +
221
- "1. Call artifact_invoke_action on slug \"" + dashboardSlug + "\", action \"getdispatchstate\", args {}.\n" +
222
- "2. From the response, extract the `id` field of each `ready_tasks` entry whose\n" +
223
- " latest_session.status is \"running\", verbatim copy the full ID exactly\n" +
224
- " as it appears; do not shorten, truncate, or reformat it. Return them as a\n" +
225
- " JSON array of strings. Do NOT reproduce any other part of the response.\n" +
226
- "3. Run: CREW_ACTIVE_TASKS=\"<space-separated ids>\" " + crewHome + "/lib/orphan-sweep.sh clean\n" +
227
- " If the list is empty, pass an empty string — do NOT omit the variable (the sweep fails closed without it).\n" +
228
- "4. Report the output. If it says CLEAN, no action was needed.\n" +
229
- "---\n\n" +
208
+ "2. If it exists, return { existed: true } WITHOUT touching it — adopt means\n" +
209
+ " adopt: do not enable it, rewrite its body, or change its schedule.\n" +
210
+ "3. If not found:\n" +
211
+ " a. Read the template at " + crewRepoPath + "/seed/cron-sweep-body-template.md\n" +
212
+ " b. Replace all occurrences of {crewHome} with: " + crewHome + "\n" +
213
+ " Replace all occurrences of {dashboardSlug} with: " + dashboardSlug + "\n" +
214
+ " c. Call cron_add with:\n" +
215
+ " - id: '" + sweepCronId + "'\n" +
216
+ " - title: 'Muse Crew orphan sweep'\n" +
217
+ " - enabled: false (deliberate holding state — the sweep stays disabled\n" +
218
+ " until explicitly enabled by a human; never resurrect it enabled)\n" +
219
+ " - mode: 'task'\n" +
220
+ " - schedule: { kind: 'interval', every: '30m' }\n" +
221
+ " - owner: 'space:" + dashboardSlug + "'\n" +
222
+ " - timeout_secs: 120\n" +
223
+ " - body: the processed template text\n" +
224
+ " d. Return { existed: false }\n\n" +
230
225
  "Return JSON with existed (boolean).",
231
226
  {
232
227
  key: "sweep-cron-1",