muse-crew 0.14.11 → 0.14.12

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.
@@ -988,7 +988,8 @@ cmd_integration_target() {
988
988
  integration_target
989
989
  }
990
990
 
991
- # Branch-state classification for Review entry (blocker 34, 2026-09-21).
991
+ # Branch-state classification for Review entry (blocker 34, 2026-09-21;
992
+ # no-branch path blocker 50, 2026-09-23).
992
993
  # Usage: classify-branch <task_id>
993
994
  # Stdout: exactly one successful line —
994
995
  # BRANCH_STATE: has-work | already-merged:<40-hex> | empty-no-work
@@ -1005,17 +1006,72 @@ cmd_integration_target() {
1005
1006
  # 1. current-tip identity — the live merge carrying the current branch
1006
1007
  # tip (current_branch_merge, blocker 35's identity primitive). If the
1007
1008
  # tip is on the live line via a merge, that merge IS the deliverable.
1009
+ # Branch-present path only — a gone branch has no tip to identify.
1008
1010
  # 2. the task's own durable merge records, newest first (at most 20
1009
1011
  # candidates scanned; garbage/unresolvable entries skipped). The
1010
1012
  # first record that resolves to a MERGE commit and is an ancestor of
1011
1013
  # the live target wins; the SHA is canonicalized to full 40-hex.
1012
1014
  # Poison rule: the newest RESOLVABLE non-ancestor record poisons the
1013
1015
  # fallback — an older record is never accepted beneath a newer known-
1014
- # stale one. The answer then is empty-no-work (stderr notes the poison),
1015
- # never a fabricated already-merged.
1016
+ # stale one. On the branch-present path the answer is empty-no-work
1017
+ # (stderr notes the poison), never a fabricated already-merged. On the
1018
+ # no-branch path the poison fails closed (exit 1): without a branch,
1019
+ # "empty" is indistinguishable from evidence loss.
1016
1020
  # The branch is empty here, so the blocker-35 identity/ancestry
1017
1021
  # distinction does not apply: there is no moved branch to go stale
1018
1022
  # against, and every candidate is this task's own record.
1023
+ #
1024
+ # No-branch path (blocker 50): a missing branch is the normal
1025
+ # post-integrate state — Integrate merges, the worktree lifecycle reclaims
1026
+ # the branch, and QA/rework run after. The classifier resolves the
1027
+ # integration target independently of the branch and runs the durable
1028
+ # merge-record scan (step 2 below). A hit prints already-merged:<sha> with
1029
+ # the real DIAG counts, exit 0. No hit — no records, none resolvable, none
1030
+ # an ancestor, or poisoned — exits 1 with diagnostics, never empty-no-work.
1031
+
1032
+ # Durable merge-record scan, shared by classify-branch's branch-present
1033
+ # and no-branch paths (blocker 50). Newest first, at most 20 candidates:
1034
+ # valid-hex merge_commit= lines only; a candidate must resolve to a MERGE
1035
+ # commit (^2 exists) to count as resolvable. The first resolvable record
1036
+ # that is an ancestor of $2 wins. Results in SCAN_MERGED (full 40-hex or
1037
+ # empty) and SCAN_POISONED (1 when the newest resolvable record is a
1038
+ # non-ancestor — an older ancestor beneath it is refused, stderr-noted).
1039
+ # Updates the caller's diag_records/diag_resolvable (dynamic scope; the
1040
+ # defaults keep set -u safe). No stdout of its own.
1041
+ scan_merge_records() {
1042
+ local task_id="$1" target="$2"
1043
+ SCAN_MERGED=""; SCAN_POISONED=0
1044
+ : "${diag_records:=0}" "${diag_resolvable:=0}"
1045
+ local rec="$MERGE_RECORDS_DIR/$task_id"
1046
+ [ -f "$rec" ] || return 0
1047
+ local count=0 sha full poisoned=""
1048
+ while IFS= read -r line && [ "$count" -lt 20 ]; do
1049
+ case "$line" in
1050
+ merge_commit=*) sha="${line#merge_commit=}" ;;
1051
+ *) continue ;;
1052
+ esac
1053
+ case "$sha" in ""|*[!0-9a-f]*) continue ;; esac
1054
+ count=$((count + 1))
1055
+ diag_records=$((diag_records + 1))
1056
+ full=$(git rev-parse --verify --quiet "$sha" 2>/dev/null) || continue
1057
+ # A merge commit: the frozen first-parent diff the workflow reviews
1058
+ # only exists for merges. A non-merge ancestor is never the task's
1059
+ # merge — skipped, not poison (garbage must not veto delivery).
1060
+ git rev-parse --verify --quiet "$full^2" >/dev/null 2>&1 || continue
1061
+ diag_resolvable=$((diag_resolvable + 1))
1062
+ if git merge-base --is-ancestor "$full" "$target" 2>/dev/null; then
1063
+ if [ -n "$poisoned" ]; then
1064
+ echo "classify-branch: newest resolvable record $poisoned is stale (not an ancestor of $target); refusing older record $full" >&2
1065
+ SCAN_POISONED=1
1066
+ return 0
1067
+ fi
1068
+ SCAN_MERGED="$full"
1069
+ return 0
1070
+ fi
1071
+ [ -z "$poisoned" ] && poisoned="$full"
1072
+ done < <(tac "$rec" 2>/dev/null)
1073
+ return 0
1074
+ }
1019
1075
  cmd_classify_branch() {
1020
1076
  local task_id="$1"
1021
1077
  validate_task_id "$task_id"
@@ -1035,7 +1091,29 @@ cmd_classify_branch() {
1035
1091
  branch=$(resolve_branch "$task_id") || { echo "classify-branch: cannot resolve task branch" >&2; diag; return 1; }
1036
1092
 
1037
1093
  cd "$REPO"
1038
- git rev-parse --verify "$branch" >/dev/null 2>&1 || { echo "classify-branch: branch $branch does not exist" >&2; diag; return 1; }
1094
+
1095
+ # Blocker 50: a missing branch is the normal post-integrate state, not a
1096
+ # classification failure — Integrate merges, the worktree lifecycle
1097
+ # reclaims the branch, and QA/rework run after. Resolve the target
1098
+ # independently of the branch and let the durable merge records plus
1099
+ # trunk ancestry decide. Never empty-no-work here: without a branch,
1100
+ # "empty" is indistinguishable from evidence loss.
1101
+ if ! git rev-parse --verify "$branch" >/dev/null 2>&1; then
1102
+ echo "classify-branch: branch $branch does not exist; scanning durable merge records" >&2
1103
+ local nb_target
1104
+ nb_target=$(integration_target) || { echo "classify-branch: cannot resolve integration target" >&2; diag; return 1; }
1105
+ scan_merge_records "$task_id" "$nb_target"
1106
+ if [ -n "$SCAN_MERGED" ]; then
1107
+ diag
1108
+ echo "BRANCH_STATE: already-merged:$SCAN_MERGED"
1109
+ return 0
1110
+ fi
1111
+ if [ "$SCAN_POISONED" != "1" ]; then
1112
+ echo "classify-branch: no merge record for $task_id is an ancestor of $nb_target" >&2
1113
+ fi
1114
+ diag
1115
+ return 1
1116
+ fi
1039
1117
 
1040
1118
  local target
1041
1119
  target=$(integration_target) || { echo "classify-branch: cannot resolve integration target" >&2; diag; return 1; }
@@ -1059,36 +1137,13 @@ cmd_classify_branch() {
1059
1137
  fi
1060
1138
  fi
1061
1139
 
1062
- # 2. Durable merge records, newest first. Empty-file-safe: no pipelines
1063
- # under set -e/pipefail, at most 20 candidate lines read.
1064
- local rec="$MERGE_RECORDS_DIR/$task_id"
1065
- if [ -f "$rec" ]; then
1066
- local count=0 sha full poisoned=""
1067
- while IFS= read -r line && [ "$count" -lt 20 ]; do
1068
- case "$line" in
1069
- merge_commit=*) sha="${line#merge_commit=}" ;;
1070
- *) continue ;;
1071
- esac
1072
- case "$sha" in ""|*[!0-9a-f]*) continue ;; esac
1073
- count=$((count + 1))
1074
- diag_records=$((diag_records + 1))
1075
- full=$(git rev-parse --verify --quiet "$sha" 2>/dev/null) || continue
1076
- # A merge commit: the frozen first-parent diff the workflow reviews
1077
- # only exists for merges. A non-merge ancestor is never the task's
1078
- # merge — skipped, not poison (garbage must not veto delivery).
1079
- git rev-parse --verify --quiet "$full^2" >/dev/null 2>&1 || continue
1080
- diag_resolvable=$((diag_resolvable + 1))
1081
- if git merge-base --is-ancestor "$full" "$target" 2>/dev/null; then
1082
- if [ -n "$poisoned" ]; then
1083
- echo "classify-branch: newest resolvable record $poisoned is stale (not an ancestor of $target); refusing older record $full" >&2
1084
- break
1085
- fi
1086
- diag
1087
- echo "BRANCH_STATE: already-merged:$full"
1088
- return 0
1089
- fi
1090
- [ -z "$poisoned" ] && poisoned="$full"
1091
- done < <(tac "$rec" 2>/dev/null)
1140
+ # 2. Durable merge records, newest first (scan_merge_records; blocker 50
1141
+ # shares this scan with the no-branch path).
1142
+ scan_merge_records "$task_id" "$target"
1143
+ if [ -n "$SCAN_MERGED" ]; then
1144
+ diag
1145
+ echo "BRANCH_STATE: already-merged:$SCAN_MERGED"
1146
+ return 0
1092
1147
  fi
1093
1148
 
1094
1149
  diag
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muse-crew",
3
- "version": "0.14.11",
3
+ "version": "0.14.12",
4
4
  "description": "Opinionated orchestration for Muse — workflows, identities, and tooling for autonomous software development.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,