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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "valent-pipeline",
3
- "version": "0.19.16",
3
+ "version": "0.19.22",
4
4
  "description": "v3 multi-agent AI pipeline for software development lifecycle",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 from the \`valent:bugs\` block in bugs.md) — do NOT count from execution-report.md prose. ` +
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). Never carry a blocking P1-P3 story bug — that is a REJECT ' +
2779
- '(enforced in code: a ship carrying a non-pre-existing P1-P3 bug is mechanically voided to REJECT).\n' +
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.category !== 'pre-existing',
2897
+ (b) => b && ['P1', 'P2', 'P3'].includes(b.priority) && !isPreCarried(b),
2866
2898
  )
2867
2899
  let clamp = null
2868
2900
  if (shipping && blockingCarried.length) {
@@ -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) && reported[field] !== actual) {
61
- mismatches.push({ field, claimed: reported[field], actual });
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).
Binary file
@@ -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
- const target = targetBranch || onBranch;
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) {