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.
@@ -906,13 +906,17 @@ while (i < STEPS.length) {
906
906
  instructions = "Publish the npm package by running exactly ONE command — the deterministic publish script.\n" +
907
907
  "The release decision is already made and recorded: release: yes, version_bump: " + publishTarget.scope +
908
908
  ", target version " + publishTarget.target + " (computed as " + publishTarget.base + " + " + publishTarget.scope +
909
- " → " + publishTarget.target + " by the workflow, not by you). There is no decision to make and no skip path.\n\n" +
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" +
910
910
  "Run exactly this command and no other publish-related commands:\n" +
911
911
  "TASK_ID=" + taskId + " REPO_PATH=" + REPO_PATH + " PKG=muse-crew TARGET_VERSION=" + publishTarget.target +
912
912
  " CREW_HOME=" + crewHome + " LIFECYCLE=" + LIFECYCLE + " RELEASE_SCRIPT=" + RELEASE_SCRIPT +
913
913
  " bash " + PUBLISH_NPM + "\n\n" +
914
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" +
915
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" +
916
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" +
917
921
  "TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
918
922
  "published: muse-crew@" + publishTarget.target + "\n" +
@@ -931,20 +935,34 @@ while (i < STEPS.length) {
931
935
  // getprovenance-vs-HEAD verification below stays as the final gate.
932
936
  var artifactPublish = null;
933
937
  var publishLockRefreshed = false;
938
+ var publishSkippedNoLock = false;
934
939
  try {
935
- // STEP 0 (mechanical): refresh the merge lock so a long build cannot
936
- // go stale mid-publish.
937
- var lockRefresh = await agent(
938
- "Run: "+ LIFECYCLE_ENV + " refresh-lock " + taskId + "\n" +
939
- "Return JSON { \"refreshed\": <true if the command exited zero, false otherwise>, \"output\": \"<trimmed stdout>\" } and nothing else.",
940
- { key: attemptKey("publish-lock-refresh-" + taskId, totalReworkCount), label: "Refreshing merge lock for publish",
941
- schema: { type: "object", properties: { refreshed: { type: "boolean" }, output: { type: "string" } }, required: ["refreshed"] } }
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"] } }
942
951
  );
943
- if (!lockRefresh.refreshed) {
944
- return await parkTask("Publish lock refresh failed: " + (lockRefresh.output || "no output") + ". Fail-closed — lock state unknown.");
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;
945
960
  }
946
- publishLockRefreshed = true;
947
- // STEP 1 (mechanical): trigger the rebuild with one narrow call. The
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
948
966
  // agent only makes the artifact_edit call and reports whether it was
949
967
  // accepted — no prose claim to trust. If the artifact tool namespace
950
968
  // is missing from this child it reports honestly and the workflow
@@ -1028,25 +1046,34 @@ while (i < STEPS.length) {
1028
1046
  } else {
1029
1047
  publishFailure = "Artifact rebuild trigger failed: " + (rebuildTrigger.error || "artifact_edit not accepted") + ". The publish did not land.";
1030
1048
  }
1031
- // STEP 2 (mechanical, always once the lock was refreshed): post-deploy
1049
+ } // end: publishSkippedNoLock no rebuild, no stamp, nothing to ship
1050
+ // STEP 2 (mechanical, always — skip path included): post-deploy
1032
1051
  // commits builder leftovers if any, removes the worktree, and releases
1033
- // the merge lock — even when the publish itself failed, so a skipped
1034
- // or failed publish can never leave the lock held (canary b5efd1b1).
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).
1035
1055
  var postDeploy = await agent(
1036
1056
  "Run: "+ LIFECYCLE_ENV + " post-deploy " + taskId + "\n" +
1037
1057
  "Return JSON { \"deployed\": <true if the output contains DEPLOYED, false otherwise>, \"output\": \"<trimmed output>\" } and nothing else.",
1038
1058
  { key: attemptKey("publish-postdeploy-" + taskId, totalReworkCount), label: "Finalizing publish (post-deploy)",
1039
1059
  schema: { type: "object", properties: { deployed: { type: "boolean" }, output: { type: "string" } }, required: ["deployed"] } }
1040
1060
  );
1041
- if (publishFailure) {
1042
- return await parkTask(publishFailure + (postDeploy.deployed
1043
- ? " Post-deploy finalized cleanup."
1044
- : " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
1045
- }
1046
- if (!postDeploy.deployed) {
1047
- 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.");
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);
1048
1076
  }
1049
- log("Deterministic artifact publish completed for task " + taskId + ": provenance at " + artifactPublish.source_commit);
1050
1077
  } catch (pubErr) {
1051
1078
  // Best-effort cleanup: if the lock was refreshed, try to release it
1052
1079
  // before parking so the failure cannot wedge the next run.
@@ -1067,6 +1094,12 @@ while (i < STEPS.length) {
1067
1094
  // The work agent no longer performs the publish — it reports on the
1068
1095
  // mechanical outcome above. It must not rebuild or re-stamp: a second
1069
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 {
1070
1103
  instructions = "Publish the merged code to the live artifact.\n\n" +
1071
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" +
1072
1105
  "For the change summary, run: cd " + REPO_PATH + " && git log -1 --stat\n\n" +
@@ -1079,6 +1112,7 @@ while (i < STEPS.length) {
1079
1112
  "The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
1080
1113
  "Write plain prose describing what was published, then on its own line: VERDICT: PASS\n" +
1081
1114
  "The VERDICT line must be the last line of your report.";
1115
+ }
1082
1116
  } else if (PUBLISH_TYPE === "vercel") {
1083
1117
  // vercel publish is not yet implemented — block without inventing behavior
1084
1118
  instructions = "The project's publish target is \"vercel\", which is not yet implemented.\n" +
@@ -1090,7 +1124,9 @@ while (i < STEPS.length) {
1090
1124
  // declared release: yes, QA verifies the registry actually moved. A silent
1091
1125
  // publish skip becomes a loud QA failure with evidence, not a pass.
1092
1126
  var npmPublishCheck = (PUBLISH_TYPE === "npm" && releaseDecision && releaseDecision.release === "yes")
1093
- ? "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" +
1094
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" +
1095
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" +
1096
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"
@@ -1305,8 +1341,21 @@ while (i < STEPS.length) {
1305
1341
  const status = passed ? "completed" : (step.name === "Integrate" || step.name === "Publish" ? "failed" : "rejected");
1306
1342
 
1307
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.
1308
1351
  var publishVerified = false;
1352
+ var npmPublishSkipped = false;
1309
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) {
1310
1359
  try {
1311
1360
  var verifyResult = await agent(
1312
1361
  "Run: npm view muse-crew version 2>/dev/null. Return JSON { \"registry_version\": \"<output trimmed>\" } and nothing else.",
@@ -1323,6 +1372,7 @@ while (i < STEPS.length) {
1323
1372
  } catch (e) {
1324
1373
  return await parkTask("Publish verification failed: could not read the registry version (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1325
1374
  }
1375
+ } // end: !npmPublishSkipped — a skipped publish has nothing to verify
1326
1376
  }
1327
1377
 
1328
1378
  // Artifact publish verification: the worker cannot self-certify a deploy.
@@ -1330,6 +1380,14 @@ while (i < STEPS.length) {
1330
1380
  // integrated commit. A stale or missing provenance means the publish did not
1331
1381
  // land — fail closed, do not trust the worker's prose.
1332
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 {
1333
1391
  try {
1334
1392
  var headResult = await agent(
1335
1393
  "Run: cd " + REPO_PATH + " && git rev-parse HEAD. Return JSON { \"head\": \"<output trimmed>\" } and nothing else.",
@@ -1353,6 +1411,7 @@ while (i < STEPS.length) {
1353
1411
  } catch (e) {
1354
1412
  return await parkTask("Publish verification failed: could not read artifact provenance (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1355
1413
  }
1414
+ } // end: !publishSkippedNoLock — a skipped publish has nothing to verify
1356
1415
  }
1357
1416
 
1358
1417
  // Session notes. Machine-readable marker lines are extracted from the full
@@ -1361,9 +1420,13 @@ while (i < STEPS.length) {
1361
1420
  // reading TARGET_VERSION=) depend on them.
1362
1421
  let summary;
1363
1422
  var workerMarkers = extractMarkerLines(workerText);
1364
- 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).
1365
1426
  const markerLines = "TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
1366
- "published: muse-crew@" + publishTarget.target;
1427
+ (npmPublishSkipped
1428
+ ? "skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)"
1429
+ : "published: muse-crew@" + publishTarget.target);
1367
1430
  summary = (stepResult.summary || "Step completed").slice(0, 2000 - markerLines.length - workerMarkers.length - 2) + "\n" + markerLines + (workerMarkers ? "\n" + workerMarkers : "");
1368
1431
  } else {
1369
1432
  summary = (stepResult.summary || "Step completed").slice(0, 2000 - workerMarkers.length - 1) + (workerMarkers ? "\n" + workerMarkers : "");