valent-pipeline 0.19.49 → 0.19.51

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.49",
3
+ "version": "0.19.51",
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 {
@@ -337,6 +370,19 @@ export function readFlowRecord(root, story) {
337
370
  }
338
371
  }
339
372
 
373
+ // When the flow record isn't in the run context's working tree, read it from a git ref. A --from-story
374
+ // bug's git-flow.json is committed only on its own branch (evidence-on-branch — the class the .50 fixed
375
+ // for the backlog entry), so story-status run from a context that lacks it (the post-ship verify on
376
+ // build/v1) would otherwise read None. Returns null on any failure (missing path/ref) — callers fall
377
+ // back to their prior behavior.
378
+ function readFlowRecordFromRef(root, ref, story) {
379
+ try {
380
+ return JSON.parse(git(root, ['show', `${ref}:stories/${story}/evidence/git-flow.json`]));
381
+ } catch {
382
+ return null;
383
+ }
384
+ }
385
+
340
386
  function writeFlowRecord(root, story, record) {
341
387
  const dir = join(root, 'stories', story, 'evidence');
342
388
  mkdirSync(dir, { recursive: true });
@@ -475,8 +521,15 @@ export function startStory({ root, story, targetBranch = '', prefix = 'story/',
475
521
  buildCommitMessage({ storyId: story, phase: 'sync', summary: `sync ${target} into ${branch} (inherit shared state from a behind base)` }), target]);
476
522
  refreshedFromTarget = true;
477
523
  } catch {
478
- if (mergeInProgress(wtPath)) git(wtPath, ['merge', '--abort']);
479
- refreshConflict = true;
524
+ // A conflict ONLY on target-authoritative shared state (the backlog with the filed bug entry)
525
+ // resolve toward the target so the entry reaches this branch (M-65/M-69); any real-code
526
+ // conflict aborts cleanly (setupWorktree re-materializes the install if the sync removed it).
527
+ if (resolveRefreshConflict(wtPath)) {
528
+ refreshedFromTarget = true;
529
+ } else {
530
+ if (mergeInProgress(wtPath)) git(wtPath, ['merge', '--abort']);
531
+ refreshConflict = true;
532
+ }
480
533
  }
481
534
  }
482
535
  const setup = setupWorktree(root, wtPath, setupCommands);
@@ -533,8 +586,16 @@ export function startStory({ root, story, targetBranch = '', prefix = 'story/',
533
586
  buildCommitMessage({ storyId: story, phase: 'sync', summary: `sync ${target} into ${branch} (inherit shared state from a behind base)` }), target]);
534
587
  refreshedFromTarget = true;
535
588
  } catch {
536
- if (mergeInProgress(root)) git(root, ['merge', '--abort']);
537
- refreshConflict = true;
589
+ // A conflict ONLY on target-authoritative shared state (the backlog the disposition filed the
590
+ // bug entry into) is resolved toward the target so the entry reaches this branch (M-65/M-69); a
591
+ // conflict touching any real code aborts cleanly as before (degraded, but no worse). The M-52
592
+ // framework-deletion guard below still runs on the concluded merge.
593
+ if (resolveRefreshConflict(root)) {
594
+ refreshedFromTarget = true;
595
+ } else {
596
+ if (mergeInProgress(root)) git(root, ['merge', '--abort']);
597
+ refreshConflict = true;
598
+ }
538
599
  }
539
600
  // M-52 transition hazard: when `target` UNTRACKS the vendored framework (the post-untrack install
540
601
  // model) while THIS branch still TRACKS it (a pre-untrack branch), the merge above applied target's
@@ -581,7 +642,13 @@ export function storyStatus({ root, story, targetBranch = '', prefix = 'story/'
581
642
  if (!isRepoWithCommits(root)) return NOT_REPO;
582
643
  const branch = storyBranchName(prefix, story);
583
644
  const exists = branchExists(root, branch);
584
- const record = readFlowRecord(root, story);
645
+ // Evidence-on-branch (M-90/M-91): a --from-story bug's git-flow.json is committed on its OWN branch,
646
+ // so a story-status run from a context lacking it (the post-ship verify on build/v1) reads None and
647
+ // defaults the target to build/v1 — a bug merged --into its SOURCE then reads merged:false (a FALSE
648
+ // git-ship-failed that strands the parent's revalidation). Fall back to the branch ref so basedOnStory
649
+ // still resolves (the .49 authority can only fire if the record is readable). Mirrors the .50 backlog
650
+ // branch-resolution. The record CONTENT is identical to the local copy — only the read source differs.
651
+ const record = readFlowRecord(root, story) || (exists ? readFlowRecordFromRef(root, branch, story) : null);
585
652
  // A `--from-story` item (an inline bug fix) ships `--into` its SOURCE story branch (recorded as
586
653
  // basedOnStory), NOT the sprint target — so "did it land" is ancestry against basedOnStory. The sprint
587
654
  // `--target` that the post-ship verify passes globally (gitFlags `--target`) is the wrong base for such