valent-pipeline 0.5.4 → 0.5.6
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 +1 -1
- package/pipeline/orchestrators/claude-code/plan.workflow.js +13 -5
- package/pipeline/steps/orchestration/sprint-init.md +8 -3
- package/pipeline/steps/orchestration/sprint-plan.md +11 -3
- package/pipeline/steps/orchestration/sprint-review.md +5 -1
- package/pipeline/steps/orchestration/sprint-size.md +3 -2
- package/pipeline/templates/sprint-status.template.yaml +1 -0
- package/src/board/public/app.js +8 -1
- package/src/board/public/styles.css +5 -0
- package/src/lib/board.js +45 -2
- package/src/lib/sprint.js +28 -1
package/package.json
CHANGED
|
@@ -101,6 +101,7 @@ const PACK_SCHEMA = {
|
|
|
101
101
|
buffer_story_ids: { type: 'array', items: { type: 'string' } },
|
|
102
102
|
points_planned: { type: 'integer' },
|
|
103
103
|
remaining_capacity: { type: 'integer' },
|
|
104
|
+
over_budget: { type: 'boolean' },
|
|
104
105
|
},
|
|
105
106
|
}
|
|
106
107
|
|
|
@@ -291,10 +292,14 @@ const sized = await parallel(
|
|
|
291
292
|
}),
|
|
292
293
|
{ label: `estimate:${est.toLowerCase()}:${g.storyId}`, phase: 'Size', schema: ESTIMATE_SCHEMA, model: modelFor(est) },
|
|
293
294
|
)),
|
|
294
|
-
).then((ests) =>
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
).then((ests) => {
|
|
296
|
+
// Each estimator sizes the WHOLE story from its surface's lens (BEND sees the full story,
|
|
297
|
+
// IAC sees the full story), so the points are the MAX single estimate — NOT the sum.
|
|
298
|
+
// Summing double-counts shared scaffolding: a 2-profile story is not twice the work, and
|
|
299
|
+
// summing systematically over-points multi-profile stories until they exceed velocity.
|
|
300
|
+
const vals = ests.filter(Boolean).map((e) => e.points || 0)
|
|
301
|
+
return { ...g, points: vals.length ? Math.max(...vals) : 0 }
|
|
302
|
+
})
|
|
298
303
|
}),
|
|
299
304
|
)
|
|
300
305
|
const sizedStories = sized.filter(Boolean)
|
|
@@ -313,10 +318,13 @@ phase('Pack')
|
|
|
313
318
|
// Deterministic greedy packing happens in code (src/lib/sprint.js), invoked via the CLI.
|
|
314
319
|
const pack = await agent(
|
|
315
320
|
`Run exactly: \`node .valent-pipeline/bin/cli.js sprint-pack --velocity ${velocity} --backlog ${backlogPath}\` ` +
|
|
316
|
-
`in the project root and return its stdout JSON verbatim (fields: sprint_stories, buffer_story_ids, points_planned, remaining_capacity).`,
|
|
321
|
+
`in the project root and return its stdout JSON verbatim (fields: sprint_stories, buffer_story_ids, points_planned, remaining_capacity, over_budget).`,
|
|
317
322
|
{ label: 'sprint-pack', phase: 'Pack', schema: PACK_SCHEMA, model: modelFor('PACK') },
|
|
318
323
|
)
|
|
319
324
|
log(`packed ${pack.sprint_stories.length} stories (${pack.points_planned} pts); buffer: ${pack.buffer_story_ids.length}`)
|
|
325
|
+
if (pack.over_budget) {
|
|
326
|
+
log(`⚠ sprint ${sprintId} is OVER BUDGET: the highest-priority story exceeds velocity ${velocity} and was planned alone — consider splitting it (${pack.points_planned} pts vs ${velocity} velocity)`)
|
|
327
|
+
}
|
|
320
328
|
|
|
321
329
|
phase('Validate')
|
|
322
330
|
// Write the human plan + machine status artifacts, tag the backlog, then cross-check in code.
|
|
@@ -19,9 +19,14 @@ node .valent-pipeline/bin/cli.js db query-velocity
|
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
**Velocity rules:**
|
|
22
|
-
|
|
23
|
-
- **
|
|
24
|
-
|
|
22
|
+
|
|
23
|
+
Velocity is your *capacity* — how many points you can ship when capacity is the binding constraint. Only **capacity-constrained** sprints carry that signal. A **supply-constrained** sprint (one that shipped everything eligible with capacity to spare — it ran out of groomed/eligible work, not out of capacity) reflects how much work *existed*, not how much you *could do*, so counting it would falsely ratchet velocity down. Each sprint's status YAML summary records its `constraint:` (`capacity` or `supply`) — see sprint-review.md Step 2.
|
|
24
|
+
|
|
25
|
+
- **Sprint 1:** Use `{sprint_initial_velocity}` from config (default: 60 points).
|
|
26
|
+
- **Later sprints:** Moving average of `points_shipped` over **capacity-constrained sprints only** — exclude every `constraint: supply` sprint.
|
|
27
|
+
- 2-4 capacity-constrained sprints available: average their `points_shipped`.
|
|
28
|
+
- 5+ available: SMA of the last 5 capacity-constrained sprints (older data ages out).
|
|
29
|
+
- **If no capacity-constrained sprint has happened yet** (e.g. early sprints were all supply-constrained because the backlog is a dependency chain): keep `{sprint_initial_velocity}`. **Never lower velocity based on a supply-constrained sprint** — this is the failure that drives a healthy initial velocity down to a tiny number after one small starter story.
|
|
25
30
|
|
|
26
31
|
Record: `current_velocity = {value}` points.
|
|
27
32
|
|
|
@@ -12,9 +12,17 @@ node .valent-pipeline/bin/cli.js sprint-pack --velocity {current_velocity} --bac
|
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
It emits JSON: `sprint_stories` (packed, in dependency-safe order), `buffer_story_ids`
|
|
15
|
-
(groomed but not packed — the mid-sprint pull buffer, see Step 1b), `points_planned`,
|
|
16
|
-
`remaining_capacity`. Only `groomed` stories are eligible; lower `priority`
|
|
17
|
-
priority; a groomed prerequisite is auto-included before its dependent when it
|
|
15
|
+
(groomed but not packed — the mid-sprint pull buffer, see Step 1b), `points_planned`,
|
|
16
|
+
`remaining_capacity`, and `over_budget`. Only `groomed` stories are eligible; lower `priority`
|
|
17
|
+
number = higher priority; a groomed prerequisite is auto-included before its dependent when it
|
|
18
|
+
fits the budget.
|
|
19
|
+
|
|
20
|
+
**If `over_budget` is `true`:** no story fit the velocity budget, so the highest-priority groomed
|
|
21
|
+
story (plus its groomed prerequisites) was planned ALONE and `points_planned` exceeds velocity
|
|
22
|
+
(`remaining_capacity` is negative). This is the anti-stall path — the sprint still makes progress.
|
|
23
|
+
Surface it to the user: this story is larger than a full sprint and is a **split candidate** — note
|
|
24
|
+
it in the sprint plan and consider asking REQS to break it into smaller stories before a future run.
|
|
25
|
+
Do NOT treat the negative `remaining_capacity` as room to groom more stories.
|
|
18
26
|
|
|
19
27
|
Use this output directly for Steps 1b–8. If the backlog isn't the right input shape, pass an
|
|
20
28
|
explicit story array with `--stories <path>` instead of `--backlog`.
|
|
@@ -20,7 +20,11 @@ Update `sprint-{n}-plan.md` Sprint Summary:
|
|
|
20
20
|
- Points rolled over: sum of unexecuted story points
|
|
21
21
|
- Total elapsed minutes: sum of execution time (not grooming)
|
|
22
22
|
- Velocity this sprint: points_shipped (all shipped stories count toward velocity, including pulls)
|
|
23
|
-
-
|
|
23
|
+
- **Sprint constraint:** classify this sprint so the next sprint-init calibrates velocity correctly:
|
|
24
|
+
- `capacity` — capacity was the binding constraint: stories were left in the groomed buffer or rolled over (more eligible work existed than fit), OR the plan was over-budget (a single oversized story planned alone). These sprints count toward velocity.
|
|
25
|
+
- `supply` — supply was the binding constraint: the sprint shipped all eligible/groomed work with capacity to spare (groomed buffer empty AND positive `capacity_remaining` at plan time). These sprints do NOT count toward velocity (they measure available work, not capacity).
|
|
26
|
+
Record as `constraint:` in the status YAML summary.
|
|
27
|
+
- Updated velocity (SMA-5): recompute the moving average over **capacity-constrained sprints only** (see sprint-init.md Step 2). A `supply`-constrained sprint must not lower velocity.
|
|
24
28
|
- Mid-sprint pulls: count and list (stories pulled from groomed buffer during execution)
|
|
25
29
|
|
|
26
30
|
## Step 3: Finalize Sprint Status YAML
|
|
@@ -34,8 +34,9 @@ For each story with status `groomed`:
|
|
|
34
34
|
- `iac` in profiles → send to IAC
|
|
35
35
|
Multiple profiles can be active (e.g., `[api, data-pipeline]` sends to both BEND and DATA).
|
|
36
36
|
4. Agents write estimation files (`{agent}-estimation.md`)
|
|
37
|
-
5. **Record points:**
|
|
38
|
-
`story_points =
|
|
37
|
+
5. **Record points:** take the **maximum** single estimate, NOT the sum.
|
|
38
|
+
`story_points = max of all agent estimates received`
|
|
39
|
+
Each estimator sizes the *whole* story from its surface's lens (BEND sizes the entire story, IAC sizes the entire story), so their estimates overlap on shared scaffolding. Summing double-counts that overlap and systematically over-points multi-profile stories until they exceed velocity. The max is the best-informed single read. (If you believe two surfaces carry genuinely independent, non-overlapping work, flag it for the Lead rather than silently summing.)
|
|
39
40
|
6. Update story's `story_points` field in `{backlog_path}`
|
|
40
41
|
|
|
41
42
|
## Step 3: Update Sprint State
|
package/src/board/public/app.js
CHANGED
|
@@ -90,9 +90,16 @@ function statusKind(status, column) {
|
|
|
90
90
|
/* ---------- Chips ----------------------------------------------------------------------- */
|
|
91
91
|
function statusChip(story) {
|
|
92
92
|
const k = statusKind(story.status, story.column);
|
|
93
|
-
|
|
93
|
+
// When the status was inferred from on-disk artifacts (backlog status lags an in-flight groom),
|
|
94
|
+
// mark it so the board reads honestly: a dotted chip + a title explaining the real backlog state.
|
|
95
|
+
const inferred = story.status_source === 'artifacts';
|
|
96
|
+
const title = inferred
|
|
97
|
+
? `status: ${k.label} (inferred from artifacts — backlog still says ${story.backlog_status || 'pending'})`
|
|
98
|
+
: `status: ${k.label}`;
|
|
99
|
+
return h('span', { class: `chip chip--status ${k.cls}${inferred ? ' chip--inferred' : ''}`, title },
|
|
94
100
|
h('span', { class: 'chip__icon', 'aria-hidden': 'true' }, k.icon),
|
|
95
101
|
k.label,
|
|
102
|
+
inferred ? h('span', { class: 'chip__inferred-mark', 'aria-hidden': 'true', title: 'inferred' }, '~') : null,
|
|
96
103
|
);
|
|
97
104
|
}
|
|
98
105
|
|
|
@@ -457,6 +457,11 @@ body {
|
|
|
457
457
|
.chip--status.is-idle { color: var(--ink-muted); background: var(--idle-soft); border-color: transparent; }
|
|
458
458
|
.chip--status.is-warn { color: var(--warn); background: var(--warn-soft); border-color: transparent; }
|
|
459
459
|
|
|
460
|
+
/* Inferred status — derived from on-disk artifacts because the backlog status lags an active
|
|
461
|
+
groom. Dashed outline + a faint "~" mark signals "best-effort, not yet persisted". */
|
|
462
|
+
.chip--status.chip--inferred { border: 1px dashed currentColor; }
|
|
463
|
+
.chip__inferred-mark { opacity: 0.6; font-weight: 600; margin-left: 1px; }
|
|
464
|
+
|
|
460
465
|
/* ---------- Backlog list ---------------------------------------------------------------- */
|
|
461
466
|
.backlog {
|
|
462
467
|
max-width: 1100px;
|
package/src/lib/board.js
CHANGED
|
@@ -60,6 +60,30 @@ export function columnForStatus(status) {
|
|
|
60
60
|
return STATUS_TO_COLUMN.get(status) || 'other'
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Grooming artifacts, most-advanced phase first. Their presence means a story is mid-groom even
|
|
65
|
+
* when the backlog `status` still says `pending` — the Workflow path (plan.workflow.js) grooms
|
|
66
|
+
* the batch in-memory and only persists `status: groomed` at the very end, so the backlog status
|
|
67
|
+
* lags reality during a live groom (and there is often no pipeline-state.json at all). The board
|
|
68
|
+
* is a projection, so it infers the grooming phase from the artifacts the groom agents have
|
|
69
|
+
* already written, exactly as it overlays the live current_story over a lagging backlog.
|
|
70
|
+
*/
|
|
71
|
+
const GROOMING_ARTIFACT_STATUS = [
|
|
72
|
+
['readiness-review.md', 'readiness-review'],
|
|
73
|
+
['qa-test-spec.md', 'test-case-development'],
|
|
74
|
+
['uxa-spec.md', 'ux-spec'],
|
|
75
|
+
['reqs-brief.md', 'requirements-spec'],
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
/** The most-advanced grooming status implied by present artifacts, or null if none. */
|
|
79
|
+
export function inferGroomingStatus(artifacts) {
|
|
80
|
+
if (!Array.isArray(artifacts)) return null
|
|
81
|
+
for (const [file, status] of GROOMING_ARTIFACT_STATUS) {
|
|
82
|
+
if (artifacts.includes(file)) return status
|
|
83
|
+
}
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
63
87
|
/** Story points, tolerating either field name (matches sprint.js `pointsOf`). */
|
|
64
88
|
function pointsOf(item) {
|
|
65
89
|
const p = item.points ?? item.story_points
|
|
@@ -74,10 +98,29 @@ function depsOf(item) {
|
|
|
74
98
|
/** Normalize one backlog item into a board story (stable shape regardless of input quirks). */
|
|
75
99
|
function normalizeStory(item, artifactsByStory) {
|
|
76
100
|
const id = item.id
|
|
77
|
-
const
|
|
101
|
+
const backlogStatus = item.status || 'pending'
|
|
102
|
+
const artifacts = (artifactsByStory && artifactsByStory[id]) || []
|
|
103
|
+
|
|
104
|
+
// Reflect an in-flight groom the backlog status hasn't caught up to: if a story still reads
|
|
105
|
+
// `pending` (its column would be Backlog) but grooming artifacts already exist on disk, surface
|
|
106
|
+
// the inferred grooming phase so it shows in Grooming instead of sitting in Backlog. An explicit
|
|
107
|
+
// forward status (groomed/sprint-planned/development/…/shipped) always wins — only `pending` is
|
|
108
|
+
// upgraded, and `status_source` records that the status was derived, not read from the backlog.
|
|
109
|
+
let status = backlogStatus
|
|
110
|
+
let statusSource = 'backlog'
|
|
111
|
+
if (columnForStatus(backlogStatus) === 'backlog') {
|
|
112
|
+
const inferred = inferGroomingStatus(artifacts)
|
|
113
|
+
if (inferred) {
|
|
114
|
+
status = inferred
|
|
115
|
+
statusSource = 'artifacts'
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
78
119
|
return {
|
|
79
120
|
id,
|
|
80
121
|
status,
|
|
122
|
+
backlog_status: backlogStatus,
|
|
123
|
+
status_source: statusSource,
|
|
81
124
|
column: columnForStatus(status),
|
|
82
125
|
depends_on: depsOf(item),
|
|
83
126
|
points: pointsOf(item),
|
|
@@ -88,7 +131,7 @@ function normalizeStory(item, artifactsByStory) {
|
|
|
88
131
|
active: false, // set true below if it matches current_story
|
|
89
132
|
phase: null, // overlaid from current_story when active
|
|
90
133
|
branch: null,
|
|
91
|
-
artifacts
|
|
134
|
+
artifacts,
|
|
92
135
|
cost: null, // { tokens, agent_ms, invocations, roles:[...] } when an audit report is supplied
|
|
93
136
|
}
|
|
94
137
|
}
|
package/src/lib/sprint.js
CHANGED
|
@@ -31,13 +31,19 @@ function depsOf(story) {
|
|
|
31
31
|
* @param {Array} stories - candidate stories: { id, points|story_points, priority, depends_on, status }
|
|
32
32
|
* @param {number} velocity - capacity in story points
|
|
33
33
|
* @returns {{ sprint_stories: string[], buffer_story_ids: string[], points_planned: number,
|
|
34
|
-
* remaining_capacity: number, velocity: number }}
|
|
34
|
+
* remaining_capacity: number, velocity: number, over_budget: boolean }}
|
|
35
35
|
*
|
|
36
36
|
* Only `groomed` stories are eligible (matches the source). Lower `priority` number = higher
|
|
37
37
|
* priority; missing priority sorts last. A prerequisite is auto-included only when it is also
|
|
38
38
|
* `groomed` and fits the remaining budget — deps already `shipped`/done are assumed satisfied
|
|
39
39
|
* and silently skipped, exactly as the prose specifies.
|
|
40
40
|
*
|
|
41
|
+
* Anti-stall: if no story fits the budget at all (the smallest groomed story is larger than the
|
|
42
|
+
* whole velocity), the highest-priority groomed story is planned ALONE — with its groomed
|
|
43
|
+
* prerequisites pulled in first — accepting an over-budget sprint (`over_budget: true`,
|
|
44
|
+
* `remaining_capacity` may go negative). This prevents an oversized story from stalling the
|
|
45
|
+
* project forever; the planner should surface it as a split candidate.
|
|
46
|
+
*
|
|
41
47
|
* Preserved quirk: if a story's dependency chain does not fully fit, the deps already added
|
|
42
48
|
* for it stay in the sprint (capacity is not rolled back) and the dependent is skipped. This
|
|
43
49
|
* matches the source pseudocode; `validateSprint` is the safety net for ordering consistency.
|
|
@@ -85,6 +91,26 @@ export function packSprint(stories, velocity) {
|
|
|
85
91
|
// else: skip this story, try smaller ones to fill capacity
|
|
86
92
|
}
|
|
87
93
|
|
|
94
|
+
// Anti-stall: if NOTHING fit the budget but groomed work exists, the smallest eligible story is
|
|
95
|
+
// larger than the entire velocity. Returning an empty sprint would stall the project forever
|
|
96
|
+
// (that story can never be packed, so the loop never progresses). Plan the highest-priority
|
|
97
|
+
// groomed story alone — pulling in its groomed prerequisites first so order stays dependency-safe
|
|
98
|
+
// — accepting an over-budget sprint. `over_budget` is flagged so the planner can surface
|
|
99
|
+
// "this story exceeds velocity; planned alone — consider splitting it."
|
|
100
|
+
let overBudget = false;
|
|
101
|
+
if (sprintStories.length === 0 && byPriority.length > 0) {
|
|
102
|
+
const forceAdd = (story) => {
|
|
103
|
+
if (inSprint.has(story.id)) return;
|
|
104
|
+
for (const depId of depsOf(story)) {
|
|
105
|
+
const dep = byId.get(depId);
|
|
106
|
+
if (dep && dep.status === 'groomed' && !inSprint.has(depId)) forceAdd(dep);
|
|
107
|
+
}
|
|
108
|
+
add(story);
|
|
109
|
+
};
|
|
110
|
+
forceAdd(byPriority[0]);
|
|
111
|
+
overBudget = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
88
114
|
const buffer = groomed.filter((s) => !inSprint.has(s.id)).map((s) => s.id);
|
|
89
115
|
const pointsPlanned = sprintStories.reduce((sum, id) => sum + pointsOf(byId.get(id)), 0);
|
|
90
116
|
|
|
@@ -94,6 +120,7 @@ export function packSprint(stories, velocity) {
|
|
|
94
120
|
points_planned: pointsPlanned,
|
|
95
121
|
remaining_capacity: remaining,
|
|
96
122
|
velocity,
|
|
123
|
+
over_budget: overBudget,
|
|
97
124
|
};
|
|
98
125
|
}
|
|
99
126
|
|