valent-pipeline 0.19.22 → 0.19.23

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.22",
3
+ "version": "0.19.23",
4
4
  "description": "v3 multi-agent AI pipeline for software development lifecycle",
5
5
  "type": "module",
6
6
  "bin": {
@@ -837,6 +837,13 @@ const gitBranchPrefix = (typeof (gitCfg.storyBranchPrefix ?? gitCfg.story_branch
837
837
  // plain ref-safe names stay bare so the composed commands read naturally in prompts.
838
838
  const refq = (s) => (/^[A-Za-z0-9._/-]+$/.test(s) ? s : shq(s))
839
839
  const gitFlags = (gitTargetBranch ? ` --target ${refq(gitTargetBranch)}` : '') + (gitBranchPrefix !== 'story/' ? ` --prefix ${refq(gitBranchPrefix)}` : '')
840
+ // Ship-merge retry cap (review 2026-06-14): how many times a non-landed, non-conflict ship-story
841
+ // result (a transient GIT-runner failure — the haiku transcriber dying / returning null, a momentary
842
+ // index lock) is re-attempted before the story is declared merge-blocked. The merge is deterministic
843
+ // and ship-story is idempotent (an already-merged branch returns already:true), so retrying is always
844
+ // safe. A single hiccup used to drop a JUDGE-approved story to merge-blocked → requeued a whole sprint
845
+ // later, silently. Default 3 (1 + 2 retries); pipeline-config.yaml git.ship_merge_attempts overrides.
846
+ const shipMergeAttempts = Math.max(1, Number.isInteger(gitCfg.shipMergeAttempts ?? gitCfg.ship_merge_attempts) ? (gitCfg.shipMergeAttempts ?? gitCfg.ship_merge_attempts) : 3)
840
847
  const gitCommitCmd = (sid, phase, extra = '') =>
841
848
  `node .valent-pipeline/bin/cli.js git commit-phase --story ${sid} --phase ${phase}${extra}`
842
849
 
@@ -1423,6 +1430,16 @@ phase('Backlog')
1423
1430
  // outcomes) is also REQUEUED to `groomed` — the sweep that closes the `sprint-planned` black hole
1424
1431
  // (review #16): nothing the plan tagged may end the sprint still in-flight.
1425
1432
  const isMergeBlocked = (r) => r.verdict === 'blocked' && (r.reason === 'merge-conflict' || r.reason === 'git-ship-failed')
1433
+ // Surface merge-blocked stories LOUDLY (review 2026-06-14): a JUDGE-approved story whose merge did
1434
+ // not land is requeued to groomed (correct — an infra outcome, not a quality reject), but that
1435
+ // requeue used to be silent: a run could end with a green-looking ledger while a shipped-on-paper
1436
+ // story sat un-integrated and a later story merged ahead of it. Name them in the summary + return.
1437
+ const mergeBlocked = results
1438
+ .filter((r) => !r.resumedSkip && isMergeBlocked(r))
1439
+ .map((r) => ({ storyId: r.storyId, reason: r.reason }))
1440
+ if (mergeBlocked.length) {
1441
+ log(`⚠ MERGE-BLOCKED: ${mergeBlocked.length} JUDGE-approved stor${mergeBlocked.length === 1 ? 'y' : 'ies'} did NOT reach the target after ${shipMergeAttempts} ship attempt(s) and ${mergeBlocked.length === 1 ? 'was' : 'were'} requeued to groomed for re-ship — ${mergeBlocked.map((m) => `${m.storyId} (${m.reason})`).join(', ')}. NOT lost, NOT integrated: a follow-up sprint re-ships from the retained branch, or ship manually.`)
1442
+ }
1426
1443
  const backlog = await persistBacklog(
1427
1444
  results.filter((r) => r.shipped),
1428
1445
  results.filter((r) => r.partial),
@@ -1439,6 +1456,7 @@ return {
1439
1456
  results,
1440
1457
  integration,
1441
1458
  post_merge_findings: postMergeFindings,
1459
+ merge_blocked: mergeBlocked,
1442
1460
  backlog,
1443
1461
  }
1444
1462
 
@@ -1612,6 +1630,25 @@ async function runGitStep(sid, commands, label, stepPhase, dirPhrase = 'the proj
1612
1630
  return result
1613
1631
  }
1614
1632
 
1633
+ // runShipStep: runGitStep specialized for the terminal ship-story merge, with a retry (review
1634
+ // 2026-06-14). A single transient GIT-runner failure (null/agent-death, or a non-landed non-conflict
1635
+ // result) used to drop a JUDGE-approved story straight to merge-blocked — requeued a whole sprint
1636
+ // later, with the only trace a mid-run log line. ship-story is deterministic and idempotent (an
1637
+ // already-merged branch returns already:true; a never-merged one re-attempts the same merge), so a
1638
+ // non-terminal result is re-run up to shipMergeAttempts. A REAL conflict (conflict===true) is NOT
1639
+ // retried — a blind re-merge cannot clear it; that needs the --reship sync/resolve path.
1640
+ async function runShipStep(sid, commandStr, stepPhase) {
1641
+ let res = null
1642
+ for (let attempt = 1; attempt <= shipMergeAttempts; attempt++) {
1643
+ res = await runGitStep(sid, [commandStr], 'ship', stepPhase)
1644
+ if (res && (res.merged === true || res.skipped === true || res.conflict === true)) break
1645
+ if (attempt < shipMergeAttempts) {
1646
+ log(`${sid}: ship-story attempt ${attempt}/${shipMergeAttempts} did not land (${res ? 'transient git-runner failure' : 'GIT runner died'}) — retrying the deterministic, idempotent merge`)
1647
+ }
1648
+ }
1649
+ return res
1650
+ }
1651
+
1615
1652
  // ===========================================================================
1616
1653
  // runStory: the per-story pipeline (kept inline, not a nested workflow(), so the single
1617
1654
  // workflow() nesting level stays free for plan/retro — see reimplementation-plan §5b).
@@ -1785,8 +1822,8 @@ async function runStory(story) {
1785
1822
  }
1786
1823
  } else if (sync && (sync.synced === true || sync.skipped === true)) {
1787
1824
  // Clean (or non-repo skip): the standing JUDGE verdict still covers the tree — ship directly.
1788
- const ship = await withShipTurn(storyId, () => runGitStep(storyId,
1789
- [`node .valent-pipeline/bin/cli.js git ship-story --story ${storyId} --verdict ship${gitFlags}`], 'ship', 'QA'))
1825
+ const ship = await withShipTurn(storyId, () => runShipStep(storyId,
1826
+ `node .valent-pipeline/bin/cli.js git ship-story --story ${storyId} --verdict ship${gitFlags}`, 'QA'))
1790
1827
  const merged = ship && (ship.skipped === true || ship.merged === true)
1791
1828
  if (merged) {
1792
1829
  recordStage('ship', { verdict: 'pass' })
@@ -1855,8 +1892,8 @@ async function runStory(story) {
1855
1892
  if (gitFlowEnabled) {
1856
1893
  if (shipped || shippedPartial) {
1857
1894
  const mergeVerdict = shippedPartial ? 'ship-partial' : decision.verdict === 'ship-with-bugs' ? 'ship-with-bugs' : 'ship'
1858
- const ship = await withShipTurn(storyId, () => runGitStep(storyId,
1859
- [`node .valent-pipeline/bin/cli.js git ship-story --story ${storyId} --verdict ${mergeVerdict}${gitFlags}`], 'ship', 'Judge'))
1895
+ const ship = await withShipTurn(storyId, () => runShipStep(storyId,
1896
+ `node .valent-pipeline/bin/cli.js git ship-story --story ${storyId} --verdict ${mergeVerdict}${gitFlags}`, 'Judge'))
1860
1897
  const merged = ship && (ship.skipped === true || ship.merged === true)
1861
1898
  if (!merged) {
1862
1899
  const reason = ship && ship.conflict ? 'merge-conflict' : 'git-ship-failed'
@@ -2094,8 +2131,8 @@ async function runStory(story) {
2094
2131
  // Standalone bugs (no sourceStory — defects in already-shipped code) merge to target as before.
2095
2132
  const intoFlag = itemType === 'bug' && sourceStory ? ` --into ${gitBranchPrefix}${sourceStory}` : ''
2096
2133
  const ship = await withShipTurn(storyId, async () => {
2097
- const res = await runGitStep(storyId,
2098
- [`node .valent-pipeline/bin/cli.js git ship-story --story ${storyId} --verdict ${mergeVerdict}${intoFlag}${wtFlag}${gitFlags}`], 'ship', 'Judge')
2134
+ const res = await runShipStep(storyId,
2135
+ `node .valent-pipeline/bin/cli.js git ship-story --story ${storyId} --verdict ${mergeVerdict}${intoFlag}${wtFlag}${gitFlags}`, 'Judge')
2099
2136
  // Post-merge acceptance verify (parallel mode + atdd armed): the story was QA'd on its
2100
2137
  // PRE-merge SHA; other stories' merges may have landed since. Re-run its acceptance suite
2101
2138
  // against the merged target — still under the mutex, so nothing else moves the target