valent-pipeline 0.19.35 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/lib/sprint.js +33 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "valent-pipeline",
3
- "version": "0.19.35",
3
+ "version": "0.19.36",
4
4
  "description": "v3 multi-agent AI pipeline for software development lifecycle",
5
5
  "type": "module",
6
6
  "bin": {
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