valent-pipeline 0.3.4 → 0.4.2
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/bin/cli.js +80 -0
- package/package.json +7 -5
- package/pipeline/docs/design/provider-adapter-guide.md +6 -7
- package/pipeline/orchestrators/claude-code/README.md +99 -0
- package/pipeline/orchestrators/claude-code/plan.workflow.js +284 -0
- package/pipeline/orchestrators/claude-code/retro.workflow.js +274 -0
- package/pipeline/orchestrators/claude-code/sprint.workflow.js +354 -0
- package/pipeline/orchestrators/codex/README.md +52 -0
- package/pipeline/orchestrators/codex/lead-loop.md +115 -0
- package/pipeline/prompts/critic.md +2 -0
- package/pipeline/prompts/lead.md +1 -1
- package/pipeline/schemas/handoff.schema.json +19 -0
- package/pipeline/schemas/task-graph.schema.json +53 -0
- package/pipeline/schemas/verdict.schema.json +20 -0
- package/pipeline/steps/common/distilled-handoff-format.md +15 -0
- package/pipeline/steps/critic/acceptance-audit.md +1 -1
- package/pipeline/steps/critic/edge-case-hunt.md +2 -2
- package/pipeline/steps/critic/triage.md +2 -2
- package/pipeline/steps/orchestration/adopt-lead-and-create-team.md +13 -12
- package/pipeline/steps/orchestration/sprint-plan.md +28 -31
- package/pipeline/steps/retrospective/calibration.md +18 -31
- package/pipeline/task-graphs/backend-api.yaml +1 -1
- package/pipeline/task-graphs/data-pipeline.yaml +1 -1
- package/pipeline/task-graphs/document-generation.yaml +1 -1
- package/pipeline/task-graphs/frontend-only.yaml +9 -8
- package/pipeline/task-graphs/fullstack-web.yaml +11 -10
- package/pipeline/task-graphs/library.yaml +1 -1
- package/pipeline/task-graphs/mcp-server.yaml +1 -1
- package/pipeline/task-graphs/mobile-app.yaml +8 -7
- package/pipeline/templates/bend-handoff.template.md +11 -0
- package/pipeline/templates/critic-review.template.md +15 -1
- package/pipeline/templates/data-handoff.template.md +11 -0
- package/pipeline/templates/docgen-handoff.template.md +11 -0
- package/pipeline/templates/execution-report.template.md +11 -0
- package/pipeline/templates/fend-handoff.template.md +11 -0
- package/pipeline/templates/iac-handoff.template.md +11 -0
- package/pipeline/templates/judge-decision.template.md +13 -0
- package/pipeline/templates/libdev-handoff.template.md +11 -0
- package/pipeline/templates/mcp-dev-handoff.template.md +11 -0
- package/pipeline/templates/mobile-handoff.template.md +11 -0
- package/pipeline/templates/qa-test-spec.template.md +11 -0
- package/pipeline/templates/readiness-review.template.md +13 -0
- package/pipeline/templates/reqs-brief.template.md +11 -0
- package/pipeline/templates/uxa-spec.template.md +11 -0
- package/skills/valent-run-story/SKILL.md +12 -0
- package/src/commands/calibrate.js +86 -0
- package/src/commands/init.js +1 -1
- package/src/commands/rejection-cap.js +70 -0
- package/src/commands/resolve-graph.js +79 -0
- package/src/commands/sprint-pack.js +62 -0
- package/src/commands/validate-handoff.js +32 -0
- package/src/commands/validate-sprint.js +55 -0
- package/src/lib/graph.js +98 -0
- package/src/lib/handoff.js +99 -0
- package/src/lib/rejection.js +38 -0
- package/src/lib/sprint.js +312 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* valent-pipeline RETROSPECTIVE orchestrator — Claude Code (native Workflow) provider.
|
|
3
|
+
*
|
|
4
|
+
* STATUS: Step 7 (reimplementation-plan §5b, feedback R5). Reviewable; control flow validated
|
|
5
|
+
* by scripts/test-workflow.js. Opt-in, not the default. The Codex provider keeps the
|
|
6
|
+
* markdown-skill Lead (hybrid, R3).
|
|
7
|
+
*
|
|
8
|
+
* The retrospective is the ONE place the meta-loop adds genuine *quality*, not just reliable
|
|
9
|
+
* structure (R5): the prose retro is a fixed single pass; here the aggregate review runs
|
|
10
|
+
* LOOP-UNTIL-DRY (keep reviewing until K consecutive rounds surface nothing new) followed by a
|
|
11
|
+
* COMPLETENESS-CRITIC ("which pattern did we not check?"). That is the same rigor that makes
|
|
12
|
+
* CRITIC's 3-pass and JUDGE the strongest existing features, applied to the learning loop.
|
|
13
|
+
*
|
|
14
|
+
* Flow: calibrate (CLI) -> analyze -> aggregate-review (loop-until-dry) -> completeness-critic
|
|
15
|
+
* -> directives (agent proposes; CODE enforces impact gating + the architectural-invariant
|
|
16
|
+
* guard) -> embed (CLI).
|
|
17
|
+
*
|
|
18
|
+
* The deterministic pieces are NOT in this script: calibration arithmetic is
|
|
19
|
+
* `valent-pipeline calibrate` (src/lib/sprint.js); embedding is `valent-pipeline db embed`.
|
|
20
|
+
* Both run through agents (a Workflow script has no CLI/fs access). The directive IMPACT
|
|
21
|
+
* GATING and INVARIANT GUARD are deterministic policy, so they are enforced HERE in code —
|
|
22
|
+
* the agent only proposes; the script decides what gets applied vs. surfaced for approval.
|
|
23
|
+
*
|
|
24
|
+
* args: { batchNumber, sprintId?, storyOutputDirs?: string[], dryRounds?: number, maxRounds?: number }
|
|
25
|
+
* sprintId present => sprint-mode (calibration runs). dryRounds = consecutive empty rounds
|
|
26
|
+
* that end the loop-until-dry (default 2). maxRounds caps it (default 5).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export const meta = {
|
|
30
|
+
name: 'valent-retro',
|
|
31
|
+
description: 'Retrospective: calibrate, loop-until-dry aggregate review, gated directives, embed (Workflow)',
|
|
32
|
+
phases: [
|
|
33
|
+
{ title: 'Calibrate', detail: 'valent-pipeline calibrate (estimation accuracy, in code) — sprint mode' },
|
|
34
|
+
{ title: 'Analyze', detail: 'CRITIC/QA/JUDGE batch outputs + cost' },
|
|
35
|
+
{ title: 'Aggregate', detail: 'loop-until-dry 3-pass aggregate review + completeness critic (R5)' },
|
|
36
|
+
{ title: 'Directives', detail: 'agent proposes; code enforces impact gating + invariant guard' },
|
|
37
|
+
{ title: 'Embed', detail: 'valent-pipeline db embed (persist curated patterns)' },
|
|
38
|
+
],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- schemas (inlined) ---
|
|
42
|
+
|
|
43
|
+
const FINDINGS_SCHEMA = {
|
|
44
|
+
type: 'object',
|
|
45
|
+
required: ['schema', 'findings'],
|
|
46
|
+
additionalProperties: true,
|
|
47
|
+
properties: {
|
|
48
|
+
schema: { const: 1 },
|
|
49
|
+
findings: {
|
|
50
|
+
type: 'array',
|
|
51
|
+
items: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
required: ['id', 'summary'],
|
|
54
|
+
properties: {
|
|
55
|
+
id: { type: 'string' },
|
|
56
|
+
summary: { type: 'string' },
|
|
57
|
+
severity: { type: 'string' },
|
|
58
|
+
stories: { type: 'array', items: { type: 'string' } },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const COMPLETENESS_SCHEMA = {
|
|
66
|
+
type: 'object',
|
|
67
|
+
required: ['schema', 'gaps'],
|
|
68
|
+
additionalProperties: true,
|
|
69
|
+
// gaps = review angles NOT yet covered (e.g. "no security-boundary scan run"). Empty => complete.
|
|
70
|
+
properties: {
|
|
71
|
+
schema: { const: 1 },
|
|
72
|
+
gaps: { type: 'array', items: { type: 'string' } },
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const DIRECTIVES_SCHEMA = {
|
|
77
|
+
type: 'object',
|
|
78
|
+
required: ['schema', 'directives'],
|
|
79
|
+
additionalProperties: true,
|
|
80
|
+
properties: {
|
|
81
|
+
schema: { const: 1 },
|
|
82
|
+
directives: {
|
|
83
|
+
type: 'array',
|
|
84
|
+
items: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
required: ['target_agent', 'directive', 'reason', 'impact_level'],
|
|
87
|
+
properties: {
|
|
88
|
+
target_agent: { type: 'string' },
|
|
89
|
+
directive: { type: 'string' },
|
|
90
|
+
reason: { type: 'string' },
|
|
91
|
+
impact_level: { enum: ['low', 'medium', 'high'] },
|
|
92
|
+
// Agent flags whether the directive touches an Architectural Invariant (skip tests,
|
|
93
|
+
// ship without evidence, weaken a gate, exempt mandatory tests). The CODE decides
|
|
94
|
+
// what to do with that flag — see the gate below.
|
|
95
|
+
touchesInvariant: { type: 'boolean' },
|
|
96
|
+
category: { type: 'string' },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const HANDOFF_SCHEMA = {
|
|
104
|
+
type: 'object',
|
|
105
|
+
required: ['schema'],
|
|
106
|
+
additionalProperties: true,
|
|
107
|
+
properties: { schema: { const: 1 } },
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- args ---
|
|
111
|
+
|
|
112
|
+
const a = args || {}
|
|
113
|
+
const batchNumber = a.batchNumber
|
|
114
|
+
const sprintId = a.sprintId || null
|
|
115
|
+
const dryRounds = a.dryRounds ?? 2
|
|
116
|
+
const maxRounds = a.maxRounds ?? 5
|
|
117
|
+
if (batchNumber == null) throw new Error('args must include { batchNumber }')
|
|
118
|
+
|
|
119
|
+
const retroPrompt = (instruction, returnContract) =>
|
|
120
|
+
`You are **RETROSPECTIVE**, analyzing story batch ${batchNumber} in the valent-pipeline. ` +
|
|
121
|
+
`Read \`.valent-pipeline/prompts/retrospective.md\` and the step file named in the task. ${instruction} ` +
|
|
122
|
+
(returnContract || 'Return your findings as the JSON object specified.')
|
|
123
|
+
|
|
124
|
+
// A stable de-dup key so loop-until-dry converges (don't re-count the same finding).
|
|
125
|
+
const findingKey = (f) => `${(f.summary || '').toLowerCase().trim().slice(0, 80)}`
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
let calibration = null
|
|
130
|
+
if (sprintId) {
|
|
131
|
+
phase('Calibrate')
|
|
132
|
+
// Estimation-accuracy arithmetic lives in code (src/lib/sprint.js); run it via the CLI.
|
|
133
|
+
calibration = await agent(
|
|
134
|
+
`Run exactly: \`valent-pipeline calibrate --sprint ${sprintId}\` in the project root and return its stdout JSON verbatim ` +
|
|
135
|
+
`(fields: ratios, flagged_pairs, surface_averages, velocity). This feeds calibration directives.`,
|
|
136
|
+
{ label: 'calibrate', phase: 'Calibrate', schema: { type: 'object', additionalProperties: true } },
|
|
137
|
+
)
|
|
138
|
+
log(`calibration: ${(calibration.flagged_pairs || []).length} flagged pair(s); velocity unstable=${calibration.velocity?.unstable}`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
phase('Analyze')
|
|
142
|
+
await agent(
|
|
143
|
+
retroPrompt(
|
|
144
|
+
'Run analyze.md: read all CRITIC reviews, QA-B bug reports, JUDGE rejections, and cost data; categorize rejection/bug patterns.',
|
|
145
|
+
'Return ONLY { schema:1, findings:[{id,summary,severity,stories}] } as JSON.',
|
|
146
|
+
),
|
|
147
|
+
{ label: 'analyze', phase: 'Analyze', schema: FINDINGS_SCHEMA },
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
phase('Aggregate')
|
|
151
|
+
// LOOP-UNTIL-DRY (R5): re-run the 3-pass aggregate review until `dryRounds` consecutive
|
|
152
|
+
// rounds surface nothing new, deduping against everything already seen. A simple
|
|
153
|
+
// fixed-pass review (the prose behavior) misses the tail; this does not.
|
|
154
|
+
const seen = new Set()
|
|
155
|
+
const confirmed = []
|
|
156
|
+
let dry = 0
|
|
157
|
+
let round = 0
|
|
158
|
+
while (dry < dryRounds && round < maxRounds) {
|
|
159
|
+
round += 1
|
|
160
|
+
const r = await agent(
|
|
161
|
+
retroPrompt(
|
|
162
|
+
`Run aggregate-review.md (round ${round}): 3-pass CRITIC-style review of the aggregate diff (last retro tag to HEAD) — ` +
|
|
163
|
+
`correctness across story boundaries, convention/pattern drift, architecture/integration. ` +
|
|
164
|
+
`Report ONLY findings not already reported in earlier rounds.`,
|
|
165
|
+
'Return ONLY { schema:1, findings:[{id,summary,severity,stories}] } as JSON.',
|
|
166
|
+
),
|
|
167
|
+
{ label: `aggregate:round-${round}`, phase: 'Aggregate', schema: FINDINGS_SCHEMA },
|
|
168
|
+
)
|
|
169
|
+
const fresh = (r.findings || []).filter((f) => !seen.has(findingKey(f)))
|
|
170
|
+
if (!fresh.length) {
|
|
171
|
+
dry += 1
|
|
172
|
+
log(`aggregate round ${round}: dry (${dry}/${dryRounds})`)
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
dry = 0
|
|
176
|
+
for (const f of fresh) seen.add(findingKey(f))
|
|
177
|
+
confirmed.push(...fresh)
|
|
178
|
+
log(`aggregate round ${round}: +${fresh.length} new finding(s) (${confirmed.length} total)`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// COMPLETENESS-CRITIC (R5): ask what review angle we never ran. Each named gap gets one
|
|
182
|
+
// targeted review round; anything it surfaces joins the confirmed set.
|
|
183
|
+
const critic = await agent(
|
|
184
|
+
retroPrompt(
|
|
185
|
+
`We ran ${round} aggregate-review round(s) and found ${confirmed.length} finding(s). ` +
|
|
186
|
+
`What review angle was NOT covered (e.g. a modality, a security boundary, a contract surface)? ` +
|
|
187
|
+
`List only genuine gaps — empty if coverage is complete.`,
|
|
188
|
+
'Return ONLY { schema:1, gaps:["..."] } as JSON.',
|
|
189
|
+
),
|
|
190
|
+
{ label: 'completeness-critic', phase: 'Aggregate', schema: COMPLETENESS_SCHEMA },
|
|
191
|
+
)
|
|
192
|
+
if ((critic.gaps || []).length) {
|
|
193
|
+
log(`completeness-critic surfaced ${critic.gaps.length} gap(s) — running targeted reviews`)
|
|
194
|
+
const extra = await parallel(
|
|
195
|
+
critic.gaps.map((gap, i) => () =>
|
|
196
|
+
agent(
|
|
197
|
+
retroPrompt(`Targeted aggregate review for the previously-uncovered angle: "${gap}". Report only findings not already reported.`,
|
|
198
|
+
'Return ONLY { schema:1, findings:[{id,summary,severity,stories}] } as JSON.'),
|
|
199
|
+
{ label: `aggregate:gap-${i + 1}`, phase: 'Aggregate', schema: FINDINGS_SCHEMA },
|
|
200
|
+
)),
|
|
201
|
+
)
|
|
202
|
+
for (const r of extra.filter(Boolean)) {
|
|
203
|
+
for (const f of (r.findings || [])) {
|
|
204
|
+
if (!seen.has(findingKey(f))) { seen.add(findingKey(f)); confirmed.push(f) }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
log(`aggregate review complete: ${confirmed.length} confirmed finding(s)`)
|
|
209
|
+
|
|
210
|
+
phase('Directives')
|
|
211
|
+
// The agent PROPOSES directives (with impact_level + a touchesInvariant flag). The CODE
|
|
212
|
+
// enforces the policy — deterministic, uncheatable — per the §5b determinism map:
|
|
213
|
+
// - touchesInvariant -> ARCHITECTURE-CONFLICT: never auto-applied, surfaced to the user
|
|
214
|
+
// - impact_level 'high' -> proposal only, requires user approval
|
|
215
|
+
// - 'low' / 'medium' -> auto-applied (medium also notifies the Lead)
|
|
216
|
+
const drafted = await agent(
|
|
217
|
+
retroPrompt(
|
|
218
|
+
`Run directives.md against the ${confirmed.length} confirmed finding(s)` +
|
|
219
|
+
(calibration ? ' and the calibration metrics' : '') +
|
|
220
|
+
`. For EACH proposed directive set impact_level (low|medium|high) and touchesInvariant=true if it would skip test ` +
|
|
221
|
+
`execution, allow shipping without evidence, weaken a quality gate, or exempt mandatory tests. Do NOT self-censor — ` +
|
|
222
|
+
`propose it and flag it; the orchestrator decides what gets applied.`,
|
|
223
|
+
'Return ONLY { schema:1, directives:[{target_agent,directive,reason,impact_level,touchesInvariant,category}] } as JSON.',
|
|
224
|
+
),
|
|
225
|
+
{ label: 'draft-directives', phase: 'Directives', schema: DIRECTIVES_SCHEMA },
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
const all = drafted.directives || []
|
|
229
|
+
const conflicts = all.filter((d) => d.touchesInvariant)
|
|
230
|
+
const highImpact = all.filter((d) => !d.touchesInvariant && d.impact_level === 'high')
|
|
231
|
+
const applied = all.filter((d) => !d.touchesInvariant && d.impact_level !== 'high')
|
|
232
|
+
const proposals = [...conflicts, ...highImpact]
|
|
233
|
+
|
|
234
|
+
log(`directives: ${applied.length} auto-applied, ${proposals.length} require user approval ` +
|
|
235
|
+
`(${conflicts.length} architecture-conflict, ${highImpact.length} high-impact)`)
|
|
236
|
+
|
|
237
|
+
if (applied.length) {
|
|
238
|
+
await agent(
|
|
239
|
+
`Append these APPROVED correction directives to \`correction-directives.yaml\` (status: active, created_batch: ${batchNumber}). ` +
|
|
240
|
+
`They have passed the impact gate (low/medium only). Directives (JSON): ${JSON.stringify(applied)}. ` +
|
|
241
|
+
`Return { schema:1 } when done.`,
|
|
242
|
+
{ label: 'apply-directives', phase: 'Directives', schema: HANDOFF_SCHEMA },
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
if (proposals.length) {
|
|
246
|
+
// Surfaced, never silently applied — this is the [ARCHITECTURE-CONFLICT] / high-impact path.
|
|
247
|
+
await agent(
|
|
248
|
+
`Write these directive PROPOSALS to \`retrospective-batch-${batchNumber}.md\` under "## Pending Approval" — do NOT add them to ` +
|
|
249
|
+
`correction-directives.yaml. For each, document the proposed directive, why it needs approval (architecture-conflict or high-impact), ` +
|
|
250
|
+
`evidence, risk, and an alternative. Proposals (JSON): ${JSON.stringify(proposals)}. Return { schema:1 } when done.`,
|
|
251
|
+
{ label: 'surface-proposals', phase: 'Directives', schema: HANDOFF_SCHEMA },
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
phase('Embed')
|
|
256
|
+
// Persisting curated knowledge is deterministic ingestion — write the manifest, then run the CLI.
|
|
257
|
+
const embed = await agent(
|
|
258
|
+
`Run embed-instructions.md: write \`embed-instructions.md\` (curated recurring patterns / novel decisions / bug patterns / ` +
|
|
259
|
+
`broadly-applicable directives only — NOT one-offs) in the most recent story output dir, then run ` +
|
|
260
|
+
`\`valent-pipeline db embed --file <that path>\`. Return { schema:1, embedded:<int count> }.`,
|
|
261
|
+
{ label: 'embed', phase: 'Embed', schema: { type: 'object', additionalProperties: true } },
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
batchNumber,
|
|
266
|
+
sprintId,
|
|
267
|
+
aggregate_findings: confirmed.length,
|
|
268
|
+
aggregate_rounds: round,
|
|
269
|
+
completeness_gaps: (critic.gaps || []).length,
|
|
270
|
+
directives_applied: applied.length,
|
|
271
|
+
directives_pending_approval: proposals.length,
|
|
272
|
+
architecture_conflicts: conflicts.length,
|
|
273
|
+
embedded: embed.embedded ?? null,
|
|
274
|
+
}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* valent-pipeline sprint orchestrator — Claude Code (native Workflow) provider.
|
|
3
|
+
*
|
|
4
|
+
* STATUS: Step 6 (sprint loop over the batch + 3b parallel CRITIC + full spawn-context),
|
|
5
|
+
* building on the Step 4 per-story slice. Reviewable; control flow is validated by
|
|
6
|
+
* scripts/test-workflow.js but it has NOT yet been exercised end-to-end against a live
|
|
7
|
+
* story, so it is opt-in, not the default (see README + skills/valent-run-story).
|
|
8
|
+
* The Codex provider keeps the markdown-skill Lead (hybrid, per reimplementation-plan.md
|
|
9
|
+
* R3); this script is the Claude Code deployment over the same shared substrate
|
|
10
|
+
* (prompts/steps/task-graphs/schemas).
|
|
11
|
+
*
|
|
12
|
+
* How it composes the substrate built in steps 1-3:
|
|
13
|
+
* - resolve-graph (step 2) produces each story's stage list — conditional/skip_when
|
|
14
|
+
* predicates evaluated and blockedBy pruned in code, never by model judgment.
|
|
15
|
+
* - gates (readiness/critic/judge) are schema-validated verification stages; the
|
|
16
|
+
* verdict invariant (verdict:pass => highFindingsOpen:0) is enforced both by the
|
|
17
|
+
* schema and by assertGate(), so a gate cannot assert "pass" over open High findings.
|
|
18
|
+
* - CRITIC's three passes (step 3b) run as INDEPENDENT parallel agents — each reads only
|
|
19
|
+
* its own pass step file — then a triage barrier dedups and writes the verdict. This is
|
|
20
|
+
* real perspective-diverse verify; the passes cannot anchor on each other.
|
|
21
|
+
* - the CRITIC rejection loop is a real JS while-loop with a code-owned cap, replacing
|
|
22
|
+
* the model-counted circuit breaker (lead.md) that could miscount.
|
|
23
|
+
* - resume is journal-based for free: relaunch with resumeFromRunId and the unchanged
|
|
24
|
+
* prefix of agent() calls replays from the journal. No disk-state rehydration.
|
|
25
|
+
*
|
|
26
|
+
* Sprint loop: the planned batch runs SEQUENTIALLY (a for-loop, not pipeline()). Executing
|
|
27
|
+
* stories share one git branch, so they cannot overlap without per-story worktrees — the
|
|
28
|
+
* for-loop is the sequentiality guarantee. Parallelism lives WITHIN a story (dev fan-out,
|
|
29
|
+
* CRITIC passes), never across executing stories. A rejected story (JUDGE fail / cap trip)
|
|
30
|
+
* is recorded as rolled-over and the batch continues with the next story.
|
|
31
|
+
*
|
|
32
|
+
* Workflow runtime note: this script body has NO filesystem/import access. Every side
|
|
33
|
+
* effect (running `valent-pipeline resolve-graph`, reading inputs, writing handoffs,
|
|
34
|
+
* git) is performed by the agents it spawns. The script only sequences them and validates
|
|
35
|
+
* their structured returns.
|
|
36
|
+
*
|
|
37
|
+
* args (either form):
|
|
38
|
+
* { stories: [{ storyId, projectType?, profiles? }, ...], projectType?, profiles?, maxRejectionCycles? }
|
|
39
|
+
* { storyId, projectType, profiles?, maxRejectionCycles? } // single-story (back-compat)
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export const meta = {
|
|
43
|
+
name: 'valent-sprint',
|
|
44
|
+
description: 'Run a valent-pipeline sprint batch as a deterministic Workflow with schema-validated quality gates',
|
|
45
|
+
phases: [
|
|
46
|
+
{ title: 'Resolve', detail: 'resolve-graph -> per-story stage list (predicates + pruning in code)' },
|
|
47
|
+
{ title: 'Spec', detail: 'reqs -> uxa -> qa-a' },
|
|
48
|
+
{ title: 'Readiness', detail: 'pre-dev quality gate' },
|
|
49
|
+
{ title: 'Build', detail: 'dev agents in parallel (barrier before CRITIC)' },
|
|
50
|
+
{ title: 'Critic', detail: 'three independent passes in parallel -> triage -> rejection loop (code-owned cap)' },
|
|
51
|
+
{ title: 'QA', detail: 'execute tests against real infra' },
|
|
52
|
+
{ title: 'Judge', detail: 'evidence-based ship decision' },
|
|
53
|
+
],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --- Structured-output schemas (mirror pipeline/schemas/*.json; inlined because a
|
|
57
|
+
// Workflow script cannot read files). The gate schema carries the pass-invariant. ---
|
|
58
|
+
|
|
59
|
+
const HANDOFF_SCHEMA = {
|
|
60
|
+
type: 'object',
|
|
61
|
+
required: ['schema', 'agent', 'story'],
|
|
62
|
+
additionalProperties: true,
|
|
63
|
+
properties: {
|
|
64
|
+
schema: { const: 1 },
|
|
65
|
+
agent: { type: 'string' },
|
|
66
|
+
story: { type: 'string' },
|
|
67
|
+
files: { type: 'array', items: { type: 'string' } },
|
|
68
|
+
nextAgent: { type: ['string', 'null'] },
|
|
69
|
+
flags: { type: 'array', items: { type: 'string' } },
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const VERDICT_SCHEMA = {
|
|
74
|
+
type: 'object',
|
|
75
|
+
required: ['schema', 'agent', 'story', 'verdict', 'highFindingsOpen'],
|
|
76
|
+
additionalProperties: true,
|
|
77
|
+
// Enforced post-validation in code as well (a JSON-Schema-only encoding of an
|
|
78
|
+
// implication is awkward); see assertGate().
|
|
79
|
+
properties: {
|
|
80
|
+
schema: { const: 1 },
|
|
81
|
+
agent: { type: 'string' },
|
|
82
|
+
story: { type: 'string' },
|
|
83
|
+
verdict: { enum: ['pass', 'fail', 'needs-review'] },
|
|
84
|
+
highFindingsOpen: { type: 'integer', minimum: 0 },
|
|
85
|
+
rejectionTarget: { type: ['string', 'null'] },
|
|
86
|
+
files: { type: 'array', items: { type: 'string' } },
|
|
87
|
+
flags: { type: 'array', items: { type: 'string' } },
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// A single CRITIC pass returns its findings (no verdict — only triage decides). Kept loose:
|
|
92
|
+
// passes intentionally overlap, and dedup/severity is triage's job.
|
|
93
|
+
const FINDINGS_SCHEMA = {
|
|
94
|
+
type: 'object',
|
|
95
|
+
required: ['schema', 'agent', 'story', 'pass', 'findings'],
|
|
96
|
+
additionalProperties: true,
|
|
97
|
+
properties: {
|
|
98
|
+
schema: { const: 1 },
|
|
99
|
+
agent: { type: 'string' },
|
|
100
|
+
story: { type: 'string' },
|
|
101
|
+
pass: { enum: ['blind', 'edge', 'acceptance'] },
|
|
102
|
+
findings: {
|
|
103
|
+
type: 'array',
|
|
104
|
+
items: {
|
|
105
|
+
type: 'object',
|
|
106
|
+
required: ['summary'],
|
|
107
|
+
properties: {
|
|
108
|
+
summary: { type: 'string' },
|
|
109
|
+
file: { type: 'string' },
|
|
110
|
+
severity: { enum: ['High', 'Med', 'Low'] },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const RESOLVED_GRAPH_SCHEMA = {
|
|
118
|
+
type: 'object',
|
|
119
|
+
required: ['tasks', 'skipped'],
|
|
120
|
+
properties: {
|
|
121
|
+
tasks: {
|
|
122
|
+
type: 'array',
|
|
123
|
+
items: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
required: ['ref', 'agent', 'blockedBy'],
|
|
126
|
+
properties: {
|
|
127
|
+
ref: { type: 'string' },
|
|
128
|
+
agent: { type: 'string' },
|
|
129
|
+
subject: { type: 'string' },
|
|
130
|
+
description: { type: 'string' },
|
|
131
|
+
blockedBy: { type: 'array', items: { type: 'string' } },
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
skipped: { type: 'array', items: { type: 'string' } },
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const DEV_AGENTS = new Set(['BEND', 'FEND', 'IAC', 'DATA', 'DOCGEN', 'LIBDEV', 'MCP-DEV', 'MOBILE'])
|
|
140
|
+
|
|
141
|
+
// CRITIC's three independent passes (step 3b). Each reads ONLY its own pass step file and
|
|
142
|
+
// the diff/artifacts it is told to — never another pass's output — so they cannot anchor.
|
|
143
|
+
const CRITIC_PASSES = [
|
|
144
|
+
{ pass: 'blind', step: 'blind-hunt.md', reads: 'ONLY the git diff (do NOT read reqs-brief or qa-test-spec)' },
|
|
145
|
+
{ pass: 'edge', step: 'edge-case-hunt.md', reads: 'the diff plus reqs-brief.md (hunt boundary/error/concurrency cases)' },
|
|
146
|
+
{ pass: 'acceptance', step: 'acceptance-audit.md', reads: 'the diff plus qa-test-spec.md and reqs-brief.md (audit every AC)' },
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
// --- arg normalization: accept a batch or a single story ---------------------
|
|
150
|
+
|
|
151
|
+
const a = args || {}
|
|
152
|
+
const batch = Array.isArray(a.stories) && a.stories.length
|
|
153
|
+
? a.stories.map((s) => ({
|
|
154
|
+
storyId: s.storyId,
|
|
155
|
+
projectType: s.projectType || a.projectType,
|
|
156
|
+
profiles: s.profiles || a.profiles || [],
|
|
157
|
+
}))
|
|
158
|
+
: [{ storyId: a.storyId, projectType: a.projectType, profiles: a.profiles || [] }]
|
|
159
|
+
|
|
160
|
+
const maxRejectionCycles = a.maxRejectionCycles ?? 5
|
|
161
|
+
|
|
162
|
+
for (const s of batch) {
|
|
163
|
+
if (!s.storyId || !s.projectType) {
|
|
164
|
+
throw new Error('each story needs { storyId, projectType }; profiles[] optional')
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// --- prompt builder: mirrors providers/claude-code/spawn.template.md so spawned agents
|
|
169
|
+
// get full pipeline context (core prompt + shared context + step-at-execution + the
|
|
170
|
+
// handoff contract), not a terse one-liner. ------------------------------------------
|
|
171
|
+
|
|
172
|
+
function buildPrompt({ role, promptFile, storyId, taskRef, taskSubject, trigger, completion, returnContract }) {
|
|
173
|
+
const outputDir = `stories/${storyId}/output`
|
|
174
|
+
return [
|
|
175
|
+
`You are **${role}**, for story ${storyId} in the valent-pipeline.`,
|
|
176
|
+
'',
|
|
177
|
+
'## Setup',
|
|
178
|
+
`1. Read your core prompt: \`.valent-pipeline/prompts/${promptFile}\` — identity, protocols, step sequence.`,
|
|
179
|
+
`2. Read shared context: \`${outputDir}/pipeline-context.md\` (and correction directives if present).`,
|
|
180
|
+
'3. Read each step file at the point of execution, not before. Check decision gates first.',
|
|
181
|
+
'',
|
|
182
|
+
'## Task Assignment',
|
|
183
|
+
`${taskRef ? `Task ${taskRef}: ` : ''}${taskSubject}`,
|
|
184
|
+
'',
|
|
185
|
+
'## Trigger',
|
|
186
|
+
trigger || 'Begin now.',
|
|
187
|
+
'',
|
|
188
|
+
'## On Completion',
|
|
189
|
+
completion ||
|
|
190
|
+
'Write your handoff artifact as usual.',
|
|
191
|
+
'',
|
|
192
|
+
returnContract ||
|
|
193
|
+
'Return ONLY the fields of your `valent:handoff` machine block as a JSON object.',
|
|
194
|
+
].join('\n')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// A gate's verdict must satisfy the pass-invariant even though the schema can't express the
|
|
198
|
+
// implication directly. This is the KANBAN-002 guard, enforced in the orchestrator.
|
|
199
|
+
function assertGate(v, gate) {
|
|
200
|
+
if (v.verdict === 'pass' && v.highFindingsOpen > 0) {
|
|
201
|
+
throw new Error(`${gate}: illegal verdict — pass with highFindingsOpen=${v.highFindingsOpen}`)
|
|
202
|
+
}
|
|
203
|
+
return v
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
const results = []
|
|
209
|
+
for (let i = 0; i < batch.length; i++) {
|
|
210
|
+
if (i > 0) log(`--- story boundary: shared branch advances to ${batch[i].storyId} (sequential) ---`)
|
|
211
|
+
results.push(await runStory(batch[i]))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const shippedCount = results.filter((r) => r.shipped).length
|
|
215
|
+
log(`sprint complete: ${shippedCount}/${results.length} shipped`)
|
|
216
|
+
return {
|
|
217
|
+
shipped: results.every((r) => r.shipped),
|
|
218
|
+
stories_shipped: shippedCount,
|
|
219
|
+
stories_rolled_over: results.length - shippedCount,
|
|
220
|
+
results,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ===========================================================================
|
|
224
|
+
// runStory: the per-story pipeline (kept inline, not a nested workflow(), so the single
|
|
225
|
+
// workflow() nesting level stays free for plan/retro — see reimplementation-plan §5b).
|
|
226
|
+
async function runStory(story) {
|
|
227
|
+
const { storyId, projectType, profiles } = story
|
|
228
|
+
const profilesCsv = profiles.join(',')
|
|
229
|
+
|
|
230
|
+
phase('Resolve')
|
|
231
|
+
// The script cannot run the CLI itself; an agent runs resolve-graph and returns its JSON.
|
|
232
|
+
const graph = await agent(
|
|
233
|
+
`Run exactly: \`valent-pipeline resolve-graph --type ${projectType} --profiles ${profilesCsv}\` ` +
|
|
234
|
+
`in the project root for story ${storyId} and return its stdout JSON verbatim (fields: tasks, skipped).`,
|
|
235
|
+
{ label: `resolve:${storyId}`, phase: 'Resolve', schema: RESOLVED_GRAPH_SCHEMA },
|
|
236
|
+
)
|
|
237
|
+
const has = (ref) => graph.tasks.some((t) => t.ref === ref)
|
|
238
|
+
const devTasks = graph.tasks.filter((t) => DEV_AGENTS.has(t.agent))
|
|
239
|
+
log(`${storyId}: resolved ${graph.tasks.length} tasks; skipped: ${graph.skipped.join(', ') || 'none'}`)
|
|
240
|
+
|
|
241
|
+
const spawn = (role, promptFile, taskSubject, opts = {}) =>
|
|
242
|
+
agent(buildPrompt({ role, promptFile, storyId, taskSubject, ...opts }), {
|
|
243
|
+
label: opts.label || `${role.toLowerCase()}:${storyId}`,
|
|
244
|
+
phase: opts.phase,
|
|
245
|
+
schema: opts.schema || HANDOFF_SCHEMA,
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
phase('Spec')
|
|
249
|
+
await spawn('REQS', 'reqs.md', 'Analyze the story and produce reqs-brief.md.', { phase: 'Spec' })
|
|
250
|
+
if (has('uxa')) {
|
|
251
|
+
await spawn('UXA', 'uxa.md', 'Translate the brief into uxa-spec.md.', { phase: 'Spec' })
|
|
252
|
+
}
|
|
253
|
+
await spawn('QA-A', 'qa-a.md', 'Produce qa-test-spec.md before any code is written.', { phase: 'Spec' })
|
|
254
|
+
|
|
255
|
+
phase('Readiness')
|
|
256
|
+
await runGate(storyId, 'READINESS', 'readiness.md', 'Validate the spec chain (reqs/uxa/qa) is implementation-ready.',
|
|
257
|
+
'Readiness', (verdict) => {
|
|
258
|
+
const target = verdict.rejectionTarget || 'REQS'
|
|
259
|
+
return spawn(target, `${target.toLowerCase()}.md`, 'Address the READINESS rejection and rewrite the affected spec.', {
|
|
260
|
+
label: `rework:${target.toLowerCase()}:${storyId}`,
|
|
261
|
+
phase: 'Readiness',
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
phase('Build')
|
|
266
|
+
// Genuine barrier: CRITIC needs ALL active dev agents' work before reviewing.
|
|
267
|
+
await parallel(
|
|
268
|
+
devTasks.map((t) => () =>
|
|
269
|
+
spawn(t.agent, `${t.agent.toLowerCase()}.md`, t.subject || 'Implement production code and tests per the brief and test spec.', {
|
|
270
|
+
label: `build:${t.ref}:${storyId}`,
|
|
271
|
+
phase: 'Build',
|
|
272
|
+
})),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
phase('Critic')
|
|
276
|
+
await runCriticGate(storyId, devTasks)
|
|
277
|
+
|
|
278
|
+
phase('QA')
|
|
279
|
+
await spawn('QA-B', 'qa-b.md', 'Execute the full test suite, file bugs, build the traceability matrix.', { phase: 'QA' })
|
|
280
|
+
|
|
281
|
+
phase('Judge')
|
|
282
|
+
const decision = await runGate(storyId, 'JUDGE', 'judge.md',
|
|
283
|
+
'Review evidence (tests, traceability, bugs) and make the ship decision.', 'Judge', null)
|
|
284
|
+
|
|
285
|
+
return { storyId, shipped: decision.verdict === 'pass', verdict: decision.verdict, skipped: graph.skipped }
|
|
286
|
+
|
|
287
|
+
// --- per-story closures over storyId/devTasks ----------------------------
|
|
288
|
+
|
|
289
|
+
// runGate: a schema-validated verification stage with a code-owned rejection loop.
|
|
290
|
+
// `reworkThunk` (or null for terminal gates) produces the fix work before re-gating.
|
|
291
|
+
async function runGate(sid, role, promptFile, instruction, gatePhase, reworkThunk) {
|
|
292
|
+
let rejections = 0
|
|
293
|
+
while (true) {
|
|
294
|
+
const verdict = assertGate(
|
|
295
|
+
await spawn(role, promptFile, instruction, { label: `gate:${role.toLowerCase()}:${sid}`, phase: gatePhase, schema: VERDICT_SCHEMA }),
|
|
296
|
+
role,
|
|
297
|
+
)
|
|
298
|
+
if (verdict.verdict === 'pass') return verdict
|
|
299
|
+
if (!reworkThunk) return verdict // terminal gate (JUDGE): reject is the answer, Lead diagnoses
|
|
300
|
+
rejections += 1
|
|
301
|
+
if (rejections >= maxRejectionCycles) {
|
|
302
|
+
log(`${sid}/${role}: circuit breaker tripped after ${rejections} rejections — escalating`)
|
|
303
|
+
return { ...verdict, escalated: true }
|
|
304
|
+
}
|
|
305
|
+
log(`${sid}/${role}: rejection ${rejections}/${maxRejectionCycles} — reworking`)
|
|
306
|
+
await reworkThunk(verdict)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// runCriticGate (step 3b): three INDEPENDENT passes in parallel, then a triage barrier
|
|
311
|
+
// that dedups and writes the verdict. The whole thing is wrapped in the code-owned
|
|
312
|
+
// rejection loop; on reject, the routed dev agents rework and the passes re-run.
|
|
313
|
+
async function runCriticGate(sid, devs) {
|
|
314
|
+
let rejections = 0
|
|
315
|
+
while (true) {
|
|
316
|
+
// Independent perspective-diverse verify: each pass reads only its own inputs.
|
|
317
|
+
await parallel(
|
|
318
|
+
CRITIC_PASSES.map((p) => () =>
|
|
319
|
+
spawn('CRITIC', 'critic.md',
|
|
320
|
+
`Run pass ${p.pass} per \`.valent-pipeline/steps/critic/${p.step}\`. Read ${p.reads}. ` +
|
|
321
|
+
`Do NOT read any other pass's output. Record findings only — do NOT deduplicate or set a verdict.`,
|
|
322
|
+
{
|
|
323
|
+
label: `critic:${p.pass}:${sid}`,
|
|
324
|
+
phase: 'Critic',
|
|
325
|
+
schema: FINDINGS_SCHEMA,
|
|
326
|
+
returnContract: 'Return ONLY { schema:1, agent:"critic", story, pass, findings:[...] } as JSON.',
|
|
327
|
+
})),
|
|
328
|
+
)
|
|
329
|
+
// Triage barrier: the single point of deduplication; produces the schema-validated verdict.
|
|
330
|
+
const verdict = assertGate(
|
|
331
|
+
await spawn('CRITIC', 'critic.md',
|
|
332
|
+
'Triage per `.valent-pipeline/steps/critic/triage.md`: gather findings from ALL three passes, ' +
|
|
333
|
+
'collapse duplicates (same root cause) into one, classify final severity, then write the verdict.',
|
|
334
|
+
{ label: `gate:critic:${sid}`, phase: 'Critic', schema: VERDICT_SCHEMA }),
|
|
335
|
+
'CRITIC',
|
|
336
|
+
)
|
|
337
|
+
if (verdict.verdict === 'pass') return verdict
|
|
338
|
+
rejections += 1
|
|
339
|
+
if (rejections >= maxRejectionCycles) {
|
|
340
|
+
log(`${sid}/CRITIC: circuit breaker tripped after ${rejections} rejections — escalating`)
|
|
341
|
+
return { ...verdict, escalated: true }
|
|
342
|
+
}
|
|
343
|
+
log(`${sid}/CRITIC: rejection ${rejections}/${maxRejectionCycles} — reworking`)
|
|
344
|
+
// Route fixes to the owning dev agent(s), then re-run the passes.
|
|
345
|
+
await parallel(
|
|
346
|
+
devs.map((t) => () =>
|
|
347
|
+
spawn(t.agent, `${t.agent.toLowerCase()}.md`, 'Fix every High finding CRITIC routed to you.', {
|
|
348
|
+
label: `rework:${t.ref}:${sid}`,
|
|
349
|
+
phase: 'Critic',
|
|
350
|
+
})),
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Codex orchestrator (thin prose Lead)
|
|
2
|
+
|
|
3
|
+
This is the Codex deployment of the valent-pipeline orchestrator, per the hybrid target in
|
|
4
|
+
[`../../../docs-feedback/reimplementation-plan.md`](../../../docs-feedback/reimplementation-plan.md)
|
|
5
|
+
(R3): the Claude Code provider runs a deterministic **Workflow script**
|
|
6
|
+
(`../claude-code/`), while the Codex provider runs a **thin prose Lead loop** — because Codex
|
|
7
|
+
has no Workflow / team / inbox / cron primitives and genuinely needs an explicit loop. Both
|
|
8
|
+
consume the same shared substrate (`prompts/`, `steps/`, `task-graphs/`, `schemas/`, templates).
|
|
9
|
+
|
|
10
|
+
## The file
|
|
11
|
+
|
|
12
|
+
| File | Role |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `lead-loop.md` | The thin Codex orchestrator: resolve the DAG (CLI) → walk it honoring `blockedBy`, spawning Codex threads → validate each handoff (CLI) → gates with a code-owned rejection cap (CLI) → steer-when-alive. |
|
|
15
|
+
|
|
16
|
+
## Status
|
|
17
|
+
|
|
18
|
+
`lead-loop.md` is the **greenfield thin shell** that will replace the 1207-line `prompts/lead.md`
|
|
19
|
+
for Codex. It is **opt-in and not the default**: `prompts/lead.md` remains the production Codex
|
|
20
|
+
orchestrator until the step-9 live validation passes and step-11 cutover deletes it. It was
|
|
21
|
+
written *from the plan*, not by trimming `lead.md` (the plan §0 rule — reading 1000+ lines of
|
|
22
|
+
prose control flow re-absorbs the drift we're removing).
|
|
23
|
+
|
|
24
|
+
## What it demonstrates (parity with the Claude Code engine, over the same substrate)
|
|
25
|
+
|
|
26
|
+
| Concern | How (Codex shell) | Claude Code equivalent |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| DAG resolution | `valent-pipeline resolve-graph` (predicates + pruning in code) | same CLI, via an agent |
|
|
29
|
+
| Quality gates | `valent-pipeline validate-handoff --gate` (schema + pass-invariant) | `agent(_, { schema })` + `assertGate()` |
|
|
30
|
+
| Rejection cap | `valent-pipeline rejection-cap --increment` (file-backed counter, non-zero exit when tripped) | JS `while (rejections < cap)` |
|
|
31
|
+
| Spawn vs. steer | **decided once**: steer-when-alive, spawn-when-not | n/a (every `agent()` is a fresh spawn) |
|
|
32
|
+
| State of record | `task-registry.yaml` + handoff files on disk | the run journal (`resumeFromRunId`) |
|
|
33
|
+
|
|
34
|
+
The point of the hybrid: the **deterministic decisions are the same CLIs** in both shells. The
|
|
35
|
+
shells differ only in their spawn/await primitives (JS `agent()`/`parallel()` vs. Codex threads).
|
|
36
|
+
|
|
37
|
+
## The contradiction this resolves
|
|
38
|
+
|
|
39
|
+
`lead.md`'s Codex rejection path said *spawn a new subagent* on rejection, while
|
|
40
|
+
`providers/codex/runtime.md` (Thread Persistence) and `lead.md`'s own sprint-mode `[STORY-RESET]`
|
|
41
|
+
rule say *steer the persistent thread*. `lead-loop.md` decides it once — **steer-when-alive,
|
|
42
|
+
spawn-when-not** — and `lead.md`'s lone contradictory line is reconciled (see that file's Codex
|
|
43
|
+
rejection section). The duplicated legacy prose is deleted at cutover.
|
|
44
|
+
|
|
45
|
+
## Known simplifications (next slices)
|
|
46
|
+
|
|
47
|
+
- The thin shell covers the per-story execute loop. The sprint/plan/retro meta loop for Codex
|
|
48
|
+
reuses the same CLIs (`sprint-pack` / `calibrate` / `validate-sprint` / `db embed`) the prose
|
|
49
|
+
Lead already calls; folding those into a thin `plan`/`retro` prose loop mirrors the Claude Code
|
|
50
|
+
`plan.workflow.js` / `retro.workflow.js` and lands with cutover.
|
|
51
|
+
- Cloud-multi thread persistence is handled by the spawn-when-not branch (threads can't cross
|
|
52
|
+
container boundaries), matching `runtime.md`'s documented fallback.
|