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
|
@@ -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'
|
|
@@ -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
|
|
2098
|
-
|
|
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
|