valent-pipeline 0.19.34 → 0.19.36

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.34",
3
+ "version": "0.19.36",
4
4
  "description": "v3 multi-agent AI pipeline for software development lifecycle",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@ import { dirname, join, isAbsolute } from 'path';
16
16
  import { randomBytes } from 'crypto';
17
17
  import { parse as parseYaml } from 'yaml';
18
18
  import {
19
- runEvidence, pinGate, formatRunTrailer, formatRunDigest, resolveRoot, evidenceDir, latestRun, resolveRunForAssert, latestPins,
19
+ runEvidence, pinGate, formatRunTrailer, formatRunDigest, resolveRoot, evidenceDir, latestRun, resolveRunForAssert, detectLabelMultiplex, latestPins,
20
20
  verifyIntegrity, assertRules, assertRedRules, pinSourceDelta, snapshotFiles, buildProof, utcCompact,
21
21
  latestAssert, collectFailingCases, authenticateBaseline, verifyShipEvidence, sha256Hex,
22
22
  validStoryId, recordStageResults,
@@ -225,6 +225,19 @@ export async function evidenceAssertCmd(options) {
225
225
  process.exit(0);
226
226
  }
227
227
 
228
+ // Advisory (option C): residual label-multiplex at this SHA. Record-time auto-namespacing heals the
229
+ // common case (a sibling recorded AFTER the acceptance run); this catches one that recorded BEFORE it
230
+ // (so it kept the base label). Non-blocking — resolveRunForAssert already bound to the required-case
231
+ // run; the warning just surfaces the smell so the project fixes its labels at source.
232
+ try {
233
+ const mx = detectLabelMultiplex(evDir, { sha: run.record?.git?.sha, label });
234
+ if (mx.multiplexed) {
235
+ console.error(`evidence-assert: ⚠ label-multiplex — ${mx.suites.length} disjoint suites share label "${label}" ` +
236
+ `at SHA ${String(run.record?.git?.sha).slice(0, 8)} (runs ${mx.suites.map((s) => s.runId).join(', ')}). Bound to the ` +
237
+ `required-case run; give each suite a distinct --label in QA-B to retire this warning. (HARNESS-GAPS option C)`);
238
+ }
239
+ } catch { /* advisory only — never block the assert */ }
240
+
228
241
  // Re-parse the frozen reports from disk — machine-derived twice (at run time and here).
229
242
  const parsedReports = (run.record.reports || []).map((r) => {
230
243
  let parsed = null;
Binary file
package/src/lib/sprint.js CHANGED
@@ -226,22 +226,27 @@ export function packSprint(stories, velocity, opts = {}) {
226
226
  // groomed story alone — pulling in its groomed prerequisites first so order stays dependency-safe
227
227
  // — accepting an over-budget sprint. `over_budget` is flagged so the planner can surface
228
228
  // "this story exceeds velocity; planned alone — consider splitting it."
229
+ // forceAdd: include a story over-budget, pulling in its unsatisfied blocking bugs + groomed deps
230
+ // FIRST so execution order stays dependency-safe. Shared by both over-budget guards below.
231
+ const forceAdd = (story) => {
232
+ if (inSprint.has(story.id)) return;
233
+ for (const bugId of bugsOf(story)) {
234
+ const bug = byId.get(bugId);
235
+ if (bug && !satisfied.has(bug.status) && !inSprint.has(bugId)) forceAdd(bug);
236
+ }
237
+ for (const depId of depsOf(story)) {
238
+ const dep = byId.get(depId);
239
+ if (dep && dep.status === 'groomed' && !inSprint.has(depId)) forceAdd(dep);
240
+ }
241
+ add(story);
242
+ };
243
+
229
244
  let overBudget = false;
245
+ // Anti-stall (empty sprint): nothing fit the budget but groomed work exists — the smallest eligible
246
+ // story exceeds the whole velocity. Plan the highest-priority groomed story alone, over budget, so the
247
+ // project can never stall on a story that can never be packed. Same out-of-batch guard as the main
248
+ // loop (review #20): never force-plan a story whose unpacked prerequisite never shipped.
230
249
  if (sprintStories.length === 0 && byPriority.length > 0) {
231
- const forceAdd = (story) => {
232
- if (inSprint.has(story.id)) return;
233
- for (const bugId of bugsOf(story)) {
234
- const bug = byId.get(bugId);
235
- if (bug && !satisfied.has(bug.status) && !inSprint.has(bugId)) forceAdd(bug);
236
- }
237
- for (const depId of depsOf(story)) {
238
- const dep = byId.get(depId);
239
- if (dep && dep.status === 'groomed' && !inSprint.has(depId)) forceAdd(dep);
240
- }
241
- add(story);
242
- };
243
- // Same out-of-batch guard as the main loop (review #20): the anti-stall must not force-plan
244
- // a story whose unpacked prerequisite never shipped.
245
250
  const candidate = byPriority.find((s) => outOfBatchDepsSatisfied(s));
246
251
  if (candidate) {
247
252
  forceAdd(candidate);
@@ -249,6 +254,20 @@ export function packSprint(stories, velocity, opts = {}) {
249
254
  }
250
255
  }
251
256
 
257
+ // Feature-starvation guard (HARNESS-GAPS 2026-06-18, M-19 Issue 2 / orchestrator-confirmed): the
258
+ // empty-sprint anti-stall above does NOT fire when a small bug/spike fits — so a 1-pt bug packed while
259
+ // every groomed FEATURE exceeds velocity strands all feature work indefinitely (a bug-only sprint makes
260
+ // no product progress). If the packed set has NO feature story yet a deps-satisfied groomed feature
261
+ // exists, force the highest-priority one in over budget (same dependency-safe forceAdd).
262
+ const isFeature = (s) => s && s.type !== 'bug' && s.type !== 'spike';
263
+ if (sprintStories.length > 0 && !sprintStories.some((id) => isFeature(byId.get(id)))) {
264
+ const topFeature = byPriority.find((s) => isFeature(s) && !inSprint.has(s.id) && outOfBatchDepsSatisfied(s));
265
+ if (topFeature) {
266
+ forceAdd(topFeature);
267
+ overBudget = true;
268
+ }
269
+ }
270
+
252
271
  const buffer = groomed.filter((s) => !inSprint.has(s.id)).map((s) => s.id);
253
272
  const pointsPlanned = sprintStories.reduce((sum, id) => sum + pointsOf(byId.get(id)), 0);
254
273