valent-pipeline 0.19.21 → 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
|
@@ -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, () =>
|
|
1789
|
-
|
|
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, () =>
|
|
1859
|
-
|
|
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
|
|
2098
|
-
|
|
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
|
package/src/lib/crosscheck.js
CHANGED
|
@@ -55,11 +55,25 @@ export function crosscheckVerdict({ reported = null, bugsBlock = null, assertVer
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
const claim = (field) => reported && reported[field] !== undefined && reported[field] !== null;
|
|
58
|
+
// Counts of BAD things (failures, open blocking bugs): a self-report HIGHER than the machine
|
|
59
|
+
// recount is the SAFE, conservative direction — the gate was harder on itself than reality (e.g. a
|
|
60
|
+
// JUDGE that counted a pre-existing, baseline-excluded failure as testsFailed/openP1toP3). That is
|
|
61
|
+
// NOT the integrity breach this module exists to catch — a gate HIDING failures/bugs to sneak a
|
|
62
|
+
// ship is claimed < actual — and refusing it stranded genuinely shippable stories. So an
|
|
63
|
+
// over-report on these fields reconciles to the authoritative recount with a warning; an
|
|
64
|
+
// under-report stays a hard mismatch. (Prompt tightening alone did not hold — the LLM still
|
|
65
|
+
// over-counts; this is the mechanical backstop.) Good-thing counts (testsPassed) and exact-match
|
|
66
|
+
// fields keep strict equality, where over-claiming IS the unsafe direction.
|
|
67
|
+
const SAFE_IF_OVER_REPORTED = new Set(['testsFailed', 'openP1toP3']);
|
|
58
68
|
const compare = (field, actual) => {
|
|
59
69
|
recounted[field] = actual;
|
|
60
|
-
if (claim(field)
|
|
61
|
-
|
|
70
|
+
if (!claim(field) || reported[field] === actual) return;
|
|
71
|
+
if (SAFE_IF_OVER_REPORTED.has(field) && typeof reported[field] === 'number'
|
|
72
|
+
&& typeof actual === 'number' && reported[field] > actual) {
|
|
73
|
+
warnings.push(`${field}: self-reported ${reported[field]} but artifacts show ${actual} — over-report (safe, conservative direction), reconciled to the recount`);
|
|
74
|
+
return;
|
|
62
75
|
}
|
|
76
|
+
mismatches.push({ field, claimed: reported[field], actual });
|
|
63
77
|
};
|
|
64
78
|
|
|
65
79
|
// Test counts — from the assert verdict (machine-derived twice over).
|
package/src/lib/git-flow.js
CHANGED
|
@@ -402,7 +402,19 @@ export function startStory({ root, story, targetBranch = '', prefix = 'story/',
|
|
|
402
402
|
snapshotSha = snap.sha || null;
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
-
|
|
405
|
+
// An inline fix (--from-story) inherits its SOURCE story's recorded target — NOT the
|
|
406
|
+
// currently-checked-out branch. Running several fixes for one story back-to-back leaves HEAD on the
|
|
407
|
+
// previous fix's branch; defaulting `target` to onBranch made each later fix target (and, via the
|
|
408
|
+
// base check below, base on) its SIBLING fix branch — so the fixes silently chained onto the first
|
|
409
|
+
// fix's branch instead of the story, stranded off the real target (2026-06-14). An explicit
|
|
410
|
+
// --target still wins; onBranch stays the fallback for a plain story or a fix whose source has no
|
|
411
|
+
// flow record yet. With the right target, the base check (`!isMergedInto(source, target)`) also
|
|
412
|
+
// resolves correctly — the source story is unmerged into the REAL target, so the fix bases on it.
|
|
413
|
+
let inheritedTarget = null;
|
|
414
|
+
if (fromStory && !targetBranch) {
|
|
415
|
+
inheritedTarget = readFlowRecord(root, fromStory)?.target || null;
|
|
416
|
+
}
|
|
417
|
+
const target = targetBranch || inheritedTarget || onBranch;
|
|
406
418
|
let base = target;
|
|
407
419
|
let basedOnStory = false;
|
|
408
420
|
if (fromStory) {
|