muse-crew 0.14.1 → 0.14.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -475,7 +475,10 @@ function hydrateReleaseDecision(rec) {
475
475
  // fabricated from the diff; it must match the artifact's real content.
476
476
 
477
477
  // See docs/decisions/publish-path.md#publish-attempt-ledger: every trigger outcome is recorded in the durable ledger.
478
- async function recordPublishLedger(entry, rework) {
478
+ // keyTag (optional): same-outcome ledger writes in one pass need distinct
479
+ // agent-call keys — the runtime replays cached calls by key, so a shared key
480
+ // silently drops the second write (2026-09-19, submitted-on-issuance).
481
+ async function recordPublishLedger(entry, rework, keyTag) {
479
482
  try {
480
483
  var ledgerDir = crewHome + "/.publish-ledger";
481
484
  var line = JSON.stringify({
@@ -497,7 +500,7 @@ async function recordPublishLedger(entry, rework) {
497
500
  "Run: mkdir -p " + sq(ledgerDir) + " && printf '%s\n' " + sq(line) +
498
501
  " | sed \"s/@LEDGER_TS@/$(date -u +%Y-%m-%dT%H:%M:%SZ)/\" >> " + sq(ledgerDir + "/" + PUBLISH_SLUG + ".jsonl") + " && echo LEDGER_OK\n" +
499
502
  "Return JSON { \"result\": \"<verbatim output>\" } and nothing else.",
500
- { key: attemptKey("publish-ledger-" + taskId + "-" + entry.outcome, rework),
503
+ { key: attemptKey("publish-ledger-" + taskId + "-" + entry.outcome + (keyTag ? "-" + keyTag : ""), rework),
501
504
  label: "Recording publish attempt in ledger",
502
505
  schema: { type: "object", properties: { result: { type: "string" } }, required: ["result"] } }
503
506
  );
@@ -690,7 +693,7 @@ let releaseDecision = null; // { release: "yes"|"no", version_bump: "patch"|"min
690
693
  // Already-merged idempotency: the verified sha from the builder's
691
694
  // `repo_diff: none (already-merged: <sha>)` declaration (null when the
692
695
  // builder made commits or declared a runtime-state deliverable). The
693
- // workflow verifies the sha is an ancestor of main at Build closeout;
696
+ // workflow verifies the sha is an ancestor of the integration target at Build closeout;
694
697
  // Review's no-diff branch reads this, never the builder's prose.
695
698
  let alreadyMergedSha = null;
696
699
  // Deterministic publish target — computed by the workflow (registry base +
@@ -1132,7 +1135,7 @@ while (i < STEPS.length) {
1132
1135
  "cd " + WORKTREE_HINT + "\n" +
1133
1136
  "git add -A\n" +
1134
1137
  "git commit -m \"chore: " + safeTitle + "\"\n\n" +
1135
- "If the task's deliverable is runtime state (a cron definition, scheduler change, or dashboard/config state created outside the repo) and the repository genuinely needs no change, do NOT fabricate a commit: leave the branch with no commits ahead of main and declare `repo_diff: none` in your report, naming the runtime-state deliverable. If you verified the deliverable is already on main (a prior merge or hand-repair landed it — do NOT re-implement working code), make no commit and declare `repo_diff: none (already-merged: <sha>)` naming the main commit that carries the work; the workflow verifies the sha is an ancestor of main, and a false declaration fails the phase. Otherwise commit your changes normally.\n\n" +
1138
+ "If the task's deliverable is runtime state (a cron definition, scheduler change, or dashboard/config state created outside the repo) and the repository genuinely needs no change, do NOT fabricate a commit: leave the branch with no commits ahead of the integration target and declare `repo_diff: none` in your report, naming the runtime-state deliverable. If you verified the deliverable is already on the integration target (a prior merge or hand-repair landed it — do NOT re-implement working code), make no commit and declare `repo_diff: none (already-merged: <sha>)` naming the integration-target commit that carries the work; the workflow verifies the sha is an ancestor of the integration target, and a false declaration fails the phase. Otherwise commit your changes normally.\n\n" +
1136
1139
  (rejectionNotes ? "REWORK after rejection. Address:\n" + rejectionNotes + "\n\n" : "") +
1137
1140
  "Report back in plain prose: what you built and the outcome." +
1138
1141
  (PUBLISH_TYPE === "npm" ? " End your report with the release: and version_bump: lines exactly as specified above — keep them on their own lines, lowercase, unrephrased — then a line `worktree: ` followed by the exact working directory path from above (copy it verbatim \u2014 it must match character-for-character), then a final line with exactly: VERDICT: PASS if the build is complete, VERDICT: FAIL if it is not." : " End your report with a line `worktree: ` followed by the exact working directory path from above (copy it verbatim \u2014 it must match character-for-character), then exactly one line: VERDICT: PASS if the build is complete, VERDICT: FAIL if it is not.");
@@ -1170,16 +1173,16 @@ while (i < STEPS.length) {
1170
1173
  (mapperSpec ? "MAPPER'S SPEC (the builder was asked to implement exactly this):\n" + mapperSpec + "\n\n" : "Read the spec from the task description.\n\n") +
1171
1174
  "Examine the code changes by running:\n" +
1172
1175
  LIFECYCLE_ENV + LIFECYCLE + " inspect " + taskId + "\n\n" +
1173
- "The inspect output is authoritative: it prints the task branch's actual tip commit (TIP) and every commit ahead of main. Base your review ONLY on this output — do NOT run git log yourself to pick commits, and do NOT discuss commit hashes from any other source (they may come from stale rework rounds or a different repo).\n\n" +
1176
+ "The inspect output is authoritative: it prints the task branch's actual tip commit (TIP) and every commit ahead of the integration target (inspect prints the target name). Base your review ONLY on this output — do NOT run git log yourself to pick commits, and do NOT discuss commit hashes from any other source (they may come from stale rework rounds or a different repo).\n\n" +
1174
1177
  "You can also read specific files in the worktree at:\n" +
1175
1178
  WORKTREE_HINT + "/\n\n" +
1176
1179
  "Check quality, correctness, spec compliance.\n" +
1177
1180
  (SURFACE_TERMINAL ? "TERMINAL UX REVIEW: judge the CLI surface against " + UX_DOCTRINE_PATH + " — help accuracy, error quality, exit codes, output clarity. Reject when the bar is not met.\n" : "") +
1178
1181
  (SURFACE_ARTIFACT ? "ARTIFACT UX REVIEW: judge the rendered surface against " + UX_DOCTRINE_PATH + " — alignment, spacing, hierarchy, composition, balance, finish, correctness. Reject when the bar is not met.\n" : "") +
1179
1182
  "Check that public-affecting changes have matching public doc updates (API.md or the published API contract). If the docs are missing or inaccurate, report what is stale, then end your report with exactly this line: VERDICT: FAIL.\n" +
1180
- "If the branch has no commits ahead of main (inspect shows an empty commit log), approve ONLY if the Build summary declares `repo_diff: none` with (a) a plausible runtime-state deliverable (e.g. a cron created via the cron tool), or (b) an already-merged declaration `repo_diff: none (already-merged: <sha>)` AND the mechanical fact below confirms the sha verified. MECHANICAL FACT (computed by the workflow, never by the builder): already_merged sha = " + (alreadyMergedSha ? alreadyMergedSha + " (verified ancestor of main: YES)" : "none declared") + ". Otherwise report 'no commits ahead of main and no valid repo_diff: none declaration — the builder likely forgot to commit', then end your report with exactly this line: VERDICT: FAIL.\n" +
1183
+ "If the branch has no commits ahead of the integration target (inspect shows an empty commit log), approve ONLY if the Build summary declares `repo_diff: none` with (a) a plausible runtime-state deliverable (e.g. a cron created via the cron tool), or (b) an already-merged declaration `repo_diff: none (already-merged: <sha>)` AND the mechanical fact below confirms the sha verified. MECHANICAL FACT (computed by the workflow, never by the builder): already_merged sha = " + (alreadyMergedSha ? alreadyMergedSha + " (verified ancestor of the integration target: YES)" : "none declared") + ". Otherwise report 'no commits ahead of the integration target and no valid repo_diff: none declaration — the builder likely forgot to commit', then end your report with exactly this line: VERDICT: FAIL.\n" +
1181
1184
  (PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry, and versions are assigned at publish time — never in branches. Two checks:\n" +
1182
- "(a) The task branch must NOT have changed package.json's `version` field. Check: cd " + REPO_PATH + " && git diff main..." + TASK_BRANCH + " -- package.json. If the branch touched `version` in any way, report 'versions are assigned at publish time, never in branches — remove the version change' in your notes, then end your report with exactly this line: VERDICT: FAIL.\n" +
1185
+ "(a) The task branch must NOT have changed package.json's `version` field. First resolve the integration target: " + LIFECYCLE_ENV + LIFECYCLE + " integration-target. Then check: cd " + REPO_PATH + " && git diff <target>..." + TASK_BRANCH + " -- package.json (substitute the exact token integration-target printed — when it prints HEAD, use the literal word HEAD: `git diff HEAD...<branch>` diffs the detached checkout against the branch). If the branch touched `version` in any way, report 'versions are assigned at publish time, never in branches — remove the version change' in your notes, then end your report with exactly this line: VERDICT: FAIL.\n" +
1183
1186
  "(b) The accepted Build report declares: " + releaseDecisionText() + ". " +
1184
1187
  (releaseDecision
1185
1188
  ? "Validate this decision against the change: release must be 'yes' when the change is consumer-observable and 'no' when internal-only; the version_bump scope must fit the change (patch for fixes, minor for new behavior, major for breaking changes). If the decision is wrong or mis-scoped, report your notes, then end with exactly this line: VERDICT: FAIL."
@@ -1187,36 +1190,32 @@ while (i < STEPS.length) {
1187
1190
  "Write your review as plain prose — findings, then decision. End your report with exactly one line: VERDICT: PASS if it passes, VERDICT: FAIL if it fails.";
1188
1191
 
1189
1192
  } else if (step.name === "Integrate") {
1190
- instructions = "Merge the approved task branch into main.\n\n" +
1191
- "Run: "+ LIFECYCLE_ENV + "WORKFLOW_RUN_ID=" + lockHolder + " integrate " + taskId + " \"merge: chore: " + safeTitle + "\"\n\n" +
1192
- "Read the output:\n" +
1193
- "- If it contains MERGED, integration succeeded. Report the merged commit hash.\n" +
1194
- "- If it contains MERGED_EMPTY, the branch had no commits ahead of main (declared by Build as repo_diff: none — either a runtime-state deliverable or an already-merged sha the workflow verified). 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 or already on main', then end your report with exactly this line: VERDICT: PASS. SKIP STEP 2 (push): there is no new commit to push.\n" +
1195
- "- 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" +
1196
- "- 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" +
1193
+ instructions = "Merge the approved task branch into the integration target and push the result.\n" +
1194
+ "The integration target is the repo's current checkout — a branch name, or the literal word HEAD when detached (no branch name exists when detached). Print the exact token with: "+ LIFECYCLE_ENV + LIFECYCLE + " integration-target\n" +
1195
+ "NEVER run git checkout — the lifecycle merges into the current checkout; moving it abandons the line Publish verifies.\n\n" +
1196
+ "Read the staged publish base (return its stdout verbatim):\n" + crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
1197
+ "Then run: "+ LIFECYCLE_ENV + "WORKFLOW_RUN_ID=" + lockHolder + " CREW_STAGED_BASE=<provenance.source_commit, empty when null> integrate " + taskId + " \"merge: chore: " + safeTitle + "\\\"\\n" +
1198
+ "The script merges, reconciles, records, and pushes under the merge lock. Informational only: RECONCILED, NO_REMOTE, NO_REMOTE_RECONCILE. Read the output:\n" +
1199
+ "- STAGED_BASE_MISMATCH: the line left the staged base (stray checkout). VERDICT: FAIL.\n" +
1200
+ "- MERGED_EMPTY — check FIRST (it contains the word MERGED): either a runtime-state deliverable or an already-merged sha the workflow verified (Build declared repo_diff: none). No lock taken, no new commit. Report 'merged empty: no repo changes'. VERDICT: PASS.\n" +
1201
+ "- MERGED: the merge landed; the script pushed inline. Read the push line: PUSHED — report the hash (detached prints PUSHED: origin/main (refspec HEAD:main)), VERDICT: PASS; NO_REMOTE_PUSH — no remote configured, VERDICT: PASS; ERROR after MERGED — a retry recovers the push; report it, VERDICT: FAIL.\n" +
1202
+ "- LOCK_HELD: 10-minute backoff exhausted. VERDICT: FAIL.\n" +
1203
+ "- CONFLICT: the plain merge failed — aborted, the target is clean, and your task still holds the merge lock. Do NOT fail yet. Resolve it:\n" +
1197
1204
  "RESOLUTION:\n" +
1198
- "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" +
1199
- "R2. For each conflicted file, read the three sides: git show :1:<file> (base), git show :2:<file> (ours = main), git show :3:<file> (theirs = task branch). Resolve each hunk by keeping both sides' changes when they do not semantically overlap. Version-only hunks resolve to the higher semver (safety net). Never invent new behavior. Leave no markers.\n" +
1200
- "R3. Verify in the scratch worktree, in this order: (a) git diff --check is clean; (b) git grep -n '^<<<<<<<' -- . returns nothing; (c) node --check every changed .js file (conflicted files plus everything listed by git diff --name-only). If any check fails, refresh the merge lock ("+ LIFECYCLE_ENV + " refresh-lock " + taskId + "), then retry the resolution using the failure output as context — max 3 attempts total.\n" +
1205
+ "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 (the integration target is checked out in the repo, so create the scratch worktree from HEAD — never check out another branch in the repo checkout): cd " + REPO_PATH + " && git worktree add -b resolve/" + taskId + " /tmp/crew-resolve-" + taskId + " HEAD. Reproduce the conflict in the scratch worktree: cd /tmp/crew-resolve-" + taskId + " && git merge " + TASK_BRANCH + ". This reproduces the exact conflict (the integration target has not moved — the lock was held throughout). The task branch " + TASK_BRANCH + " is never modified.\n" +
1206
+ "R2. For each conflicted file, read the three sides: git show :1:<file> (base), git show :2:<file> (ours = the integration target), git show :3:<file> (theirs = task branch). Resolve each hunk by keeping both sides' changes when they do not semantically overlap. Version-only hunks resolve to the higher semver (safety net). Never invent new behavior. Leave no markers.\n" +
1207
+ "R3. Verify in the scratch worktree, in this order: (a) git diff --check is clean; (b) git grep -n '^<<<<<<<' -- . returns nothing; (c) node --check every changed .js file (conflicted files plus everything listed by git diff --name-only). If any check fails, refresh the merge lock ("+ LIFECYCLE_ENV + " refresh-lock " + taskId + "), then retry using the failure output as context — max 3 attempts total.\n" +
1201
1208
  "R4. Commit the resolution on resolve/" + taskId + ": git add -A && git commit -m \"resolve conflicts: " + taskId + "\".\n" +
1202
- "R5. Back in " + REPO_PATH + ": git checkout main && git merge --ff-only resolve/" + taskId + ". This fast-forwards — main has not moved while the lock was held. Then STEP 2 applies: git push origin main. Report the merged commit hash.\n" +
1209
+ "R5. Back in " + REPO_PATH + ": git merge --ff-only resolve/" + taskId + " — merges the resolution into the current checkout (the integration target; no git checkout anywhere). It fast-forwards — the target has not moved under the lock. Report the merged hash.\n" +
1203
1210
  "R6. Clean up: cd " + REPO_PATH + " && git worktree remove --force /tmp/crew-resolve-" + taskId + " && git branch -D resolve/" + taskId + ".\n" +
1204
- "ESCALATE — report 'conflict needs human resolution', then end your report with exactly this line: VERDICT: FAIL — when: 3 attempts are exhausted; the conflict touches generated files, migrations, or public API contracts; or 'looks right + checks pass' is not sufficient for any other reason. On escalation, RELEASE THE LOCK so the task can be reworked later: run CREW_HOME=" + crewHome + " CREW_REPO=" + REPO_PATH + " " + MERGE_LOCK + " release " + taskId + " (release is keyed on task id; no PID needed). Do NOT run post-deploy on escalation — it would delete the untouched task branch the human still needs.\n" +
1205
- "- If it contains ERROR, something else failed. Report the error, then end your report with exactly this line: VERDICT: FAIL.\n\n" +
1206
- "\n" +
1207
- "STEP 2: Push the merged main to the remote repository.\n" +
1208
- "First, refresh the merge lock — a slow push must not let the 600s lease expire under you: " + LIFECYCLE_ENV + LIFECYCLE + " refresh-lock " + taskId + ". If the refresh fails, report 'merge lock lost before push' and end your report with exactly this line: VERDICT: FAIL. Do not push without the lock.\n" +
1209
- "Run: cd " + REPO_PATH + " && git push origin main\n" +
1210
- "- If the push succeeds, report the merged commit hash.\n" +
1211
- "- If the push is rejected as non-fast-forward (the remote has commits not present locally),\n" +
1212
- " NEVER force-push. Do not run any --force variant. The lifecycle's `integrate` already reconciled origin/main under the merge lock, so a rejection means the remote advanced after that reconcile — recover mechanically while you still hold the lock:\n" +
1213
- " 1. Run: cd " + REPO_PATH + " && git fetch origin main && git merge --no-edit origin/main -m \"merge: push-time reconcile (" + taskId + ")\".\n" +
1214
- " 2. If the merge conflicts, abort it (git merge --abort), report 'push-time reconcile conflicted — manual resolution required', then end your report with exactly this line: VERDICT: FAIL.\n" +
1215
- " 3. If it merges cleanly, retry the push exactly once: git push origin main. If the second push still rejects, report 'push rejected twice as non-fast-forward', then end your report with exactly this line: VERDICT: FAIL.\n" +
1216
- " 4. If the retry succeeds, report the merged commit hash.\n\n" +
1217
- "Report back in plain prose — what happened at each step — and end your report with exactly one line: VERDICT: PASS or VERDICT: FAIL.";
1218
-
1219
- } else if (step.name === "Publish") {
1211
+ "ESCALATE — 'conflict needs human resolution', VERDICT: FAIL — when: 3 attempts are exhausted; the conflict touches generated files, migrations, or public API contracts; or 'looks right + checks pass' is not sufficient for any other reason. On escalation, RELEASE THE LOCK so the task can be reworked later: run CREW_HOME=" + crewHome + " CREW_REPO=" + REPO_PATH + " " + MERGE_LOCK + " release " + taskId + " (release is keyed on task id; no PID needed). Do NOT run post-deploy on escalation — it would delete the untouched task branch the human still needs.\n" +
1212
+ "- ERROR: report it. VERDICT: FAIL.\n\n" +
1213
+ "R5-PUSH (manual R5 resolution only — the normal path pushed inline). Run: " + LIFECYCLE_ENV + LIFECYCLE + " push-target " + taskId + "\n" +
1214
+ "PUSHED — report the merged hash (detached prints PUSHED: origin/main (refspec HEAD:main)), VERDICT: PASS; PUSH_SKIPPED — no push attempted (no record + no lock); NO_REMOTE_PUSH — no remote; VERDICT: PASS; ERROR or CONFLICT — report it, VERDICT: FAIL.\n" +
1215
+ "NEVER force-push.\n\n" +
1216
+ "Report what happened at each step, ending with exactly one line: VERDICT: PASS or VERDICT: FAIL.";
1217
+
1218
+ } else if (step.name === "Publish") {
1220
1219
  if (PUBLISH_TYPE === "npm") {
1221
1220
  // Deterministic publish: the workflow computed the target version and the
1222
1221
  // agent runs exactly one command — the pinned publish script. The script
@@ -1233,7 +1232,7 @@ while (i < STEPS.length) {
1233
1232
  " bash " + PUBLISH_NPM + "\n\n" +
1234
1233
  "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" +
1235
1234
  "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" +
1236
- "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" +
1235
+ "If the script's output contains PUBLISH_SKIPPED=no-lock-held, the publish was skipped gracefully: Integrate reported MERGED_EMPTY (no commits ahead of the integration target), 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" +
1237
1236
  "TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
1238
1237
  "skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)\n" +
1239
1238
  "VERDICT: PASS\n\n" +
@@ -1550,7 +1549,7 @@ while (i < STEPS.length) {
1550
1549
  log("Publish pre-trigger baseline read failed for task " + taskId + " (" + (baselineErr && baselineErr.message ? baselineErr.message : baselineErr) + ") — receipt attribution skipped; durable audit-dir evidence is the only positive signal");
1551
1550
  }
1552
1551
  // See docs/decisions/publish-path.md#trigger-await: the artifact_edit call is awaited.
1553
- var rebuildTrigger = null;
1552
+ var rebuildTrigger = null, triggerThrew = false;
1554
1553
  try {
1555
1554
  var triggerText = String(await agent(rebuildPrompt,
1556
1555
  { key: rebuildAttemptKey, label: "Triggering artifact rebuild" }) || "");
@@ -1570,7 +1569,18 @@ while (i < STEPS.length) {
1570
1569
  }
1571
1570
  } catch (triggerErr) {
1572
1571
  log("Publish rebuild trigger for task " + taskId + " threw (" + (triggerErr && triggerErr.message ? triggerErr.message : triggerErr) + ") — outcome unknown until observation confirms it; the edit may have gone through");
1572
+ triggerThrew = true;
1573
1573
  }
1574
+ // (2026-09-19, room #22) Submitted-on-issuance: see docs/decisions/publish-path.md#submitted-on-issuance.
1575
+ await recordPublishLedger({
1576
+ commit: mergeCommitForPublish,
1577
+ attempt: rebuildAttemptKey,
1578
+ agent_id: null,
1579
+ applied_report: publishAppliedObservation,
1580
+ manifest_before: preTriggerManifest,
1581
+ outcome: "submitted",
1582
+ detail: "trigger issuance: rebuild-trigger agent invoked; receipt/observation pending. Invocation only - not evidence of artifact_edit or build acceptance." + (triggerThrew ? "; trigger-threw" : "; trigger-returned")
1583
+ }, reworkCount, "issuance");
1574
1584
  // Post-trigger observation (primary, not fallback): the workflow
1575
1585
  // attributes the edit itself. First the in-flight build state — a
1576
1586
  // build whose agent_id is new relative to the pre-trigger baseline
@@ -1690,8 +1700,8 @@ while (i < STEPS.length) {
1690
1700
  applied_report: publishAppliedObservation,
1691
1701
  manifest_before: preTriggerManifest,
1692
1702
  outcome: "submitted",
1693
- detail: "fire-and-forget trigger; build receipt captured by workflow-owned build-state observation (pre/post-trigger diff)"
1694
- }, reworkCount);
1703
+ detail: "fire-and-forget trigger; build receipt captured by workflow-owned build-state observation (pre/post-trigger diff); observation, not re-issuance"
1704
+ }, reworkCount, "receipt");
1695
1705
  } else {
1696
1706
  var newAuditDirs = [];
1697
1707
  try {
@@ -1822,10 +1832,6 @@ while (i < STEPS.length) {
1822
1832
  // its outcome — submitted, rejected, or unknown (unknown is recorded
1823
1833
  // at the park site above). A later run or human matches commit hash +
1824
1834
  // attempt key against the builder's eventual completion.
1825
- // (2026-09-16) The trigger is fire-and-forget: the observation above
1826
- // already recorded the ledger's submitted line on both positive paths
1827
- // and parked on unknown — there is no applied report to observe and
1828
- // no rejection signal to record.
1829
1835
  // (2026-09-18, H2 verdict-first) The verdict was decided exactly
1830
1836
  // once above; dispatch on it. landed bypasses the receipt poll
1831
1837
  // (the audit evidence already proved completion); an explicit
@@ -1959,21 +1965,12 @@ while (i < STEPS.length) {
1959
1965
  log("Publish build landed for task " + taskId + " — provenance stamp deferred to parent content verification");
1960
1966
  } else {
1961
1967
  // STEP 1b durable audit-dir fallback (2026-09-15, task aadeccc3):
1962
- // the poll above only observes IN-FLIGHT builds. A build that
1963
- // finished between the receipt capture and the poll's first check
1964
- // leaves no in-flight trace — but the platform's audit harness
1965
- // leaves a durable one (~/workspace/ts-spaces/<slug>/audits/
1966
- // <timestamp>-<id>/ per completed build). Diff the audit-dir
1967
- // listing against the pre-trigger snapshot: a timestamped dir
1968
- // that appeared during the attempt window is evidence a build
1969
- // completed. Attribution is by window, not by build identity:
1970
- // the poll's saw_stranger signal only catches stranger builds in
1971
- // flight AT a check — a stranger that finished entirely inside
1972
- // the window is indistinguishable, so any observed stranger
1973
- // blocks attribution and the outcome stays unknown. This never
1974
- // re-issues the edit and never stamps provenance — ok=true only
1975
- // routes to the parent's independent content read-back, which
1976
- // remains the real verification.
1968
+ // the poll observes only in-flight builds; a build that finished
1969
+ // inside the window leaves a durable audit dir. Diff the audit
1970
+ // listing against the pre-trigger snapshot: a new timestamped dir
1971
+ // is evidence a build completed. Attribution is by window, not by build identity: any
1972
+ // observed stranger blocks it. Never re-issues, never stamps;
1973
+ // ok=true routes to the parent's read-back (the verification).
1977
1974
  //
1978
1975
  // The poll end-state is read from the poll's own observations,
1979
1976
  // not from build_done alone: a build in flight at the last check
@@ -2142,7 +2139,7 @@ while (i < STEPS.length) {
2142
2139
  // artifact_edit would trigger a duplicate build.
2143
2140
  if (publishSkippedNoLock) {
2144
2141
  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" +
2145
- "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. Post-deploy step finished per its return — do NOT run post-deploy yourself.\n\n" +
2142
+ "Integrate reported MERGED_EMPTY (the task branch had no commits ahead of the integration target), so no merge lock was taken and there is nothing to ship. Post-deploy step finished per its return — do NOT run post-deploy yourself.\n\n" +
2146
2143
  "Write plain prose describing the skip, then on its own line: VERDICT: PASS\n" +
2147
2144
  "The VERDICT line must be the last line of your report.";
2148
2145
  } else {
@@ -2346,19 +2343,19 @@ while (i < STEPS.length) {
2346
2343
  var amCheck = await agent(
2347
2344
  "Verify the builder's already-merged declaration.\n" +
2348
2345
  "Run in shell and return the stdout verbatim:\n" +
2349
- "cd " + REPO_PATH + " && git rev-parse --verify --quiet " + am.sha + " >/dev/null && git merge-base --is-ancestor " + am.sha + " main && echo ALREADY_MERGED_YES || echo ALREADY_MERGED_NO",
2346
+ "TARGET=$(" + LIFECYCLE_ENV + LIFECYCLE + " integration-target) && cd " + REPO_PATH + " && git rev-parse --verify --quiet " + am.sha + " >/dev/null && git merge-base --is-ancestor " + am.sha + " $TARGET && echo ALREADY_MERGED_YES || echo ALREADY_MERGED_NO",
2350
2347
  { key: "verify-already-merged" + (reworkCount > 0 ? "-r" + reworkCount : ""), label: "Verifying already-merged declaration" }
2351
2348
  );
2352
2349
  var amOut = (typeof amCheck === "string") ? amCheck : JSON.stringify(amCheck);
2353
2350
  if (!/ALREADY_MERGED_YES/.test(amOut)) {
2354
- log("Build already-merged declaration failed verification — " + am.sha + " is not an ancestor of main — marking failed for retry");
2351
+ log("Build already-merged declaration failed verification — " + am.sha + " is not an ancestor of the integration target — marking failed for retry");
2355
2352
  await agent(
2356
2353
  "Record already-merged verification failure.\n" +
2357
2354
  "Run in shell and return the stdout verbatim:\n" + crewCmd("record-phase", {
2358
2355
  task_id: taskId,
2359
2356
  session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name, status: "failed",
2360
- notes: "Build declared repo_diff: none (already-merged: " + am.sha + ") but " + am.sha + " is not an ancestor of main in the configured repo. The declaration is fabricated or mistaken; the work is not on main. Phase failed for retry" },
2361
- event: { task_id: taskId, type: "failed", message: "Build already-merged declaration failed verification — " + am.sha + " not an ancestor of main, phase failed, dispatcher will retry" }
2357
+ notes: "Build declared repo_diff: none (already-merged: " + am.sha + ") but " + am.sha + " is not an ancestor of the integration target in the configured repo. The declaration is fabricated or mistaken; the work is not on the integration target. Phase failed for retry" },
2358
+ event: { task_id: taskId, type: "failed", message: "Build already-merged declaration failed verification — " + am.sha + " not an ancestor of the integration target, phase failed, dispatcher will retry" }
2362
2359
  }),
2363
2360
  { key: "record-already-merged-fail-" + step.name, label: "Recording already-merged verification failure" }
2364
2361
  );
@@ -2366,13 +2363,13 @@ while (i < STEPS.length) {
2366
2363
  __hatchWorkflowControl: "blocked",
2367
2364
  result: {
2368
2365
  blocked_reason: "Build already-merged declaration failed verification",
2369
- message: "The builder declared repo_diff: none (already-merged: " + am.sha + ") but " + am.sha + " is not an ancestor of main. The work is not on main; the phase is marked failed and the dispatcher will retry Build.",
2366
+ message: "The builder declared repo_diff: none (already-merged: " + am.sha + ") but " + am.sha + " is not an ancestor of the integration target. The work is not on the integration target; the phase is marked failed and the dispatcher will retry Build.",
2370
2367
  task_id: taskId
2371
2368
  }
2372
2369
  };
2373
2370
  }
2374
2371
  alreadyMergedSha = am.sha;
2375
- log("Build already-merged declaration verified: " + am.sha + " is an ancestor of main");
2372
+ log("Build already-merged declaration verified: " + am.sha + " is an ancestor of the integration target");
2376
2373
  }
2377
2374
  }
2378
2375
 
@@ -2401,7 +2398,7 @@ while (i < STEPS.length) {
2401
2398
  integrateVerifyOut = "";
2402
2399
  }
2403
2400
  if (/^VERIFIED:/m.test(integrateVerifyOut)) {
2404
- log("Integrate verified for task " + taskId + ": task branch tip is an ancestor of main");
2401
+ log("Integrate verified for task " + taskId + ": task branch tip is an ancestor of the integration target");
2405
2402
  } else {
2406
2403
  var integrateVerifyReason = integrateVerifyOut
2407
2404
  ? integrateVerifyOut.split("\n")[0].slice(0, 200)
@@ -2535,12 +2532,12 @@ while (i < STEPS.length) {
2535
2532
  }
2536
2533
  rejectionNotes = summary;
2537
2534
  // Already-merged corrective (room #16 blocker 11): when Review rejected
2538
- // an empty branch but the work is already on main (the workflow verified
2535
+ // an empty branch but the work is already on the integration target (the workflow verified
2539
2536
  // the sha), Wren must declare it — not re-implement or re-commit
2540
2537
  // already-landed work. Scoped to the empty-branch rejection; any other
2541
2538
  // rejection already carries its own specific notes.
2542
- if (step.name === "Review" && alreadyMergedSha && /no commits ahead of main/i.test(summary)) {
2543
- rejectionNotes += "\n\nCORRECTIVE (from the workflow, not the reviewer): the deliverable is already on main — the workflow mechanically verified that " + alreadyMergedSha + " is an ancestor of main. Do NOT re-implement the work and do NOT create a new commit for it. In your Build report, declare exactly: repo_diff: none (already-merged: " + alreadyMergedSha + ") — then end with VERDICT: PASS.";
2539
+ if (step.name === "Review" && alreadyMergedSha && /no commits ahead of (main|the integration target)/i.test(summary)) {
2540
+ rejectionNotes += "\n\nCORRECTIVE (from the workflow, not the reviewer): the deliverable is already on the integration target — the workflow mechanically verified that " + alreadyMergedSha + " is an ancestor of the integration target. Do NOT re-implement the work and do NOT create a new commit for it. In your Build report, declare exactly: repo_diff: none (already-merged: " + alreadyMergedSha + ") — then end with VERDICT: PASS.";
2544
2541
  log("Rework corrective appended for task " + taskId + ": already-merged " + alreadyMergedSha + " — Wren must declare, not rebuild");
2545
2542
  }
2546
2543
  i = BUILD_INDEX;