Skip to content

Commit 1848c69

Browse files
elberrdclaude
andauthored
feat: fda_sdlc enxuto — repair loop in-run, review só com suite verde, planner condicional e docs por milestone (#47)
O runner default do /goal era o único FDA testado sem loop de reparo e sem tracker de progresso: cada suite vermelha virava recovery por resume do orquestrador (re-retomando todas as sessões de agente na largura máxima), e o review rodava mesmo com teste vermelho — a mesma classe de bug do audit do /qa (caso de US$13/10,5M tokens). Medido em projeto real: 679M tokens em 31 runs de fda_sdlc, com um run de 16 ciclos de build (112M/US$79). - Repair loop in-run portado do fda_build_test (createRepairTracker, test_1..n, fix_i com a regra repairExecuted; NO_PROGRESS/ATTEMPT_CAP no lugar de resume). - review atrás de test.passed, com fase gratuita review_skip (nome distinto — resume pós-fix roda o review real), espelhando o audit_skip do /qa. - Planner condicional (sdlc.plan: auto|always|never): brief autocontido do sequencer pula o planner; Kind: foundation/kit e Planning: full sempre planejam. - Documentação por milestone (sdlc.document: per_milestone default): o /goal drena docs com UM fda_document na virada de milestone; per_task mantém o comportamento antigo. - builderDeclaredFiles extraído para modules/utils.mjs (5 cópias viram 1, com pattern por runner). - Config template, goal.md, cookbooks (run_fda, harness_bridge) e DOCS.md §9 atualizados; testes novos em test/fia-sdlc-repair.test.js. Claude-Session: https://claude.ai/code/session_01NPYaKxNaVRhPumKbawFNT3 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 03292a1 commit 1848c69

14 files changed

Lines changed: 478 additions & 199 deletions

File tree

DOCS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1146,7 +1146,7 @@ same name lands on the session row (§9.7). A run stopped by a policy limit
11461146
| `fda_document` | documenter | request → document | Write up recent changes (docs paths only). |
11471147
| `fda_quality` | none | request → *quality* | Lint + typecheck + build + test with no agent — works with nothing logged in. |
11481148
| `fda_plan_build_test` | planner, builder (+reviewer for the UI gate) | request → plan → build → *test* → up to `stop.attempt_cap` × (fix → *test*), default 3, cut short by `no_progress`*spec_coverage* → checklist gate → UI gate → *holdout**commit* | The task workhorse (`/task` uses it via the sequencer). |
1149-
| `fda_sdlc` | planner, builder, reviewer, documenter | request → plan → build → *test* (single run, no fix loop) → *spec_coverage* → checklist gate → UI gate → *holdout* → review → *commit_code* → document → *commit_docs* | Full cycle with an independent review — the review runs even when tests failed, and acceptance requires green tests AND an approved review. |
1149+
| `fda_sdlc` | planner, builder, reviewer, documenter | request → plan (or free *plan_skip* when the brief is self-contained — `sdlc.plan: auto`) → build → *test_1* + fix loop (up to `stop.attempt_cap` rounds, same tracker as the siblings) → *spec_coverage* → checklist gate → UI gate → *holdout* → review (green suites only; a red suite records the free *review_skip* instead) *commit_code* → document (only with `sdlc.document: per_task`; the default `per_milestone` records the free *document_skip* and `/goal` drains docs with one `fda_document` per milestone) *commit_docs* | Full cycle with an independent review — acceptance requires green tests AND an approved review; a suite still red after the fix rounds ends the run as `attempt_cap` (or `no_progress` when stalled) without paying the reviewer. |
11501150
| `fda_bug` | planner, builder (+reviewer) | request → plan → red_test → *red_check* → build → *test* → fix loop (≤ `stop.attempt_cap`, default 3; `no_progress` ends it early) → gates → *commit* | Defect fixing with a **valid RED** gate: the reproduction test must fail on an assertion BEFORE the fix (passing = "bug not reproduced"; module/syntax/env failures = invalid RED). |
11511151
| `fda_quick` | builder | request → build → *quality_1* (lint+typecheck+focal test) → one fix round (by design, not from `stop:`; still red = `attempt_cap`) → *quality_2**quicklog**commit* | Small guarded changes (`/quick`). Appends the `## Q-NNN` audit entry, then stamps the commit sha into it as a separate one-line commit. |
11521152
| `fda_build_test` | builder (+reviewer for the UI gate) | request → build → *test* → up to `stop.attempt_cap` × (fix → *test*), default 3, cut short by `no_progress`*spec_coverage* → checklist gate → UI gate → *holdout**commit* | Like `fda_plan_build_test` but **without the planner** — the brief goes directly to the builder. Saves ~1-2.5M tokens per run when the brief is already autocontained. |

fia-templates/fda_bug.mjs

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env node
22
/** FDA Bug — plan → failing reproduction (valid RED) → fix → green suite → commit. */
3-
import { existsSync, readdirSync, readFileSync } from 'node:fs';
3+
import { existsSync, readFileSync } from 'node:fs';
44
import { join, resolve } from 'node:path';
55
import { runFda, phaseParams } from './modules/fda-cli.mjs';
66
import { artifactsExist, filesNonEmpty, validateRedReason, parseSpecLine, checkSpecCoverage, checkSpecDiagram } from './modules/gates.mjs';
@@ -11,32 +11,11 @@ import { floorPath } from './modules/floor.mjs';
1111
import { runSpecDeliveryClose } from './modules/spec-lifecycle.mjs';
1212
import { OUTCOMES } from './modules/outcome.mjs';
1313
import { changedContentSignature, createRepairTracker } from './modules/stop.mjs';
14+
import { builderDeclaredFiles } from './modules/utils.mjs';
1415
import * as git from './modules/git-helper.mjs';
1516

16-
/**
17-
* Files declared by EVERY persisted builder envelope (red_test + build +
18-
* fix_N). Same reason as fda_plan_build_test: on resume the in-memory
19-
* envelopes may not cover earlier rounds — phase_results always do.
20-
*/
21-
function builderDeclaredFiles(run) {
22-
const files = [];
23-
let names = [];
24-
try {
25-
names = readdirSync(run.phaseResultsDir);
26-
} catch {
27-
/* no phase results dir — nothing persisted */
28-
}
29-
for (const name of names) {
30-
if (!/^(red_test|build|fix_\d+|fix_checklist|fix_ui)\.json$/.test(name)) continue;
31-
try {
32-
const saved = JSON.parse(readFileSync(join(run.phaseResultsDir, name), 'utf8'));
33-
files.push(...(saved.result?.changed_files || []), ...(saved.result?.artifacts || []));
34-
} catch {
35-
/* unreadable phase result — the in-memory envelopes still cover the rest */
36-
}
37-
}
38-
return files;
39-
}
17+
/** This runner's builder rounds include the RED reproduction test. */
18+
const BUG_RESULT_FILES = /^(red_test|build|fix_\d+|fix_checklist|fix_ui)\.json$/;
4019

4120
/** A phase that already ran in this fda_id (its result file is on disk). */
4221
function phaseAlreadyRan(run, name) {
@@ -280,7 +259,7 @@ await runFda(
280259
...redFiles,
281260
...(previous.changed_files || []),
282261
...(previous.artifacts || []),
283-
...builderDeclaredFiles(run),
262+
...builderDeclaredFiles(run, BUG_RESULT_FILES),
284263
...(specClose.changed_files || []),
285264
// The regression floor rises on a green suite (modules/floor.mjs)
286265
// and rides the SAME commit as the work that raised it.

fia-templates/fda_build_test.mjs

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
#!/usr/bin/env node
22
/** FDA Build Test — build → test → commit (no planner, brief goes directly to builder). */
3-
import { readdirSync, readFileSync } from 'node:fs';
4-
import { join } from 'node:path';
53
import { runFda, phaseParams } from './modules/fda-cli.mjs';
64
import { artifactsExist, parseSpecLine, checkSpecCoverage, checkSpecDiagram, isFoundationBrief } from './modules/gates.mjs';
75
import { resolveBriefPath, runChecklistGate } from './modules/checklist.mjs';
@@ -12,34 +10,9 @@ import { runHoldoutGate } from './modules/holdout.mjs';
1210
import { runSpecDeliveryClose } from './modules/spec-lifecycle.mjs';
1311
import { OUTCOMES } from './modules/outcome.mjs';
1412
import { changedContentSignature, createRepairTracker } from './modules/stop.mjs';
13+
import { builderDeclaredFiles } from './modules/utils.mjs';
1514
import * as git from './modules/git-helper.mjs';
1615

17-
/**
18-
* Files declared by EVERY persisted builder envelope (build + fix_N). On resume
19-
* the in-memory `previous` can be just the replayed build envelope (test_1
20-
* re-runs and passes because the fix is already on disk, so the fix loop never
21-
* executes) — files touched by earlier fix rounds live only in phase_results.
22-
*/
23-
function builderDeclaredFiles(run) {
24-
const files = [];
25-
let names = [];
26-
try {
27-
names = readdirSync(run.phaseResultsDir);
28-
} catch {
29-
/* no phase results dir — nothing persisted */
30-
}
31-
for (const name of names) {
32-
if (!/^(build|fix_\d+|fix_checklist|fix_ui)\.json$/.test(name)) continue;
33-
try {
34-
const saved = JSON.parse(readFileSync(join(run.phaseResultsDir, name), 'utf8'));
35-
files.push(...(saved.result?.changed_files || []), ...(saved.result?.artifacts || []));
36-
} catch {
37-
/* unreadable phase result — the in-memory envelopes still cover the rest */
38-
}
39-
}
40-
return files;
41-
}
42-
4316
await runFda(
4417
async ({ run, prompt, args }) => {
4518
// The repair cap and the no-progress window come from the student's

fia-templates/fda_plan_build_test.mjs

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
#!/usr/bin/env node
22
/** FDA Plan Build Test — plan → build → test → commit. */
3-
import { readdirSync, readFileSync } from 'node:fs';
4-
import { join } from 'node:path';
53
import { runFda, phaseParams } from './modules/fda-cli.mjs';
64
import { artifactsExist, filesNonEmpty, parseSpecLine, checkSpecCoverage, checkSpecDiagram, isFoundationBrief } from './modules/gates.mjs';
75
import { resolveBriefPath, runChecklistGate } from './modules/checklist.mjs';
@@ -12,34 +10,9 @@ import { runHoldoutGate } from './modules/holdout.mjs';
1210
import { runSpecDeliveryClose } from './modules/spec-lifecycle.mjs';
1311
import { OUTCOMES } from './modules/outcome.mjs';
1412
import { changedContentSignature, createRepairTracker } from './modules/stop.mjs';
13+
import { builderDeclaredFiles } from './modules/utils.mjs';
1514
import * as git from './modules/git-helper.mjs';
1615

17-
/**
18-
* Files declared by EVERY persisted builder envelope (build + fix_N). On resume
19-
* the in-memory `previous` can be just the replayed build envelope (test_1
20-
* re-runs and passes because the fix is already on disk, so the fix loop never
21-
* executes) — files touched by earlier fix rounds live only in phase_results.
22-
*/
23-
function builderDeclaredFiles(run) {
24-
const files = [];
25-
let names = [];
26-
try {
27-
names = readdirSync(run.phaseResultsDir);
28-
} catch {
29-
/* no phase results dir — nothing persisted */
30-
}
31-
for (const name of names) {
32-
if (!/^(build|fix_\d+|fix_checklist|fix_ui)\.json$/.test(name)) continue;
33-
try {
34-
const saved = JSON.parse(readFileSync(join(run.phaseResultsDir, name), 'utf8'));
35-
files.push(...(saved.result?.changed_files || []), ...(saved.result?.artifacts || []));
36-
} catch {
37-
/* unreadable phase result — the in-memory envelopes still cover the rest */
38-
}
39-
}
40-
return files;
41-
}
42-
4316
await runFda(
4417
async ({ run, prompt, args }) => {
4518
// The repair cap and the no-progress window come from the student's

fia-templates/fda_prototype.mjs

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,14 @@
11
#!/usr/bin/env node
22
/** FDA Prototype — build → lint/typecheck → commit (no plan, no tests, no review). */
3-
import { readdirSync, readFileSync } from 'node:fs';
4-
import { join } from 'node:path';
53
import { runFda, phaseParams } from './modules/fda-cli.mjs';
64
import { artifactsExist, isFoundationBrief, isPrototypeBrief } from './modules/gates.mjs';
75
import { defaultSpecs, runQuality, asEnvelope } from './modules/quality.mjs';
86
import { OUTCOMES } from './modules/outcome.mjs';
7+
import { builderDeclaredFiles } from './modules/utils.mjs';
98
import * as git from './modules/git-helper.mjs';
109

11-
function builderDeclaredFiles(run) {
12-
const files = [];
13-
let names = [];
14-
try {
15-
names = readdirSync(run.phaseResultsDir);
16-
} catch {
17-
/* no phase results dir */
18-
}
19-
for (const name of names) {
20-
if (!/^(build|fix)\.json$/.test(name)) continue;
21-
try {
22-
const saved = JSON.parse(readFileSync(join(run.phaseResultsDir, name), 'utf8'));
23-
files.push(...(saved.result?.changed_files || []), ...(saved.result?.artifacts || []));
24-
} catch {
25-
/* unreadable phase result */
26-
}
27-
}
28-
return files;
29-
}
10+
/** This runner has a single fix round, persisted as `fix.json`. */
11+
const PROTOTYPE_RESULT_FILES = /^(build|fix)\.json$/;
3012

3113
function protoSpecs(projectRoot) {
3214
return defaultSpecs(projectRoot).filter((s) => s.name === 'lint' || s.name === 'typecheck');
@@ -90,7 +72,7 @@ await runFda(
9072
...new Set([
9173
...(build.changed_files || []),
9274
...(build.artifacts || []),
93-
...builderDeclaredFiles(run),
75+
...builderDeclaredFiles(run, PROTOTYPE_RESULT_FILES),
9476
...(isFoundationBrief(prompt) ? git.runChangedPaths(run.repoRoot, run.baseline) : []),
9577
]),
9678
];

fia-templates/fda_quick.mjs

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,17 @@
11
#!/usr/bin/env node
22
/** FDA Quick — guardrailed small change: build → quality → quick-log → commit. */
3-
import { readdirSync, readFileSync } from 'node:fs';
3+
import { readFileSync } from 'node:fs';
44
import { join } from 'node:path';
55
import { runFda, phaseParams } from './modules/fda-cli.mjs';
66
import { artifactsExist } from './modules/gates.mjs';
77
import { defaultSpecs, focalSpec, runQuality, asEnvelope } from './modules/quality.mjs';
88
import { appendQuickLog, setQuickLogCommit } from './modules/quicklog.mjs';
99
import { OUTCOMES } from './modules/outcome.mjs';
10+
import { builderDeclaredFiles } from './modules/utils.mjs';
1011
import * as git from './modules/git-helper.mjs';
1112

12-
/** Files declared by the persisted builder envelopes (build + fix) — see fda_plan_build_test. */
13-
function builderDeclaredFiles(run) {
14-
const files = [];
15-
let names = [];
16-
try {
17-
names = readdirSync(run.phaseResultsDir);
18-
} catch {
19-
/* no phase results dir — nothing persisted */
20-
}
21-
for (const name of names) {
22-
if (!/^(build|fix)\.json$/.test(name)) continue;
23-
try {
24-
const saved = JSON.parse(readFileSync(join(run.phaseResultsDir, name), 'utf8'));
25-
files.push(...(saved.result?.changed_files || []), ...(saved.result?.artifacts || []));
26-
} catch {
27-
/* unreadable phase result — the in-memory envelopes still cover the rest */
28-
}
29-
}
30-
return files;
31-
}
13+
/** This runner has a single fix round, persisted as `fix.json`. */
14+
const QUICK_RESULT_FILES = /^(build|fix)\.json$/;
3215

3316
/** The persisted result of an earlier phase of this fda_id, or null. */
3417
function savedPhaseResult(run, name) {
@@ -105,7 +88,7 @@ await runFda(
10588

10689
if (quality.passed) {
10790
const files = [
108-
...new Set([...(build.changed_files || []), ...(build.artifacts || []), ...builderDeclaredFiles(run)]),
91+
...new Set([...(build.changed_files || []), ...(build.artifacts || []), ...builderDeclaredFiles(run, QUICK_RESULT_FILES)]),
10992
];
11093
const logPath = join(process.env.FIA_AI_DOCS || 'ai-docs', 'todos', 'quick-log.md');
11194
const logged = await run.runPhase(

0 commit comments

Comments
 (0)