valent-pipeline 0.5.2 → 0.5.4
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/retro.workflow.js +42 -141
- package/pipeline/orchestrators/claude-code/sprint.workflow.js +84 -3
- package/pipeline/prompts/qa-a.md +1 -0
- package/pipeline/scripts/query-kb.ts +7 -1
- package/pipeline/steps/common/agent-protocol.md +2 -2
- package/pipeline/steps/common/quality-standards.md +14 -0
- package/pipeline/steps/critic/test-review.md +3 -0
- package/pipeline/steps/qa-a/write-spec.md +20 -0
- package/pipeline/steps/retrospective/directives.md +7 -2
- package/skills/valent-run-project-workflow/SKILL.md +4 -2
package/package.json
CHANGED
|
@@ -5,15 +5,19 @@
|
|
|
5
5
|
* by scripts/test-workflow.js. Opt-in, not the default. The Codex provider keeps the
|
|
6
6
|
* markdown-skill Lead (hybrid, R3).
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* A retrospective LEARNS from what the sprint already produced — it does NOT re-review the code.
|
|
9
|
+
* Finding bugs is CRITIC/JUDGE's job, and they already did it as a gate DURING the sprint; by the
|
|
10
|
+
* time the retro runs the code has shipped, so a fresh review here is both too late to gate and a
|
|
11
|
+
* duplication of work. (An earlier design ran a loop-until-dry aggregate review + completeness-
|
|
12
|
+
* critic on opus — that bolted the pipeline's most expensive pattern onto the stage that should be
|
|
13
|
+
* the cheapest, for little value. The one genuine cross-story blind spot — seams between stories
|
|
14
|
+
* that per-story CRITIC can't see — now lives where it can still gate: the sprint-end integration
|
|
15
|
+
* gate in sprint.workflow.js.)
|
|
13
16
|
*
|
|
14
|
-
* Flow: calibrate (CLI) ->
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
+
* Flow: calibrate (CLI) -> synthesize (mine the sprint's OWN artifacts — CRITIC reviews, JUDGE
|
|
18
|
+
* rejections, QA bugs, rejection-cycle/cost data — into correction directives) -> directives
|
|
19
|
+
* gating (agent proposes; CODE enforces impact gating + the architectural-invariant guard)
|
|
20
|
+
* -> embed (CLI). Bounded and cheap: no opus review loop.
|
|
17
21
|
*
|
|
18
22
|
* The deterministic pieces are NOT in this script: calibration arithmetic is
|
|
19
23
|
* `node .valent-pipeline/bin/cli.js calibrate` (src/lib/sprint.js); embedding is `node .valent-pipeline/bin/cli.js db embed`.
|
|
@@ -21,21 +25,19 @@
|
|
|
21
25
|
* GATING and INVARIANT GUARD are deterministic policy, so they are enforced HERE in code —
|
|
22
26
|
* the agent only proposes; the script decides what gets applied vs. surfaced for approval.
|
|
23
27
|
*
|
|
24
|
-
* args: { batchNumber, sprintId?, storyOutputDirs?: string[],
|
|
25
|
-
* sprintId present => sprint-mode (calibration runs).
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* the baked-in default. See sprint.workflow.js for the full rationale.
|
|
28
|
+
* args: { batchNumber, sprintId?, storyOutputDirs?: string[], models?, reasoning? }
|
|
29
|
+
* sprintId present => sprint-mode (calibration runs). `models` is the pipeline-config.yaml
|
|
30
|
+
* `models` tier->roles map, passed through by the invoking skill so per-agent model tiers stay
|
|
31
|
+
* config-driven (editable via `valent configure`). Omit it to use the baked-in default. See
|
|
32
|
+
* sprint.workflow.js for the full rationale.
|
|
30
33
|
*/
|
|
31
34
|
|
|
32
35
|
export const meta = {
|
|
33
36
|
name: 'valent-retro',
|
|
34
|
-
description: 'Retrospective: calibrate,
|
|
37
|
+
description: 'Retrospective: calibrate, synthesize directives from sprint artifacts, gate, embed (Workflow)',
|
|
35
38
|
phases: [
|
|
36
39
|
{ title: 'Calibrate', detail: 'node .valent-pipeline/bin/cli.js calibrate (estimation accuracy, in code) — sprint mode' },
|
|
37
|
-
{ title: '
|
|
38
|
-
{ title: 'Aggregate', detail: 'loop-until-dry 3-pass aggregate review + completeness critic (R5)' },
|
|
40
|
+
{ title: 'Synthesize', detail: 'mine CRITIC/QA/JUDGE/cost artifacts -> propose correction directives' },
|
|
39
41
|
{ title: 'Directives', detail: 'agent proposes; code enforces impact gating + invariant guard' },
|
|
40
42
|
{ title: 'Embed', detail: 'node .valent-pipeline/bin/cli.js db embed (persist curated patterns)' },
|
|
41
43
|
],
|
|
@@ -43,39 +45,6 @@ export const meta = {
|
|
|
43
45
|
|
|
44
46
|
// --- schemas (inlined) ---
|
|
45
47
|
|
|
46
|
-
const FINDINGS_SCHEMA = {
|
|
47
|
-
type: 'object',
|
|
48
|
-
required: ['schema', 'findings'],
|
|
49
|
-
additionalProperties: true,
|
|
50
|
-
properties: {
|
|
51
|
-
schema: { const: 1 },
|
|
52
|
-
findings: {
|
|
53
|
-
type: 'array',
|
|
54
|
-
items: {
|
|
55
|
-
type: 'object',
|
|
56
|
-
required: ['id', 'summary'],
|
|
57
|
-
properties: {
|
|
58
|
-
id: { type: 'string' },
|
|
59
|
-
summary: { type: 'string' },
|
|
60
|
-
severity: { type: 'string' },
|
|
61
|
-
stories: { type: 'array', items: { type: 'string' } },
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const COMPLETENESS_SCHEMA = {
|
|
69
|
-
type: 'object',
|
|
70
|
-
required: ['schema', 'gaps'],
|
|
71
|
-
additionalProperties: true,
|
|
72
|
-
// gaps = review angles NOT yet covered (e.g. "no security-boundary scan run"). Empty => complete.
|
|
73
|
-
properties: {
|
|
74
|
-
schema: { const: 1 },
|
|
75
|
-
gaps: { type: 'array', items: { type: 'string' } },
|
|
76
|
-
},
|
|
77
|
-
}
|
|
78
|
-
|
|
79
48
|
const DIRECTIVES_SCHEMA = {
|
|
80
49
|
type: 'object',
|
|
81
50
|
required: ['schema', 'directives'],
|
|
@@ -124,8 +93,6 @@ function parseArgs(x) {
|
|
|
124
93
|
const a = parseArgs(args)
|
|
125
94
|
const batchNumber = a.batchNumber
|
|
126
95
|
const sprintId = a.sprintId || null
|
|
127
|
-
const dryRounds = a.dryRounds ?? 2
|
|
128
|
-
const maxRounds = a.maxRounds ?? 5
|
|
129
96
|
if (batchNumber == null) throw new Error('args must include { batchNumber }')
|
|
130
97
|
|
|
131
98
|
// --- per-agent model tiers ----------------------------------------------------
|
|
@@ -133,12 +100,11 @@ if (batchNumber == null) throw new Error('args must include { batchNumber }')
|
|
|
133
100
|
// args.models by the invoking skill — a Workflow script can't read files. We invert it
|
|
134
101
|
// to role->tier and overlay it on a baked-in default so the workflow self-hosts a sane
|
|
135
102
|
// assignment even when args.models is absent. Static + args only => journal-replay safe.
|
|
136
|
-
// Retro stages map to synthetic role keys (not the single RETROSPECTIVE persona) so each
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
103
|
+
// Retro stages map to synthetic role keys (not the single RETROSPECTIVE persona) so each stage can
|
|
104
|
+
// be tuned independently. The retro is learning, not review, so there is no opus tier here:
|
|
105
|
+
// synthesis/judgment over existing artifacts is RETRO -> sonnet; calibrate/embed/IO are mechanical
|
|
106
|
+
// (haiku). The cross-story review that used to justify opus now lives in the sprint-end gate.
|
|
140
107
|
const DEFAULT_MODELS = {
|
|
141
|
-
'RETRO-REVIEW': 'opus',
|
|
142
108
|
RETRO: 'sonnet',
|
|
143
109
|
CALIBRATE: 'haiku', EMBED: 'haiku', PERSIST: 'haiku',
|
|
144
110
|
}
|
|
@@ -185,9 +151,6 @@ const retroPrompt = (instruction, returnContract) => {
|
|
|
185
151
|
(returnContract || 'Return your findings as the JSON object specified.')
|
|
186
152
|
}
|
|
187
153
|
|
|
188
|
-
// A stable de-dup key so loop-until-dry converges (don't re-count the same finding).
|
|
189
|
-
const findingKey = (f) => `${(f.summary || '').toLowerCase().trim().slice(0, 80)}`
|
|
190
|
-
|
|
191
154
|
// ---------------------------------------------------------------------------
|
|
192
155
|
|
|
193
156
|
let calibration = null
|
|
@@ -202,93 +165,33 @@ if (sprintId) {
|
|
|
202
165
|
log(`calibration: ${(calibration.flagged_pairs || []).length} flagged pair(s); velocity unstable=${calibration.velocity?.unstable}`)
|
|
203
166
|
}
|
|
204
167
|
|
|
205
|
-
phase('
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
{ label: 'analyze', phase: 'Analyze', schema: FINDINGS_SCHEMA, model: modelFor('RETRO') },
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
phase('Aggregate')
|
|
215
|
-
// LOOP-UNTIL-DRY (R5): re-run the 3-pass aggregate review until `dryRounds` consecutive
|
|
216
|
-
// rounds surface nothing new, deduping against everything already seen. A simple
|
|
217
|
-
// fixed-pass review (the prose behavior) misses the tail; this does not.
|
|
218
|
-
const seen = new Set()
|
|
219
|
-
const confirmed = []
|
|
220
|
-
let dry = 0
|
|
221
|
-
let round = 0
|
|
222
|
-
while (dry < dryRounds && round < maxRounds) {
|
|
223
|
-
round += 1
|
|
224
|
-
const r = await agent(
|
|
225
|
-
retroPrompt(
|
|
226
|
-
`Run aggregate-review.md (round ${round}): 3-pass CRITIC-style review of the aggregate diff (last retro tag to HEAD) — ` +
|
|
227
|
-
`correctness across story boundaries, convention/pattern drift, architecture/integration. ` +
|
|
228
|
-
`Report ONLY findings not already reported in earlier rounds.`,
|
|
229
|
-
'Return ONLY { schema:1, findings:[{id,summary,severity,stories}] } as JSON.',
|
|
230
|
-
),
|
|
231
|
-
{ label: `aggregate:round-${round}`, phase: 'Aggregate', schema: FINDINGS_SCHEMA, model: modelFor('RETRO-REVIEW') },
|
|
232
|
-
)
|
|
233
|
-
const fresh = (r.findings || []).filter((f) => !seen.has(findingKey(f)))
|
|
234
|
-
if (!fresh.length) {
|
|
235
|
-
dry += 1
|
|
236
|
-
log(`aggregate round ${round}: dry (${dry}/${dryRounds})`)
|
|
237
|
-
continue
|
|
238
|
-
}
|
|
239
|
-
dry = 0
|
|
240
|
-
for (const f of fresh) seen.add(findingKey(f))
|
|
241
|
-
confirmed.push(...fresh)
|
|
242
|
-
log(`aggregate round ${round}: +${fresh.length} new finding(s) (${confirmed.length} total)`)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// COMPLETENESS-CRITIC (R5): ask what review angle we never ran. Each named gap gets one
|
|
246
|
-
// targeted review round; anything it surfaces joins the confirmed set.
|
|
247
|
-
const critic = await agent(
|
|
248
|
-
retroPrompt(
|
|
249
|
-
`We ran ${round} aggregate-review round(s) and found ${confirmed.length} finding(s). ` +
|
|
250
|
-
`What review angle was NOT covered (e.g. a modality, a security boundary, a contract surface)? ` +
|
|
251
|
-
`List only genuine gaps — empty if coverage is complete.`,
|
|
252
|
-
'Return ONLY { schema:1, gaps:["..."] } as JSON.',
|
|
253
|
-
),
|
|
254
|
-
{ label: 'completeness-critic', phase: 'Aggregate', schema: COMPLETENESS_SCHEMA, model: modelFor('RETRO-REVIEW') },
|
|
255
|
-
)
|
|
256
|
-
if ((critic.gaps || []).length) {
|
|
257
|
-
log(`completeness-critic surfaced ${critic.gaps.length} gap(s) — running targeted reviews`)
|
|
258
|
-
const extra = await parallel(
|
|
259
|
-
critic.gaps.map((gap, i) => () =>
|
|
260
|
-
agent(
|
|
261
|
-
retroPrompt(`Targeted aggregate review for the previously-uncovered angle: "${gap}". Report only findings not already reported.`,
|
|
262
|
-
'Return ONLY { schema:1, findings:[{id,summary,severity,stories}] } as JSON.'),
|
|
263
|
-
{ label: `aggregate:gap-${i + 1}`, phase: 'Aggregate', schema: FINDINGS_SCHEMA, model: modelFor('RETRO-REVIEW') },
|
|
264
|
-
)),
|
|
265
|
-
)
|
|
266
|
-
for (const r of extra.filter(Boolean)) {
|
|
267
|
-
for (const f of (r.findings || [])) {
|
|
268
|
-
if (!seen.has(findingKey(f))) { seen.add(findingKey(f)); confirmed.push(f) }
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
log(`aggregate review complete: ${confirmed.length} confirmed finding(s)`)
|
|
273
|
-
|
|
274
|
-
phase('Directives')
|
|
275
|
-
// The agent PROPOSES directives (with impact_level + a touchesInvariant flag). The CODE
|
|
276
|
-
// enforces the policy — deterministic, uncheatable — per the §5b determinism map:
|
|
168
|
+
phase('Synthesize')
|
|
169
|
+
// Learning, not review: one pass mines the artifacts the sprint ALREADY produced (CRITIC reviews,
|
|
170
|
+
// QA-B bug reports, JUDGE rejections, rejection-cycle counts, cost data) per analyze.md, then drafts
|
|
171
|
+
// correction directives per directives.md — no fresh code review, no opus loop. The agent proposes
|
|
172
|
+
// directives (with impact_level + a touchesInvariant flag); the CODE below enforces the policy —
|
|
173
|
+
// deterministic, uncheatable — per the §5b determinism map:
|
|
277
174
|
// - touchesInvariant -> ARCHITECTURE-CONFLICT: never auto-applied, surfaced to the user
|
|
278
175
|
// - impact_level 'high' -> proposal only, requires user approval
|
|
279
176
|
// - 'low' / 'medium' -> auto-applied (medium also notifies the Lead)
|
|
280
177
|
const drafted = await agent(
|
|
281
178
|
retroPrompt(
|
|
282
|
-
`Run directives.md
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
179
|
+
`Run analyze.md then directives.md: read all CRITIC reviews, QA-B bug reports, JUDGE rejections, ` +
|
|
180
|
+
`rejection-cycle counts, and cost data from this sprint's story outputs; categorize the recurring ` +
|
|
181
|
+
`rejection/bug patterns; then propose correction directives for those patterns` +
|
|
182
|
+
(calibration ? ' and for the calibration metrics' : '') +
|
|
183
|
+
`. Do NOT re-review the shipped code for new bugs — that was CRITIC/JUDGE's job during the sprint; ` +
|
|
184
|
+
`your job is to learn from what they already found. For EACH proposed directive set impact_level ` +
|
|
185
|
+
`(low|medium|high) and touchesInvariant=true if it would skip test execution, allow shipping without ` +
|
|
186
|
+
`evidence, weaken a quality gate, or exempt mandatory tests. Do NOT self-censor — propose it and flag ` +
|
|
187
|
+
`it; the orchestrator decides what gets applied.`,
|
|
287
188
|
'Return ONLY { schema:1, directives:[{target_agent,directive,reason,impact_level,touchesInvariant,category}] } as JSON.',
|
|
288
189
|
),
|
|
289
|
-
{ label: '
|
|
190
|
+
{ label: 'synthesize', phase: 'Synthesize', schema: DIRECTIVES_SCHEMA, model: modelFor('RETRO') },
|
|
290
191
|
)
|
|
291
192
|
|
|
193
|
+
phase('Directives')
|
|
194
|
+
|
|
292
195
|
const all = drafted.directives || []
|
|
293
196
|
const conflicts = all.filter((d) => d.touchesInvariant)
|
|
294
197
|
const highImpact = all.filter((d) => !d.touchesInvariant && d.impact_level === 'high')
|
|
@@ -328,9 +231,7 @@ const embed = await agent(
|
|
|
328
231
|
return {
|
|
329
232
|
batchNumber,
|
|
330
233
|
sprintId,
|
|
331
|
-
|
|
332
|
-
aggregate_rounds: round,
|
|
333
|
-
completeness_gaps: (critic.gaps || []).length,
|
|
234
|
+
directives_proposed: all.length,
|
|
334
235
|
directives_applied: applied.length,
|
|
335
236
|
directives_pending_approval: proposals.length,
|
|
336
237
|
architecture_conflicts: conflicts.length,
|
|
@@ -59,6 +59,7 @@ export const meta = {
|
|
|
59
59
|
{ title: 'Critic', detail: 'three independent passes in parallel -> triage -> rejection loop (code-owned cap)' },
|
|
60
60
|
{ title: 'QA', detail: 'execute tests against real infra' },
|
|
61
61
|
{ title: 'Judge', detail: 'evidence-based ship decision' },
|
|
62
|
+
{ title: 'Integration', detail: 'single cross-story seam review — only when >1 story touched overlapping files' },
|
|
62
63
|
],
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -123,6 +124,31 @@ const FINDINGS_SCHEMA = {
|
|
|
123
124
|
},
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
// Sprint-end cross-story seam review. Advisory: stories already passed JUDGE, so this does not
|
|
128
|
+
// re-gate them — it surfaces integration findings to be filed as bugs against the affected stories.
|
|
129
|
+
const INTEGRATION_SCHEMA = {
|
|
130
|
+
type: 'object',
|
|
131
|
+
required: ['schema', 'verdict', 'findings'],
|
|
132
|
+
additionalProperties: true,
|
|
133
|
+
properties: {
|
|
134
|
+
schema: { const: 1 },
|
|
135
|
+
verdict: { enum: ['clean', 'findings'] },
|
|
136
|
+
findings: {
|
|
137
|
+
type: 'array',
|
|
138
|
+
items: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
required: ['summary'],
|
|
141
|
+
properties: {
|
|
142
|
+
summary: { type: 'string' },
|
|
143
|
+
severity: { enum: ['High', 'Med', 'Low'] },
|
|
144
|
+
files: { type: 'array', items: { type: 'string' } },
|
|
145
|
+
stories: { type: 'array', items: { type: 'string' } },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
|
|
126
152
|
const RESOLVED_GRAPH_SCHEMA = {
|
|
127
153
|
type: 'object',
|
|
128
154
|
required: ['tasks', 'skipped'],
|
|
@@ -190,7 +216,7 @@ for (const s of batch) {
|
|
|
190
216
|
// assignment even when args.models is absent. Static + args only => journal-replay safe.
|
|
191
217
|
// gates -> opus (judgment), spec/build -> sonnet, CLI-runners/IO -> haiku.
|
|
192
218
|
const DEFAULT_MODELS = {
|
|
193
|
-
READINESS: 'opus', CRITIC: 'opus', JUDGE: 'opus',
|
|
219
|
+
READINESS: 'opus', CRITIC: 'opus', JUDGE: 'opus', INTEGRATION: 'opus',
|
|
194
220
|
REQS: 'sonnet', UXA: 'sonnet', 'QA-A': 'sonnet', 'QA-B': 'sonnet',
|
|
195
221
|
BEND: 'sonnet', FEND: 'sonnet', DATA: 'sonnet', 'MCP-DEV': 'sonnet',
|
|
196
222
|
LIBDEV: 'sonnet', DOCGEN: 'sonnet', IAC: 'sonnet', MOBILE: 'sonnet',
|
|
@@ -284,11 +310,66 @@ for (let i = 0; i < batch.length; i++) {
|
|
|
284
310
|
|
|
285
311
|
const shippedCount = results.filter((r) => r.shipped).length
|
|
286
312
|
log(`sprint complete: ${shippedCount}/${results.length} shipped`)
|
|
313
|
+
|
|
314
|
+
// Sprint-end integration gate: per-story CRITIC reviews each diff in ISOLATION and never sees two
|
|
315
|
+
// stories together, so cross-story SEAMS (a module touched by >1 story, mismatched integration
|
|
316
|
+
// points) are its one structural blind spot. Cover it with a SINGLE bounded review — but only when
|
|
317
|
+
// it could find something: >1 story shipped AND at least two of them touched an overlapping file.
|
|
318
|
+
// No loop, no completeness-critic (that disproportionate apparatus was removed from the retro for
|
|
319
|
+
// the same reason). Advisory only — stories already passed JUDGE, so findings are surfaced as bugs
|
|
320
|
+
// to file, not a re-gate. Overlap is computed from the dev handoff `files`, so a disjoint sprint
|
|
321
|
+
// (every story in its own corner of the tree) skips the gate entirely.
|
|
322
|
+
const integration = await runIntegrationGate(results.filter((r) => r.shipped))
|
|
323
|
+
|
|
287
324
|
return {
|
|
288
325
|
shipped: results.every((r) => r.shipped),
|
|
289
326
|
stories_shipped: shippedCount,
|
|
290
327
|
stories_rolled_over: results.length - shippedCount,
|
|
291
328
|
results,
|
|
329
|
+
integration,
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// runIntegrationGate: returns null when not warranted (≤1 shipped story or no file overlap), else
|
|
333
|
+
// the single review's structured result ({ verdict, findings }) for the orchestrator to file as bugs.
|
|
334
|
+
async function runIntegrationGate(shipped) {
|
|
335
|
+
if (shipped.length < 2) return null
|
|
336
|
+
|
|
337
|
+
// Files touched by more than one shipped story = the seams worth reviewing.
|
|
338
|
+
const owners = new Map() // file -> Set(storyId)
|
|
339
|
+
for (const r of shipped) {
|
|
340
|
+
for (const f of r.files || []) {
|
|
341
|
+
if (!owners.has(f)) owners.set(f, new Set())
|
|
342
|
+
owners.get(f).add(r.storyId)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const overlapFiles = [...owners.entries()].filter(([, s]) => s.size > 1).map(([f]) => f)
|
|
346
|
+
if (!overlapFiles.length) {
|
|
347
|
+
log(`integration gate: skipped — ${shipped.length} stories shipped but no overlapping files`)
|
|
348
|
+
return null
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
log(`integration gate: ${overlapFiles.length} file(s) touched by >1 story — running single cross-story review`)
|
|
352
|
+
const result = await agent(
|
|
353
|
+
[
|
|
354
|
+
`You are **INTEGRATION**, the sprint-end cross-story seam reviewer for this batch.`,
|
|
355
|
+
``,
|
|
356
|
+
`Per-story CRITIC reviewed each story's diff in isolation and CANNOT see two stories together.`,
|
|
357
|
+
`Review ONLY the cross-story SEAMS — the files below were each modified by more than one story`,
|
|
358
|
+
`this sprint, so that is where integration mismatches hide (incompatible signatures, ordering`,
|
|
359
|
+
`assumptions, duplicated/diverging logic, broken shared invariants).`,
|
|
360
|
+
``,
|
|
361
|
+
`Overlapping files: ${JSON.stringify(overlapFiles)}`,
|
|
362
|
+
`Shipped stories: ${JSON.stringify(shipped.map((r) => r.storyId))}`,
|
|
363
|
+
``,
|
|
364
|
+
`Do a SINGLE focused pass. Do NOT re-review within-story correctness (CRITIC already did) and do`,
|
|
365
|
+
`NOT hunt for unrelated issues. Report only genuine cross-story seam problems; empty if the seams`,
|
|
366
|
+
`are clean. Return ONLY { schema:1, verdict:"clean"|"findings", findings:[{summary,severity,files,stories}] } as JSON.`,
|
|
367
|
+
].join('\n'),
|
|
368
|
+
{ label: 'gate:integration', phase: 'Integration', schema: INTEGRATION_SCHEMA, model: modelFor('INTEGRATION') },
|
|
369
|
+
)
|
|
370
|
+
const findings = result.findings || []
|
|
371
|
+
log(`integration gate: ${findings.length} cross-story finding(s)`)
|
|
372
|
+
return { verdict: result.verdict, findings, overlapFiles }
|
|
292
373
|
}
|
|
293
374
|
|
|
294
375
|
// ===========================================================================
|
|
@@ -373,7 +454,7 @@ async function runStory(story) {
|
|
|
373
454
|
const devFiles = builds.filter(Boolean).flatMap((b) => (Array.isArray(b.files) ? b.files : []))
|
|
374
455
|
if (devFiles.length === 0) {
|
|
375
456
|
log(`${storyId}: dev phase reported no files — nothing to review; skipping CRITIC/QA/JUDGE, rolling over`)
|
|
376
|
-
return { storyId, shipped: false, verdict: 'blocked', reason: 'no-dev-output', skipped: graph.skipped }
|
|
457
|
+
return { storyId, shipped: false, verdict: 'blocked', reason: 'no-dev-output', skipped: graph.skipped, files: [] }
|
|
377
458
|
}
|
|
378
459
|
|
|
379
460
|
phase('Critic')
|
|
@@ -386,7 +467,7 @@ async function runStory(story) {
|
|
|
386
467
|
const decision = await runGate(storyId, 'JUDGE', 'judge.md',
|
|
387
468
|
'Review evidence (tests, traceability, bugs) and make the ship decision.', 'Judge', null)
|
|
388
469
|
|
|
389
|
-
return { storyId, shipped: decision.verdict === 'pass', verdict: decision.verdict, skipped: graph.skipped }
|
|
470
|
+
return { storyId, shipped: decision.verdict === 'pass', verdict: decision.verdict, skipped: graph.skipped, files: devFiles }
|
|
390
471
|
|
|
391
472
|
// --- per-story closures over storyId/devTasks ----------------------------
|
|
392
473
|
|
package/pipeline/prompts/qa-a.md
CHANGED
|
@@ -73,6 +73,7 @@ Before finalizing, verify:
|
|
|
73
73
|
- No behavior is spec'd at multiple tiers
|
|
74
74
|
- Every error test case has a specific error code and message pattern
|
|
75
75
|
- Every P0 test case has DB state verification
|
|
76
|
+
- Every P0/P1 AC on an interactive surface (`api`, `ui`) has a human-readable acceptance scenario realized as a Live Smoke Test (api) or UI Integration Smoke Test (ui) row for QA-B to execute against live infrastructure — never left to unit tests alone
|
|
76
77
|
- Seed data uses factory patterns, not raw SQL
|
|
77
78
|
- NFR requirements are tagged ([NFR-PERF], [NFR-SEC], [NFR-REL])
|
|
78
79
|
- Visual validation checkpoints cover all 5 page states for each page (if UI story)
|
|
@@ -78,10 +78,16 @@ switch (mode) {
|
|
|
78
78
|
|
|
79
79
|
case 'directives': {
|
|
80
80
|
const agent = flags['agent'];
|
|
81
|
+
// ALL-DEV is a cross-cutting directive that applies to every developer agent. Include it when
|
|
82
|
+
// the queried agent is one of them, so a per-agent query still sees shared dev directives.
|
|
83
|
+
const DEV_AGENTS = ['BEND', 'FEND', 'DATA', 'MCP-DEV', 'LIBDEV', 'DOCGEN', 'IAC', 'MOBILE'];
|
|
81
84
|
let rows;
|
|
82
85
|
if (agent) {
|
|
86
|
+
const includeAllDev = DEV_AGENTS.includes(agent.toUpperCase());
|
|
83
87
|
rows = db.prepare(
|
|
84
|
-
|
|
88
|
+
includeAllDev
|
|
89
|
+
? "SELECT id, target_agent, directive, reason FROM correction_directives WHERE (target_agent = ? OR target_agent = 'ALL-DEV') AND status = 'active'"
|
|
90
|
+
: "SELECT id, directive, reason FROM correction_directives WHERE target_agent = ? AND status = 'active'"
|
|
85
91
|
).all(agent) as { id: string; directive: string; reason: string }[];
|
|
86
92
|
} else {
|
|
87
93
|
rows = db.prepare(
|
|
@@ -42,7 +42,7 @@ When `signal_delivery` is `thread`: Lead steers you with the Design Council ques
|
|
|
42
42
|
When you need information about project conventions, architectural patterns, existing code structure, or known pitfalls: self-serve from the knowledge base before exploring the codebase directly. Curated knowledge and correction directives answer in seconds what codebase exploration takes minutes to discover.
|
|
43
43
|
|
|
44
44
|
**How to self-serve:**
|
|
45
|
-
1. Read correction directives from `{correction_directives}` — filter for `status: active` entries
|
|
45
|
+
1. Read correction directives from `{correction_directives}` — filter for `status: active` entries whose `target_agent` is your agent role (or `ALL-DEV` if you are a developer agent).
|
|
46
46
|
2. Read curated knowledge files in `{curated_files_path}` — scan file names and section headers for entries relevant to your current task.
|
|
47
47
|
3. If `{knowledge_mode}` is `sqlite`: query the database via CLI (see your read-inputs step for commands).
|
|
48
48
|
4. If `{story_output_dir}/knowledge-context.md` exists: read it directly — this is a pre-compiled reference containing all relevant knowledge for the current story.
|
|
@@ -51,7 +51,7 @@ Reserve direct codebase exploration for when curated knowledge does not have the
|
|
|
51
51
|
|
|
52
52
|
## Correction Directives
|
|
53
53
|
|
|
54
|
-
Read active correction directives from `{correction_directives}`. If the file does not exist or is empty, proceed without directives -- this is expected for new pipelines. Apply ALL directives
|
|
54
|
+
Read active correction directives from `{correction_directives}`. If the file does not exist or is empty, proceed without directives -- this is expected for new pipelines. Apply ALL directives whose `target_agent` matches your agent role **OR** is `ALL-DEV` when you are a developer agent (BEND, FEND, DATA, MCP-DEV, LIBDEV, DOCGEN, IAC, MOBILE) — `ALL-DEV` is a cross-cutting directive that applies to every developer agent, not one surface. If a directive conflicts with these instructions, the directive takes precedence. Log each applied directive in your YAML frontmatter under `correctionsApplied`.
|
|
55
55
|
|
|
56
56
|
## YAML Frontmatter
|
|
57
57
|
|
|
@@ -10,8 +10,22 @@ These are non-negotiable. CRITIC and QA-B enforce them. Every developer agent (B
|
|
|
10
10
|
- **<1.5 minutes per test** -- any test exceeding this is a design problem, not a timeout problem.
|
|
11
11
|
- **Self-cleaning via fixture auto-teardown** -- tests must not leave state behind. Use framework teardown hooks, not manual cleanup.
|
|
12
12
|
- **Explicit assertions in test bodies** -- never hide assertions in helpers. Every test body must contain at least one visible `expect`/`assert`.
|
|
13
|
+
- **Falsifiable assertions** -- every assertion must be able to FAIL when the behavior is wrong. No unfalsifiable disjunctive gates like `expect(a === 0 || b === 0 || fallback).toBe(true)` — a disjunction with a catch-all that is almost always true asserts nothing. Assert the real invariant directly (e.g. "exactly one container exists AND `SELECT 1` succeeds AND no corruption log"), not a condition that passes regardless of outcome.
|
|
14
|
+
- **Bind to the real artifact under test** -- resolve the actual artifact dynamically (built image id, running service/container name, the file the build emits) and assert against THAT. Never assert against a hardcoded tag/name or an artifact that merely happens to exist on your machine. The test: would it still pass on a clean/CI checkout with no leftover state? If it passes only because of an ambient/side-channel artifact, it is broken. (Prove the artifact exists — `expect(imageId).toBeTruthy()` — before inspecting it.)
|
|
15
|
+
- **Statistical assertions match the spec** -- when the spec states a percentile or sample count (e.g. NFR-PERF "P95 < 200ms"), assert exactly that over real samples with warm-up where appropriate. A single-shot `elapsed < 200` is not a P95 and is flaky — it does not satisfy the spec.
|
|
13
16
|
- **Parallel-safe** -- no shared mutable state between tests. Must run cleanly with `--workers=4`.
|
|
14
17
|
|
|
18
|
+
## Pre-Handoff Self-Check — MANDATORY before declaring done
|
|
19
|
+
|
|
20
|
+
CRITIC enforces every item below and will reject on any High finding, costing a full rework cycle. Run this checklist against your own tests BEFORE writing your handoff; fix anything that fails first. (Each recurred across real stories — this is the gap that buys a guaranteed rework cycle if skipped.)
|
|
21
|
+
|
|
22
|
+
- [ ] **Every P0 qa-test-spec case has a named, implemented test.** Count the spec's P0 cases; count your `it(...)`/`test(...)` blocks; a P0 case with no implementation is a High finding. Do not treat a spec note (e.g. "idempotency") as covered unless there is a named test for it.
|
|
23
|
+
- [ ] **No `if`/`switch`/ternary in any test body** (same path every run).
|
|
24
|
+
- [ ] **No unfalsifiable assertions** — no disjunctive/catch-all gates; each assert can fail.
|
|
25
|
+
- [ ] **Tests bind to the real artifact** — no hardcoded tag/name; would pass on a clean/CI machine with no leftover state.
|
|
26
|
+
- [ ] **Statistical/perf assertions match the spec's wording** (percentile + samples, not single-shot).
|
|
27
|
+
- [ ] **No assertion weakening, no infra mocking on happy paths, no hard waits, assertions visible in test bodies.**
|
|
28
|
+
|
|
15
29
|
## Live Infrastructure Standards
|
|
16
30
|
|
|
17
31
|
- **Live tests against running infrastructure** -- tests hit real systems. No mocking databases, APIs, pipelines, servers, or external services for happy-path verification.
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
- **No test deletion** -- all qa-test-spec test cases must have corresponding tests. A missing test is a High finding.
|
|
22
22
|
- **No hard waits** -- `sleep()`, `setTimeout()`, `page.waitForTimeout()` in tests is a High finding.
|
|
23
23
|
- **No conditionals** -- `if` statements in test bodies is a High finding.
|
|
24
|
+
- **Falsifiable assertions** -- a disjunctive/catch-all gate that is almost always true (e.g. `expect(a === 0 || b === 0 || transient).toBe(true)`) asserts nothing. A test that cannot fail when the behavior is wrong is a High finding. Require the real invariant.
|
|
25
|
+
- **Artifact binding** -- a test that asserts against a hardcoded tag/name, or only passes because of an ambient/leftover artifact on the dev's machine (would fail on a clean/CI checkout), is a High finding. The artifact under test must be resolved dynamically and proven to exist before inspection.
|
|
26
|
+
- **Statistical assertions match spec** -- when the spec states a percentile/sample count (NFR-PERF "P95 < Nms"), a single-shot threshold instead is a Med finding (the assertion does not match the spec).
|
|
24
27
|
- **Explicit assertions** -- assertions hidden inside helper functions is a Med finding. Every test body must contain visible `expect`/`assert`.
|
|
25
28
|
|
|
26
29
|
## Output
|
|
@@ -31,6 +31,17 @@ Structure per AC:
|
|
|
31
31
|
- **Edge cases** (P0: required, P1: key boundaries, P2: optional, P3: omit)
|
|
32
32
|
- **Concurrency** (P0: required, P1: if applicable, P2-P3: omit)
|
|
33
33
|
|
|
34
|
+
### The Given-When-Then cases are human-readable acceptance scenarios — QA-B executes them
|
|
35
|
+
|
|
36
|
+
Your Given-When-Then cases are not just developer test stubs; they are the **human-readable acceptance contract** for the story, written in plain user/consumer language (what a person does and observes), independent of implementation. They describe behavior the way a real developer or QA would verify it by hand.
|
|
37
|
+
|
|
38
|
+
For **interactive surfaces this is mandatory and first-class** — it is how the pipeline tests like a real-world human developer, not just by trusting unit tests:
|
|
39
|
+
|
|
40
|
+
- **`api` profile →** realize each acceptance scenario as a row in the **Live Smoke Tests** table (see `api.md`): real HTTP method/URL/body → expected status + response, plus a verification request for every mutation. QA-B starts the real server and drives these with real requests.
|
|
41
|
+
- **`ui` profile →** realize each user-facing scenario as a row in the **UI Integration Smoke Tests** table (see `ui.md`): user action → real API call → expected UI state → DB verification, backed by a real-browser E2E test. QA-B runs these against the live UI + API + DB (and PMCP validates the visual checkpoints).
|
|
42
|
+
|
|
43
|
+
These acceptance scenarios are **owned and executed by QA-B against live infrastructure** — they are the real-world, black-box evidence JUDGE relies on, distinct from (and never replaced by) the developer's own unit tests. Every P0/P1 AC on an interactive surface MUST have one. The Step 9b quality bar below governs the *automated test code*; it complements these acceptance scenarios — it does not replace them.
|
|
44
|
+
|
|
34
45
|
## Step 4: Database State Verification
|
|
35
46
|
|
|
36
47
|
Per-risk DB verification:
|
|
@@ -72,6 +83,15 @@ For each NFR-sensitive path: `[NFR-PERF]` response time + load patterns; `[NFR-S
|
|
|
72
83
|
- If `ui` in `{testing_profiles}` → **MANDATORY.** Read `uxa-spec.md`. If `uxa-spec.md` is missing, send `[BLOCKER]` to Lead — do NOT proceed without it. For each page state define: Checkpoint ID (VV-{NNN}), Page/Route, State (Default/Loading/Empty/Error/Success or custom), AC Reference, Area labels in scope, Screenshot filename (`{story_id}_VV-{NNN}_{page}_{state}.png`), Expected visual elements, Setup instructions, Pass criteria. Write to `{story_output_dir}/visual-validation-checklist.md`.
|
|
73
84
|
- If `ui` NOT in `{testing_profiles}` → skip, note "N/A — no UI profile."
|
|
74
85
|
|
|
86
|
+
## Step 9b: Test Quality Bar — make every case implementable AND falsifiable
|
|
87
|
+
|
|
88
|
+
The dev agents implement exactly what you specify, and CRITIC rejects tests that are weak, unfalsifiable, or bound to the wrong artifact. Spec the bar IN so it is built right the first time (each rule below traces to a real rework cycle):
|
|
89
|
+
|
|
90
|
+
- **Every P0 case is a named, must-implement test** — never leave a P0 (or an idempotency/concurrency requirement for stateful resources) as a prose note. If it matters, give it its own case ID and assertion target so the dev agent implements it directly. For stateful resources (volumes, DBs), enumerate an explicit idempotency case (apply twice → assert no-recreate / no re-init / data intact).
|
|
91
|
+
- **Falsifiable assertion target** — state the real invariant and the exact expected value (status code, row/column value, count, error code + message regex). Forbid disjunctive/catch-all expectations: never spec "passes if A or B or fallback." The assertion must be able to fail when the behavior is wrong.
|
|
92
|
+
- **Bind to the real artifact** — when a case inspects a built artifact (image, container, file), specify that the test resolves it dynamically (the compose-built image id, the actual service/container name) and proves it exists before inspecting it — never a hardcoded tag. Note it must pass on a clean/CI machine with no leftover state.
|
|
93
|
+
- **NFR-PERF wording is statistical** — specify the percentile AND sample count + warm-up (e.g. "P95 < 200ms over 20 sequential samples after one warm-up request"), not a single-shot threshold.
|
|
94
|
+
|
|
75
95
|
## Step 10: Write Final Outputs
|
|
76
96
|
|
|
77
97
|
- Write to `{story_output_dir}/qa-test-spec.md` and `{story_output_dir}/visual-validation-checklist.md` (if applicable)
|
|
@@ -12,13 +12,18 @@ If pattern touches an invariant, do NOT write a CD. Instead:
|
|
|
12
12
|
|
|
13
13
|
For patterns NOT conflicting with invariants:
|
|
14
14
|
|
|
15
|
+
**Scope the directive to the failure mode, not the agent that tripped it.** Before setting `target_agent`, ask: *could another agent hit this same pattern?* Many failure modes are cross-cutting — test quality (missing P0 cases, conditionals/unfalsifiable assertions in tests, artifact-binding, hard waits), handoff-format violations, infra-mocking — and apply to **every** developer agent, not just the one that happened to surface it this sprint. For those:
|
|
16
|
+
- Set `target_agent: ALL-DEV` (BEND, FEND, DATA, MCP-DEV, LIBDEV, DOCGEN, IAC, MOBILE) so the lesson generalizes; do NOT pin a cross-cutting test-quality directive to the single agent that tripped it.
|
|
17
|
+
- Prefer reinforcing the shared contract: if the pattern is a standing rule, the durable fix is `.valent-pipeline/steps/common/quality-standards.md` (the dev self-check) or the QA-A spec / CRITIC checklist — note that in the directive's `reason`. A per-agent directive that restates a shared standard is noise.
|
|
18
|
+
- Only use a single `target_agent` when the pattern is genuinely specific to that agent's surface (e.g. an IAC-only compose idiom, a FEND-only component pattern).
|
|
19
|
+
|
|
15
20
|
**ADD** new directives:
|
|
16
21
|
```yaml
|
|
17
22
|
- id: CD-{batch_number}-{seq}
|
|
18
23
|
status: active
|
|
19
|
-
target_agent: {agent}
|
|
24
|
+
target_agent: {agent | ALL-DEV}
|
|
20
25
|
directive: "{what to do differently}"
|
|
21
|
-
reason: "{evidence from batch}"
|
|
26
|
+
reason: "{evidence from batch; if it restates a shared standard, name the file it belongs in}"
|
|
22
27
|
impact_level: low | medium | high
|
|
23
28
|
created_batch: {batch_number}
|
|
24
29
|
last_reinforced_batch: {batch_number}
|
|
@@ -96,11 +96,13 @@ Workflow({
|
|
|
96
96
|
})
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
Runs each story sequentially through the per-story pipeline on a shared branch, rolling over any story JUDGE rejects or that trips the cap. Returns `{ shipped, stories_shipped, stories_rolled_over, results: [...] }`. Record its `runId`.
|
|
99
|
+
Runs each story sequentially through the per-story pipeline on a shared branch, rolling over any story JUDGE rejects or that trips the cap. After the batch, a **sprint-end integration gate** runs a single cross-story seam review — but ONLY when >1 story shipped and at least two touched an overlapping file (otherwise it's skipped). Returns `{ shipped, stories_shipped, stories_rolled_over, results: [...], integration }`, where `integration` is `null` (not warranted) or `{ verdict, findings, overlapFiles }`. Record its `runId`.
|
|
100
100
|
|
|
101
101
|
#### 4e. Update progress + re-resolve dependencies
|
|
102
102
|
For each shipped story: move it to `stories_completed` with a compact one-line outcome tagged with its epic; update `total_completed` / `last_updated`; **FIFO at 80 lines** (evict oldest). Read and follow `.valent-pipeline/steps/orchestration/update-backlog-status.md`. Then **re-check whether any previously blocked stories are now unblocked** (their `depends_on` / `blocked_by_bugs` may have been resolved by what just shipped) — these become eligible for the next sprint's candidate list.
|
|
103
103
|
|
|
104
|
+
If the sprint result's `integration.findings` is non-empty, file each as a `bug` backlog item (per `update-backlog-status.md`'s conditional-ship bug format) against the affected stories' epic, so the cross-story seam issue is tracked and prioritized like any other bug.
|
|
105
|
+
|
|
104
106
|
#### 4f. Retrospective (mandatory, blocking)
|
|
105
107
|
Invoke `retro.workflow.js`:
|
|
106
108
|
|
|
@@ -111,7 +113,7 @@ Workflow({
|
|
|
111
113
|
})
|
|
112
114
|
```
|
|
113
115
|
|
|
114
|
-
Record its `runId`. Retro runs every sprint and tightens future sizing.
|
|
116
|
+
Record its `runId`. Retro runs every sprint and tightens future sizing. It **learns from the sprint's own artifacts** — calibration (CLI), one synthesis pass that mines CRITIC/JUDGE/QA/cost data into correction directives, then embed — and is bounded and cheap (no fresh code review; that is CRITIC/JUDGE's job during the sprint, and cross-story seams are covered by the sprint-end integration gate in 4d).
|
|
115
117
|
|
|
116
118
|
#### 4g. Continue
|
|
117
119
|
Increment `{n}` and return to Step 4a. **Do NOT ask permission to continue between sprints** — the project loop is autonomous.
|