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.
@@ -468,6 +468,12 @@ if (missingInitialPins.length > 0) {
468
468
  }
469
469
  log("Lifecycle scripts pinned to " + RUN_LIB);
470
470
 
471
+ // Merge-lock holder identity (bug 2fc8f52f): the opaque task+run identity
472
+ // minted at this run's first claim (never a PID — short-lived agent PIDs
473
+ // made every concurrent acquire take the stale path). Set exactly once on
474
+ // the first claim; stable for the rest of the run's lifetime.
475
+ let lockHolder = taskId;
476
+
471
477
  while (i < STEPS.length) {
472
478
  const step = STEPS[i];
473
479
  const isFirstClaim = (i === startStepIndex && reworkCount === 0);
@@ -651,6 +657,13 @@ while (i < STEPS.length) {
651
657
  activeSessionId = claimResult.session_id;
652
658
  }
653
659
 
660
+ // Mint the merge-lock holder identity once: task+session of this run's
661
+ // first claim. The task ID alone cannot distinguish two runs of the same
662
+ // task (re-dispatch after park), and a PID is not a valid holder at all.
663
+ if (lockHolder === taskId && activeSessionId) {
664
+ lockHolder = taskId + "/" + activeSessionId;
665
+ }
666
+
654
667
  // ── Capture: baseline evidence for experiential tasks ─────────────
655
668
  // Hazel's QA capture pass runs right after Triage, before Map, for tasks
656
669
  // Sage flagged experiential. The capture itself is parent-driven (the
@@ -807,11 +820,11 @@ while (i < STEPS.length) {
807
820
 
808
821
  } else if (step.name === "Integrate") {
809
822
  instructions = "Merge the approved task branch into main.\n\n" +
810
- "Run: "+ LIFECYCLE_ENV + " integrate " + taskId + " \"merge: chore: " + safeTitle + "\"\n\n" +
823
+ "Run: "+ LIFECYCLE_ENV + "WORKFLOW_RUN_ID=" + lockHolder + " integrate " + taskId + " \"merge: chore: " + safeTitle + "\"\n\n" +
811
824
  "Read the output:\n" +
812
825
  "- If it contains MERGED, integration succeeded. Report the merged commit hash.\n" +
813
826
  "- 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" +
814
- "- 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" +
827
+ "- 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" +
815
828
  "- 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" +
816
829
  "RESOLUTION:\n" +
817
830
  "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" +
@@ -840,13 +853,17 @@ while (i < STEPS.length) {
840
853
  instructions = "Publish the npm package by running exactly ONE command — the deterministic publish script.\n" +
841
854
  "The release decision is already made and recorded: release: yes, version_bump: " + publishTarget.scope +
842
855
  ", target version " + publishTarget.target + " (computed as " + publishTarget.base + " + " + publishTarget.scope +
843
- " → " + 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" +
844
857
  "Run exactly this command and no other publish-related commands:\n" +
845
858
  "TASK_ID=" + taskId + " REPO_PATH=" + REPO_PATH + " PKG=muse-crew TARGET_VERSION=" + publishTarget.target +
846
859
  " CREW_HOME=" + crewHome + " LIFECYCLE=" + LIFECYCLE + " RELEASE_SCRIPT=" + RELEASE_SCRIPT +
847
860
  " bash " + PUBLISH_NPM + "\n\n" +
848
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" +
849
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" +
850
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" +
851
868
  "TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
852
869
  "published: muse-crew@" + publishTarget.target + "\n" +
@@ -865,20 +882,34 @@ while (i < STEPS.length) {
865
882
  // getprovenance-vs-HEAD verification below stays as the final gate.
866
883
  var artifactPublish = null;
867
884
  var publishLockRefreshed = false;
885
+ var publishSkippedNoLock = false;
868
886
  try {
869
- // STEP 0 (mechanical): refresh the merge lock so a long build cannot
870
- // go stale mid-publish.
871
- var lockRefresh = await agent(
872
- "Run: "+ LIFECYCLE_ENV + " refresh-lock " + taskId + "\n" +
873
- "Return JSON { \"refreshed\": <true if the command exited zero, false otherwise>, \"output\": \"<trimmed stdout>\" } and nothing else.",
874
- { key: attemptKey("publish-lock-refresh-" + taskId, reworkCount), label: "Refreshing merge lock for publish",
875
- 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"] } }
876
898
  );
877
- if (!lockRefresh.refreshed) {
878
- 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;
879
907
  }
880
- publishLockRefreshed = true;
881
- // 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
882
913
  // agent only makes the artifact_edit call and reports whether it was
883
914
  // accepted — no prose claim to trust. If the artifact tool namespace
884
915
  // is missing from this child it reports honestly and the workflow
@@ -899,14 +930,46 @@ while (i < STEPS.length) {
899
930
  }
900
931
  var publishFailure = null;
901
932
  if (rebuildTrigger.edit_started) {
902
- // STEP 1b (mechanical): bounded poll for build completion.
903
- var buildPoll = await agent(
904
- "Poll artifact_status for slug \"" + PUBLISH_SLUG + "\" until no build is running. Check every 30 seconds, up to 20 checks (10 minutes max).\n" +
905
- "Return JSON { \"build_done\": <true if no build is running within budget, false on timeout>, \"status\": \"<final status or timeout note>\" } and nothing else.",
906
- { key: attemptKey("publish-artifact-poll-" + taskId, reworkCount), label: "Waiting for artifact build to complete",
907
- schema: { type: "object", properties: { build_done: { type: "boolean" }, status: { type: "string" } }, required: ["build_done"] },
908
- timeoutMs: 660000 }
909
- );
933
+ // STEP 1b (mechanical): bounded poll for build completion, chunked so
934
+ // the merge-lock lease is refreshed before it can expire. The 600s
935
+ // lease is shorter than the worst-case 10-minute build poll, so the
936
+ // poll runs in three chunks (7 checks x 30s ~= 3.5 min each) with a
937
+ // holder-only lease refresh between chunks. If a refresh fails, the
938
+ // lock was lost: stop the run and park the task never continue to
939
+ // a provenance stamp or version assignment without holding the lock.
940
+ var buildPoll = null;
941
+ for (var chunk = 1; chunk <= 3; chunk++) {
942
+ if (chunk > 1) {
943
+ var refreshPoll = await agent(
944
+ "Refresh the merge lock for task " + taskId + ".\n" +
945
+ "Run: " + LIFECYCLE_ENV + " refresh-lock " + taskId + "\n" +
946
+ "If the output contains REFRESHED, return JSON { \"held\": true } and nothing else. Otherwise return JSON { \"held\": false, \"output\": \"<verbatim output>\" } and nothing else.",
947
+ { key: attemptKey("publish-lock-refresh-poll-" + taskId + "-c" + chunk, reworkCount), label: "Refreshing merge lock during build poll",
948
+ schema: { type: "object", properties: { held: { type: "boolean" }, output: { type: "string" } }, required: ["held"] },
949
+ timeoutMs: 60000 }
950
+ );
951
+ if (!refreshPoll || refreshPoll.held !== true) {
952
+ 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.");
953
+ }
954
+ }
955
+ // Chunk 1 keeps the original stable key; later chunks use -c<N>
956
+ // suffixed keys. All are attemptKey-scoped so rework passes stay
957
+ // disjoint (replay-key contract).
958
+ var pollKey = (chunk === 1)
959
+ ? attemptKey("publish-artifact-poll-" + taskId, reworkCount)
960
+ : attemptKey("publish-artifact-poll-" + taskId + "-c" + chunk, reworkCount);
961
+ buildPoll = await agent(
962
+ "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" +
963
+ "Return JSON { \"build_done\": <true if no build is running within budget, false on timeout>, \"status\": \"<final status or timeout note>\" } and nothing else.",
964
+ { key: pollKey, label: "Waiting for artifact build to complete (chunk " + chunk + " of 3)",
965
+ schema: { type: "object", properties: { build_done: { type: "boolean" }, status: { type: "string" } }, required: ["build_done"] },
966
+ timeoutMs: 270000 }
967
+ );
968
+ if (buildPoll && buildPoll.build_done) { break; }
969
+ }
970
+ if (!buildPoll || !buildPoll.build_done) {
971
+ buildPoll = { build_done: false, status: (buildPoll && buildPoll.status) || "build still running after the 10.5-minute bounded poll" };
972
+ }
910
973
  if (buildPoll.build_done) {
911
974
  // STEP 1c (mechanical): stamp provenance from workflow-computed values.
912
975
  var provStamp = await agent(
@@ -930,25 +993,34 @@ while (i < STEPS.length) {
930
993
  } else {
931
994
  publishFailure = "Artifact rebuild trigger failed: " + (rebuildTrigger.error || "artifact_edit not accepted") + ". The publish did not land.";
932
995
  }
933
- // 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
934
998
  // commits builder leftovers if any, removes the worktree, and releases
935
- // the merge lock — even when the publish itself failed, so a skipped
936
- // 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).
937
1002
  var postDeploy = await agent(
938
1003
  "Run: "+ LIFECYCLE_ENV + " post-deploy " + taskId + "\n" +
939
1004
  "Return JSON { \"deployed\": <true if the output contains DEPLOYED, false otherwise>, \"output\": \"<trimmed output>\" } and nothing else.",
940
1005
  { key: attemptKey("publish-postdeploy-" + taskId, reworkCount), label: "Finalizing publish (post-deploy)",
941
1006
  schema: { type: "object", properties: { deployed: { type: "boolean" }, output: { type: "string" } }, required: ["deployed"] } }
942
1007
  );
943
- if (publishFailure) {
944
- return await parkTask(publishFailure + (postDeploy.deployed
945
- ? " Post-deploy finalized cleanup."
946
- : " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
947
- }
948
- if (!postDeploy.deployed) {
949
- 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);
950
1023
  }
951
- log("Deterministic artifact publish completed for task " + taskId + ": provenance at " + artifactPublish.source_commit);
952
1024
  } catch (pubErr) {
953
1025
  // Best-effort cleanup: if the lock was refreshed, try to release it
954
1026
  // before parking so the failure cannot wedge the next run.
@@ -969,6 +1041,12 @@ while (i < STEPS.length) {
969
1041
  // The work agent no longer performs the publish — it reports on the
970
1042
  // mechanical outcome above. It must not rebuild or re-stamp: a second
971
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 {
972
1050
  instructions = "Publish the merged code to the live artifact.\n\n" +
973
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" +
974
1052
  "For the change summary, run: cd " + REPO_PATH + " && git log -1 --stat\n\n" +
@@ -981,6 +1059,7 @@ while (i < STEPS.length) {
981
1059
  "The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
982
1060
  "Write plain prose describing what was published, then on its own line: VERDICT: PASS\n" +
983
1061
  "The VERDICT line must be the last line of your report.";
1062
+ }
984
1063
  } else if (PUBLISH_TYPE === "vercel") {
985
1064
  // vercel publish is not yet implemented — block without inventing behavior
986
1065
  instructions = "The project's publish target is \"vercel\", which is not yet implemented.\n" +
@@ -1127,8 +1206,21 @@ while (i < STEPS.length) {
1127
1206
  const status = passed ? "completed" : (step.name === "Integrate" || step.name === "Publish" ? "failed" : "rejected");
1128
1207
 
1129
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.
1130
1216
  var publishVerified = false;
1217
+ var npmPublishSkipped = false;
1131
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) {
1132
1224
  try {
1133
1225
  var verifyResult = await agent(
1134
1226
  "Run: npm view muse-crew version 2>/dev/null. Return JSON { \"registry_version\": \"<output trimmed>\" } and nothing else.",
@@ -1145,6 +1237,7 @@ while (i < STEPS.length) {
1145
1237
  } catch (e) {
1146
1238
  return await parkTask("Publish verification failed: could not read the registry version (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1147
1239
  }
1240
+ } // end: !npmPublishSkipped — a skipped publish has nothing to verify
1148
1241
  }
1149
1242
 
1150
1243
  // Artifact publish verification: the worker cannot self-certify a deploy.
@@ -1152,6 +1245,14 @@ while (i < STEPS.length) {
1152
1245
  // integrated commit. A stale or missing provenance means the publish did not
1153
1246
  // land — fail closed, do not trust the worker's prose.
1154
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 {
1155
1256
  try {
1156
1257
  var headResult = await agent(
1157
1258
  "Run: cd " + REPO_PATH + " && git rev-parse HEAD. Return JSON { \"head\": \"<output trimmed>\" } and nothing else.",
@@ -1175,6 +1276,7 @@ while (i < STEPS.length) {
1175
1276
  } catch (e) {
1176
1277
  return await parkTask("Publish verification failed: could not read artifact provenance (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1177
1278
  }
1279
+ } // end: !publishSkippedNoLock — a skipped publish has nothing to verify
1178
1280
  }
1179
1281
 
1180
1282
  // Session notes. Machine-readable marker lines are extracted from the full
@@ -1183,9 +1285,13 @@ while (i < STEPS.length) {
1183
1285
  // reading TARGET_VERSION=) depend on them.
1184
1286
  let summary;
1185
1287
  var workerMarkers = extractMarkerLines(workerText);
1186
- 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).
1187
1291
  const markerLines = "TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
1188
- "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);
1189
1295
  summary = (stepResult.summary || "Step completed").slice(0, 2000 - markerLines.length - workerMarkers.length - 2) + "\n" + markerLines + (workerMarkers ? "\n" + workerMarkers : "");
1190
1296
  } else {
1191
1297
  summary = (stepResult.summary || "Step completed").slice(0, 2000 - workerMarkers.length - 1) + (workerMarkers ? "\n" + workerMarkers : "");