valent-pipeline 0.19.48 → 0.19.50

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "valent-pipeline",
3
- "version": "0.19.48",
3
+ "version": "0.19.50",
4
4
  "description": "v3 multi-agent AI pipeline for software development lifecycle",
5
5
  "type": "module",
6
6
  "bin": {
@@ -124,6 +124,39 @@ function mergeInProgress(root) {
124
124
  }
125
125
  }
126
126
 
127
+ // The TARGET is the source of truth for these CLI-owned shared-state files: the disposition files a
128
+ // rejected story's bug entries into `pipeline-backlog.yaml` on the target at sprint end (.38
129
+ // commit-at-persist). A reused --from-story branch carries an OLDER copy, so the refresh merge below
130
+ // conflicts on it.
131
+ const REFRESH_TARGET_WINS = new Set(['pipeline-backlog.yaml']);
132
+
133
+ // On a refresh-merge conflict, if EVERY conflicted path is a target-authoritative shared-state file
134
+ // (REFRESH_TARGET_WINS), resolve it toward the TARGET ("theirs" in a merge run ON the branch is the
135
+ // target being merged in) and CONCLUDE the merge — the branch makes no legitimate edits of its own to
136
+ // these files, so target-wins delivers the filed bug entry and loses nothing. ANY other conflicted
137
+ // path (real code) → return false and let the caller abort exactly as before. Returns true iff it
138
+ // resolved + committed. Closes M-65/M-69: a backlog conflict used to ABORT the refresh, so the filed
139
+ // bug entry never reached the branch and `backlog ship --story <bug>` then errored "not found" →
140
+ // manual reconcile every bug-fix sprint. The ship CLAMP is untouched — evidence still verifies against
141
+ // the branch's own tree; this only delivers the backlog ENTRY the target already committed.
142
+ function resolveRefreshConflict(dir) {
143
+ if (!mergeInProgress(dir)) return false; // a non-conflict merge failure — the caller aborts
144
+ let conflicted;
145
+ try {
146
+ conflicted = git(dir, ['diff', '--name-only', '--diff-filter=U'])
147
+ .split('\n').map((s) => s.trim()).filter(Boolean);
148
+ } catch {
149
+ return false;
150
+ }
151
+ if (!conflicted.length || !conflicted.every((p) => REFRESH_TARGET_WINS.has(p))) return false;
152
+ for (const p of conflicted) {
153
+ git(dir, ['checkout', '--theirs', '--', p]);
154
+ git(dir, ['add', '--', p]);
155
+ }
156
+ git(dir, ['commit', '--no-edit']);
157
+ return true;
158
+ }
159
+
127
160
  /** Is `branch` already merged into `target` (its tip an ancestor of target's tip)? */
128
161
  function isMergedInto(root, branch, target) {
129
162
  try {
@@ -475,8 +508,15 @@ export function startStory({ root, story, targetBranch = '', prefix = 'story/',
475
508
  buildCommitMessage({ storyId: story, phase: 'sync', summary: `sync ${target} into ${branch} (inherit shared state from a behind base)` }), target]);
476
509
  refreshedFromTarget = true;
477
510
  } catch {
478
- if (mergeInProgress(wtPath)) git(wtPath, ['merge', '--abort']);
479
- refreshConflict = true;
511
+ // A conflict ONLY on target-authoritative shared state (the backlog with the filed bug entry)
512
+ // resolve toward the target so the entry reaches this branch (M-65/M-69); any real-code
513
+ // conflict aborts cleanly (setupWorktree re-materializes the install if the sync removed it).
514
+ if (resolveRefreshConflict(wtPath)) {
515
+ refreshedFromTarget = true;
516
+ } else {
517
+ if (mergeInProgress(wtPath)) git(wtPath, ['merge', '--abort']);
518
+ refreshConflict = true;
519
+ }
480
520
  }
481
521
  }
482
522
  const setup = setupWorktree(root, wtPath, setupCommands);
@@ -533,8 +573,16 @@ export function startStory({ root, story, targetBranch = '', prefix = 'story/',
533
573
  buildCommitMessage({ storyId: story, phase: 'sync', summary: `sync ${target} into ${branch} (inherit shared state from a behind base)` }), target]);
534
574
  refreshedFromTarget = true;
535
575
  } catch {
536
- if (mergeInProgress(root)) git(root, ['merge', '--abort']);
537
- refreshConflict = true;
576
+ // A conflict ONLY on target-authoritative shared state (the backlog the disposition filed the
577
+ // bug entry into) is resolved toward the target so the entry reaches this branch (M-65/M-69); a
578
+ // conflict touching any real code aborts cleanly as before (degraded, but no worse). The M-52
579
+ // framework-deletion guard below still runs on the concluded merge.
580
+ if (resolveRefreshConflict(root)) {
581
+ refreshedFromTarget = true;
582
+ } else {
583
+ if (mergeInProgress(root)) git(root, ['merge', '--abort']);
584
+ refreshConflict = true;
585
+ }
538
586
  }
539
587
  // M-52 transition hazard: when `target` UNTRACKS the vendored framework (the post-untrack install
540
588
  // model) while THIS branch still TRACKS it (a pre-untrack branch), the merge above applied target's
@@ -582,7 +630,14 @@ export function storyStatus({ root, story, targetBranch = '', prefix = 'story/'
582
630
  const branch = storyBranchName(prefix, story);
583
631
  const exists = branchExists(root, branch);
584
632
  const record = readFlowRecord(root, story);
585
- const target = targetBranch || record?.target || currentBranch(root);
633
+ // A `--from-story` item (an inline bug fix) ships `--into` its SOURCE story branch (recorded as
634
+ // basedOnStory), NOT the sprint target — so "did it land" is ancestry against basedOnStory. The sprint
635
+ // `--target` that the post-ship verify passes globally (gitFlags `--target`) is the wrong base for such
636
+ // an item: it reads a bug correctly merged into its source — but whose source hasn't reached the sprint
637
+ // target yet — as merged:false → a FALSE git-ship-failed that stranded the parent's revalidation
638
+ // (M-60, 2.17-BUG-002). basedOnStory is therefore AUTHORITATIVE when present (it overrides even an
639
+ // explicit --target); an ordinary story (basedOnStory null) still honors --target then the record.
640
+ const target = record?.basedOnStory || targetBranch || record?.target || currentBranch(root);
586
641
  // A missing branch is UNKNOWN (merged:null), never a verdict: the flow retains branches on
587
642
  // both ship and reject, so absence means the flow never ran here (or someone pruned by hand) —
588
643
  // the caller must treat the state as unverified rather than as landed.