valent-pipeline 0.19.22 → 0.19.24

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.24",
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'
@@ -1993,11 +2030,30 @@ async function runStory(story) {
1993
2030
  }
1994
2031
  }
1995
2032
 
2033
+ // Shift-left pre-handoff self-gate (review 2026-06-15): the dev fan-out used to hand off WITHOUT
2034
+ // knowing which deterministic checks the pre-CRITIC STATIC gate would run the instant it did — so
2035
+ // lint/type failures and un-waived test skips reached the gate and cost a full rework round-trip
2036
+ // (a re-spawn), the dominant avoidable-rework pattern in the field (a soft correction-directive
2037
+ // reduced it but did not close it). Thread the gate's OWN checks into the build prompt so the dev
2038
+ // runs them IN its build turn (same context, no re-spawn) and hands off clean. The gate
2039
+ // (runPreCriticGate) stays the mechanical backstop; this just makes it pass first-try, cutting both
2040
+ // the round-trip cost and the rework-cycle count. Same preCriticChecks the gate runs; the
2041
+ // skip-waiver / spec-conformance rules are always named. Static + args only => journal-replay safe.
2042
+ const preHandoffStaticCmds = preCriticChecks.map((c) => subCmd(c, storyId, effectiveWorkspaces))
2043
+ const preHandoffNote = (preHandoffStaticCmds.length || specConformanceEnabled || traceCheckEnabled)
2044
+ ? '\n\n## Definition of done — run these BEFORE you write your handoff\n' +
2045
+ 'The deterministic pre-CRITIC gate runs the moment you hand off; a failure costs a full rework cycle (a re-spawn). Pass them first-try:\n' +
2046
+ preHandoffStaticCmds.map((c) => ` - \`${c}\` — must exit 0 (lint / type / static).\n`).join('') +
2047
+ ' - Every deliberately skipped or pending test MUST carry an inline `// valent-waiver: <reason>` (on the skip line or in the test name) — an un-waived skip is rejected by the spec-conformance check.\n' +
2048
+ ' - Do NOT drop a spec\'d assertion target or omit a spec\'d case to make a suite pass.\n' +
2049
+ 'Run them locally, fix EVERYTHING they flag, and only then write your handoff — this is part of the build, not the gate\'s job to find for you.'
2050
+ : ''
2051
+
1996
2052
  phase('Build')
1997
2053
  // Genuine barrier: CRITIC needs ALL active dev agents' work before reviewing.
1998
2054
  const builds = await parallel(
1999
2055
  devTasks.map((t) => () =>
2000
- spawn(t.agent, `${t.agent.toLowerCase()}.md`, (t.subject || 'Implement production code and tests per the brief and test spec.') + atddReadOnlyNote, {
2056
+ spawn(t.agent, `${t.agent.toLowerCase()}.md`, (t.subject || 'Implement production code and tests per the brief and test spec.') + atddReadOnlyNote + preHandoffNote, {
2001
2057
  label: `build:${t.ref}:${storyId}`,
2002
2058
  phase: 'Build',
2003
2059
  })),
@@ -2094,8 +2150,8 @@ async function runStory(story) {
2094
2150
  // Standalone bugs (no sourceStory — defects in already-shipped code) merge to target as before.
2095
2151
  const intoFlag = itemType === 'bug' && sourceStory ? ` --into ${gitBranchPrefix}${sourceStory}` : ''
2096
2152
  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')
2153
+ const res = await runShipStep(storyId,
2154
+ `node .valent-pipeline/bin/cli.js git ship-story --story ${storyId} --verdict ${mergeVerdict}${intoFlag}${wtFlag}${gitFlags}`, 'Judge')
2099
2155
  // Post-merge acceptance verify (parallel mode + atdd armed): the story was QA'd on its
2100
2156
  // PRE-merge SHA; other stories' merges may have landed since. Re-run its acceptance suite
2101
2157
  // against the merged target — still under the mutex, so nothing else moves the target