valent-pipeline 0.19.16 → 0.19.22
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
|
@@ -226,6 +226,7 @@ const JUDGE_DECISION_SCHEMA = {
|
|
|
226
226
|
description: { type: 'string' },
|
|
227
227
|
file_ref: { type: 'string' }, // e.g. bugs.md#BUG-002
|
|
228
228
|
category: { type: 'string' }, // e.g. pre-existing | minor
|
|
229
|
+
preExisting: { type: 'boolean' }, // mirrors bugs.md valent:bugs preExisting — a pre-existing carried bug is NON-blocking (the ship clamp's isPre must match crosscheck.js: preExisting===true || category==='pre-existing')
|
|
229
230
|
},
|
|
230
231
|
},
|
|
231
232
|
},
|
|
@@ -1568,9 +1569,31 @@ async function runIntegrationGate(shipped) {
|
|
|
1568
1569
|
// STATIC/EVIDENCE/BACKLOG). Returns the LAST command's JSON line, or null if the agent died;
|
|
1569
1570
|
// callers that depend on the outcome (ship-story) must check `merged === true` explicitly.
|
|
1570
1571
|
// No-op (returns null without spawning) when the git flow is disabled.
|
|
1572
|
+
//
|
|
1573
|
+
// The GIT runner is a cheap (haiku) agent that TRANSCRIBES the CLI's JSON line into a typed-free
|
|
1574
|
+
// StructuredOutput ({additionalProperties:true}). On Windows/haiku it routinely STRINGIFIES the
|
|
1575
|
+
// values — `merged:true` → `merged:"true"`, `changedFiles:[...]` → `changedFiles:"[...]"` — which
|
|
1576
|
+
// silently broke every strict `ship.merged === true` / `.conflict === true` check downstream and
|
|
1577
|
+
// reported `git-ship-failed` on merges that ACTUALLY LANDED (false reject; root of repeated manual
|
|
1578
|
+
// ship reconciles). Git is deterministic — the CLI's real types are the truth; coerce the
|
|
1579
|
+
// transcription back. "true"/"false" strings → booleans (no git result field is legitimately the
|
|
1580
|
+
// string "true"/"false"); a string that is a JSON array → the array (changedFiles/conflictFiles/files).
|
|
1581
|
+
function normalizeGitResult(r) {
|
|
1582
|
+
if (!r || typeof r !== 'object' || Array.isArray(r)) return r
|
|
1583
|
+
const out = {}
|
|
1584
|
+
for (const [k, v] of Object.entries(r)) {
|
|
1585
|
+
if (v === 'true') out[k] = true
|
|
1586
|
+
else if (v === 'false') out[k] = false
|
|
1587
|
+
else if (typeof v === 'string' && /^\s*\[[\s\S]*\]\s*$/.test(v)) {
|
|
1588
|
+
try { out[k] = JSON.parse(v) } catch { out[k] = v }
|
|
1589
|
+
} else out[k] = v
|
|
1590
|
+
}
|
|
1591
|
+
return out
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1571
1594
|
async function runGitStep(sid, commands, label, stepPhase, dirPhrase = 'the project root') {
|
|
1572
1595
|
if (!gitFlowEnabled) return null
|
|
1573
|
-
const result = await agent(
|
|
1596
|
+
const result = normalizeGitResult(await agent(
|
|
1574
1597
|
[
|
|
1575
1598
|
`You are **GIT**, the deterministic git-flow runner for story ${sid} in the valent-pipeline.`,
|
|
1576
1599
|
`Run each command below from ${dirPhrase}, in order, exactly as written. Do NOT compose any`,
|
|
@@ -1584,7 +1607,7 @@ async function runGitStep(sid, commands, label, stepPhase, dirPhrase = 'the proj
|
|
|
1584
1607
|
`return its JSON line (it carries ok:false and the failure detail).`,
|
|
1585
1608
|
].join('\n'),
|
|
1586
1609
|
{ label: `git:${label}:${sid}`, phase: stepPhase, schema: { type: 'object', additionalProperties: true }, model: modelFor('GIT') },
|
|
1587
|
-
)
|
|
1610
|
+
))
|
|
1588
1611
|
if (result && result.skipped === true) log(`${sid}/GIT: ${label} skipped — not a git repository`)
|
|
1589
1612
|
return result
|
|
1590
1613
|
}
|
|
@@ -2712,7 +2735,7 @@ async function runStory(story) {
|
|
|
2712
2735
|
`\`evidence assert\` + \`trace check\` (gate verdict: ${evGate.verdict}${evGate.escalated ? ', escalated after the rework cap' : ''}${evGate.advisory ? ', advisory' : ''}). ` +
|
|
2713
2736
|
`Read the NEWEST JSON under \`stories/${sid}/evidence/asserts/\`` +
|
|
2714
2737
|
`${evGate.verdictPath ? ` (the gate reported \`${evGate.verdictPath}\`)` : ''} and TRANSCRIBE testsPassed/testsFailed/openP1toP3 ` +
|
|
2715
|
-
`from its counts (openP1toP3
|
|
2738
|
+
`from its counts (openP1toP3 = bugs in the \`valent:bugs\` block of bugs.md with status open/fix-in-progress AND priority P1/P2/P3, EXCLUDING any with \`preExisting: true\` or \`category: pre-existing\` — baselined pre-existing bugs are non-blocking and the cross-check counts them separately, so including them here produces a verdict-integrity mismatch and refuses the ship) — do NOT count from execution-report.md prose. ` +
|
|
2716
2739
|
`Set evidenceVerdict to the assert verdict and gitSha to the evidence run's SHA. Your job on numbers is ANOMALY ` +
|
|
2717
2740
|
`DETECTION: FLAG as a discrepancy any claim in execution-report.md / traceability-matrix.md / bugs.md that the ` +
|
|
2718
2741
|
`evidence JSON does not support (counts, suites, commands, SHA, pre-existing exclusions).` +
|
|
@@ -2775,8 +2798,12 @@ async function runStory(story) {
|
|
|
2775
2798
|
'resolved, every AC covered, no assertion weakening) but bugs.md still has open NON-BLOCKING bugs: ' +
|
|
2776
2799
|
'open P4s and pre-existing failures (file-bugs.md §8b). The story ships, but every such bug MUST ' +
|
|
2777
2800
|
'carry to the backlog. List each one in `bugs` (priority, severity, affects, description, ' +
|
|
2778
|
-
'file_ref like bugs.md#BUG-00n, category).
|
|
2779
|
-
'
|
|
2801
|
+
'file_ref like bugs.md#BUG-00n, category, and `preExisting`). Set `preExisting: true` on any carried ' +
|
|
2802
|
+
'bug that is pre-existing in bugs.md (preExisting:true or category pre-existing) — KEEP its real ' +
|
|
2803
|
+
'functional category (e.g. integration); the ship clamp uses `preExisting === true || category === ' +
|
|
2804
|
+
'"pre-existing"` (matching the cross-check) to recognize it as non-blocking. Never carry a blocking ' +
|
|
2805
|
+
'P1-P3 story bug — that is a REJECT (enforced in code: a ship carrying a non-pre-existing P1-P3 bug ' +
|
|
2806
|
+
'is mechanically voided to REJECT).\n' +
|
|
2780
2807
|
' • REJECT (verdict "fail") — any blocking evidence item fails.\n' +
|
|
2781
2808
|
'If the Evidence gate is RED after its rework budget, a SHIP additionally requires `flags` to include ' +
|
|
2782
2809
|
'"pre-existing-only-failures" — your explicit, auditable assertion that EVERY remaining failure is ' +
|
|
@@ -2801,8 +2828,8 @@ async function runStory(story) {
|
|
|
2801
2828
|
model: modelFor('JUDGE-DECIDE'),
|
|
2802
2829
|
returnContract:
|
|
2803
2830
|
'Return ONLY { schema:1, agent:"judge", story, verdict:"pass"|"ship-with-bugs"|"fail", ' +
|
|
2804
|
-
'highFindingsOpen, bugs:[{id,priority,severity,affects,description,file_ref,category}], flags } ' +
|
|
2805
|
-
'as JSON. `bugs` is [] unless verdict is "ship-with-bugs".',
|
|
2831
|
+
'highFindingsOpen, bugs:[{id,priority,severity,affects,description,file_ref,category,preExisting}], flags } ' +
|
|
2832
|
+
'as JSON. `bugs` is [] unless verdict is "ship-with-bugs"; set preExisting:true on carried pre-existing bugs.',
|
|
2806
2833
|
}), `JUDGE ship decision for ${sid}`),
|
|
2807
2834
|
'JUDGE',
|
|
2808
2835
|
)
|
|
@@ -2861,8 +2888,13 @@ async function runStory(story) {
|
|
|
2861
2888
|
// one judged path the doctrine allows). Coerced to REJECT — the normal disposition machinery
|
|
2862
2889
|
// (file bugs, revalidate or escalate) takes it from there.
|
|
2863
2890
|
const shipping = decision.verdict === 'pass' || decision.verdict === 'ship-with-bugs'
|
|
2891
|
+
// isPre MUST match crosscheck.js (`preExisting === true || category === 'pre-existing'`): a bug
|
|
2892
|
+
// can be pre-existing (non-blocking) AND keep its functional category (e.g. integration). Checking
|
|
2893
|
+
// only `category !== 'pre-existing'` voided ship-with-bugs to REJECT for baselined pre-existing
|
|
2894
|
+
// P3s whose category was their real area (story 1.2 BUG-002/003 — diverged from the cross-check).
|
|
2895
|
+
const isPreCarried = (b) => b && (b.preExisting === true || b.category === 'pre-existing')
|
|
2864
2896
|
const blockingCarried = (decision.bugs || []).filter(
|
|
2865
|
-
(b) => b && ['P1', 'P2', 'P3'].includes(b.priority) && b
|
|
2897
|
+
(b) => b && ['P1', 'P2', 'P3'].includes(b.priority) && !isPreCarried(b),
|
|
2866
2898
|
)
|
|
2867
2899
|
let clamp = null
|
|
2868
2900
|
if (shipping && blockingCarried.length) {
|
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/evidence.js
CHANGED
|
Binary file
|
package/src/lib/git-flow.js
CHANGED
|
@@ -171,6 +171,12 @@ const STATE_EXCLUDE_PATTERNS = [
|
|
|
171
171
|
'stories/*/evidence/pins.jsonl',
|
|
172
172
|
'stories/*/evidence/resolved-graph.json',
|
|
173
173
|
'stories/*/evidence/stage-results.json',
|
|
174
|
+
// The run pointer the orchestrator + skills rewrite around every Workflow call (/valent-resume
|
|
175
|
+
// reads it off disk). Tracked, it churns on every call AND — on Windows with core.autocrlf=true —
|
|
176
|
+
// shows perpetually modified (LF-written, CRLF-wanted), so sync-story/ship-story's bare `git merge`
|
|
177
|
+
// is refused ("local changes would be overwritten"). A transient local pointer has no business in
|
|
178
|
+
// git history; keep it on disk, out of git. (Root cause of a 3rd Sprint-1 zero-ship, 2026-06-13.)
|
|
179
|
+
'.valent-pipeline/run-state.json',
|
|
174
180
|
];
|
|
175
181
|
|
|
176
182
|
/**
|
|
@@ -396,7 +402,19 @@ export function startStory({ root, story, targetBranch = '', prefix = 'story/',
|
|
|
396
402
|
snapshotSha = snap.sha || null;
|
|
397
403
|
}
|
|
398
404
|
|
|
399
|
-
|
|
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;
|
|
400
418
|
let base = target;
|
|
401
419
|
let basedOnStory = false;
|
|
402
420
|
if (fromStory) {
|