@llm4ts/flow 2.50.0 → 2.52.0
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/dist/FlowEvents.d.ts +2 -0
- package/dist/FlowEvents.d.ts.map +1 -1
- package/dist/FlowEvents.js +3 -1
- package/dist/FlowEvents.js.map +1 -1
- package/dist/Recheck.d.ts +69 -0
- package/dist/Recheck.d.ts.map +1 -0
- package/dist/Recheck.js +111 -0
- package/dist/Recheck.js.map +1 -0
- package/dist/Review.d.ts +32 -33
- package/dist/Review.d.ts.map +1 -1
- package/dist/Review.js +104 -45
- package/dist/Review.js.map +1 -1
- package/dist/ReviewCache.d.ts +1 -1
- package/dist/ReviewCache.d.ts.map +1 -1
- package/dist/ReviewCache.js +1 -1
- package/dist/ReviewCache.js.map +1 -1
- package/dist/ReviewModel.d.ts +36 -0
- package/dist/ReviewModel.d.ts.map +1 -0
- package/dist/ReviewModel.js +50 -0
- package/dist/ReviewModel.js.map +1 -0
- package/dist/Stories.d.ts +36 -2
- package/dist/Stories.d.ts.map +1 -1
- package/dist/Stories.js +255 -90
- package/dist/Stories.js.map +1 -1
- package/package.json +4 -2
- package/src/FlowEvents.ts +3 -1
- package/src/Recheck.ts +160 -0
- package/src/Review.ts +178 -58
- package/src/ReviewCache.ts +1 -1
- package/src/ReviewModel.ts +63 -0
- package/src/Stories.ts +270 -18
package/src/Stories.ts
CHANGED
|
@@ -20,7 +20,7 @@ import type { LlmChunk, TokenUsage } from "@llm4ts/core/Models"
|
|
|
20
20
|
import type { LlmError } from "@llm4ts/core/Errors"
|
|
21
21
|
import { BoardItem, eventedBoard, type BoardSyncShape } from "./BoardSync.ts"
|
|
22
22
|
import { makeChat } from "./Chat.ts"
|
|
23
|
-
import { implementPlanFlow, type TaskOverrides } from "./Flow.ts"
|
|
23
|
+
import { flowReviewer, implementPlanFlow, type TaskOverrides } from "./Flow.ts"
|
|
24
24
|
import {
|
|
25
25
|
GateBaseline,
|
|
26
26
|
baselineKey,
|
|
@@ -77,6 +77,8 @@ import {
|
|
|
77
77
|
applyTriage,
|
|
78
78
|
isBlocking,
|
|
79
79
|
loadRepoReviewRules,
|
|
80
|
+
minimalReviewers,
|
|
81
|
+
reviewAndFixLoop,
|
|
80
82
|
nonBlockingIssues,
|
|
81
83
|
type FixPromptOptions,
|
|
82
84
|
type GateTriageOptions
|
|
@@ -87,6 +89,7 @@ import {
|
|
|
87
89
|
defaultOracleRules,
|
|
88
90
|
parseUnifiedDiff
|
|
89
91
|
} from "./OracleGuard.ts"
|
|
92
|
+
import { ReviewMemory } from "./Recheck.ts"
|
|
90
93
|
import { cachedValue, fingerprintOf } from "./ReviewCache.ts"
|
|
91
94
|
import { withContract, type ContractProfile } from "./AutonomyContract.ts"
|
|
92
95
|
import { fabricatedStatusIssues, unverifiedClaims } from "./Evidence.ts"
|
|
@@ -116,8 +119,18 @@ const join = (root: string, path: string): string =>
|
|
|
116
119
|
|
|
117
120
|
/** A flow context rooted in a story's worktree, plus that story's own usage totals. */
|
|
118
121
|
/** A story judge's review with the rubric's scores; a plain `ReviewResult` has none. */
|
|
122
|
+
/** A dimension below the bar the story was not held to, with the judge's reasoning (ADR 0045). */
|
|
123
|
+
export const ExcusedDimension = Schema.Struct({
|
|
124
|
+
id: Schema.String,
|
|
125
|
+
score: Schema.Number,
|
|
126
|
+
max: Schema.Number,
|
|
127
|
+
reasoning: Schema.String
|
|
128
|
+
})
|
|
129
|
+
export type ExcusedDimension = typeof ExcusedDimension.Type
|
|
130
|
+
|
|
119
131
|
export class StoryVerdict extends ReviewResult.extend<StoryVerdict>("StoryVerdict")({
|
|
120
|
-
dimensions: Schema.Array(JudgedDimension)
|
|
132
|
+
dimensions: Schema.Array(JudgedDimension),
|
|
133
|
+
excused: Schema.optionalKey(Schema.Array(ExcusedDimension))
|
|
121
134
|
}) {}
|
|
122
135
|
|
|
123
136
|
/** What a judge answers: a scored verdict, or a plain review result. */
|
|
@@ -752,6 +765,14 @@ export interface StoriesOptions {
|
|
|
752
765
|
* story's plan (coder, review, gates, commit). Default 2: one revision.
|
|
753
766
|
*/
|
|
754
767
|
readonly judgeRounds?: number
|
|
768
|
+
/**
|
|
769
|
+
* Where the review lenses run (ADR 0044): each task (default), or once per
|
|
770
|
+
* story, beside the judge, with incremental re-checks and one fix turn per
|
|
771
|
+
* round.
|
|
772
|
+
*/
|
|
773
|
+
readonly reviewAt?: "task" | "story"
|
|
774
|
+
/** Story mode: rounds of the merged story review. Default 3. Task mode ignores it. */
|
|
775
|
+
readonly reviewRounds?: number
|
|
755
776
|
/**
|
|
756
777
|
* When the epic gates are red after a story merges (the merge is undone),
|
|
757
778
|
* the story goes back to its worktree this many times: it catches up with
|
|
@@ -809,6 +830,12 @@ export interface StoriesOptions {
|
|
|
809
830
|
* for a follow-up round. Default false: every finding is fixed or fails.
|
|
810
831
|
*/
|
|
811
832
|
readonly deferNonBlocking?: boolean
|
|
833
|
+
/**
|
|
834
|
+
* Names the bar the judge holds stories to when it is not the default
|
|
835
|
+
* (`defer-tests`, ADR 0045). It enters the judge's cache fingerprint, so a
|
|
836
|
+
* verdict judged under one bar is never reused under another.
|
|
837
|
+
*/
|
|
838
|
+
readonly judgeBar?: string
|
|
812
839
|
/**
|
|
813
840
|
* Second opinion on a BLOCKED_ON claim the plan cannot settle (a path no
|
|
814
841
|
* story owns, or a parallel story's). `real: false` sends the coder back
|
|
@@ -857,6 +884,31 @@ const combined = (first: ReviewResult, second: ReviewResult): ReviewResult =>
|
|
|
857
884
|
export const deferredPath = (stateDir: string, storyId: string): string =>
|
|
858
885
|
join(stateDir, `stories/${storyId}.deferred.md`)
|
|
859
886
|
|
|
887
|
+
/** Where a story review keeps its reviewers' memory between rounds and runs (ADR 0044). */
|
|
888
|
+
export const reviewStatePath = (stateDir: string, storyId: string): string =>
|
|
889
|
+
join(stateDir, `stories/${storyId}.review-state.json`)
|
|
890
|
+
|
|
891
|
+
/** Where a story's last judge verdict is kept, beside a fingerprint of what it judged. */
|
|
892
|
+
export const judgeVerdictPath = (stateDir: string, storyId: string): string =>
|
|
893
|
+
join(stateDir, `stories/${storyId}.judge.json`)
|
|
894
|
+
|
|
895
|
+
/** A story's last judge verdict, when it was a scored one (ADR 0045: what the debt reads). */
|
|
896
|
+
export const lastStoryVerdict = (
|
|
897
|
+
files: PlainFileStoreShape,
|
|
898
|
+
stateDir: string,
|
|
899
|
+
storyId: string
|
|
900
|
+
): Effect.Effect<StoryVerdict | undefined> =>
|
|
901
|
+
files.read(judgeVerdictPath(stateDir, storyId)).pipe(
|
|
902
|
+
Effect.flatMap((text) =>
|
|
903
|
+
text === undefined
|
|
904
|
+
? Effect.succeed(undefined)
|
|
905
|
+
: Schema.decodeUnknownEffect(
|
|
906
|
+
Schema.fromJsonString(Schema.Struct({ result: StoryVerdict }))
|
|
907
|
+
)(text).pipe(Effect.map((entry) => entry.result))
|
|
908
|
+
),
|
|
909
|
+
Effect.catch(() => Effect.succeed(undefined))
|
|
910
|
+
)
|
|
911
|
+
|
|
860
912
|
/** A deferred finding as one Markdown bullet, prefixed with where it was found. */
|
|
861
913
|
export const deferredLine = (source: string, issue: ReviewIssue): string => {
|
|
862
914
|
const where =
|
|
@@ -954,11 +1006,12 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
954
1006
|
// Parallel tasks are opt-in (ADR 0034 amendment): one coder per story unless asked.
|
|
955
1007
|
const taskConcurrency = Math.max(1, options.taskConcurrency ?? 1)
|
|
956
1008
|
const deferring = options.deferNonBlocking === true
|
|
1009
|
+
const reviewAtStory = options.reviewAt === "story"
|
|
1010
|
+
const reviewRounds = Math.max(1, options.reviewRounds ?? 3)
|
|
957
1011
|
const statePath = (story: Story): string => join(options.stateDir, `stories/${story.id}.json`)
|
|
958
1012
|
const planPath = (story: Story): string => join(options.stateDir, `stories/${story.id}.plan.md`)
|
|
959
1013
|
/** The judge's last verdict beside a fingerprint of what it judged. */
|
|
960
|
-
const judgePath = (story: Story): string =>
|
|
961
|
-
join(options.stateDir, `stories/${story.id}.judge.json`)
|
|
1014
|
+
const judgePath = (story: Story): string => judgeVerdictPath(options.stateDir, story.id)
|
|
962
1015
|
/** One file per review lens, beside a fingerprint of the task and diff. */
|
|
963
1016
|
const reviewCacheDir = (story: Story): string =>
|
|
964
1017
|
join(options.stateDir, `stories/${story.id}.review`)
|
|
@@ -1900,7 +1953,10 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1900
1953
|
// A lens's answer is kept beside the task and diff it answered, so a
|
|
1901
1954
|
// rerun over the same change asks nothing; every round is written to
|
|
1902
1955
|
// the story's findings log for people to read.
|
|
1903
|
-
|
|
1956
|
+
// Reviewing per story (ADR 0044), a task runs on its gates alone.
|
|
1957
|
+
...(reviewAtStory
|
|
1958
|
+
? { reviewers: [] }
|
|
1959
|
+
: { reviewCache: { files, dir: reviewCacheDir(story) } }),
|
|
1904
1960
|
onReview: (task, round, result, settled) =>
|
|
1905
1961
|
Effect.gen(function* () {
|
|
1906
1962
|
// A settled round's leftovers are deferred, not dropped: when the
|
|
@@ -1920,15 +1976,17 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1920
1976
|
onTaskReply: (task, _reply, trailer, startedAt) => checkEvidence(task, trailer, startedAt),
|
|
1921
1977
|
// The repository's own review rules ride along as one extra lens
|
|
1922
1978
|
// (ADR 0027 decision 9), read from the epic checkout.
|
|
1923
|
-
repoRules: loadRepoReviewRules(files, context.workDir),
|
|
1979
|
+
...(reviewAtStory ? {} : { repoRules: loadRepoReviewRules(files, context.workDir) }),
|
|
1924
1980
|
...(options.votes === undefined ? {} : { votes: options.votes }),
|
|
1925
1981
|
...(options.fixer === undefined ? {} : { fixer: options.fixer }),
|
|
1926
1982
|
...(options.stall === undefined ? {} : { stall: options.stall }),
|
|
1927
|
-
...(options.reviewers === undefined ? {} : { reviewers: options.reviewers }),
|
|
1983
|
+
...(options.reviewers === undefined || reviewAtStory ? {} : { reviewers: options.reviewers }),
|
|
1928
1984
|
...(options.maxRounds === undefined ? {} : { maxRounds: options.maxRounds }),
|
|
1929
1985
|
...(deferring ? { settle: "blocking" as const } : {})
|
|
1930
1986
|
})
|
|
1931
1987
|
if (mergeFailure !== undefined) {
|
|
1988
|
+
// The tasks run again: what the story review remembered no longer holds.
|
|
1989
|
+
yield* files.remove(reviewStatePath(options.stateDir, story.id))
|
|
1932
1990
|
// The branch already caught up with the epic above: the gates in the
|
|
1933
1991
|
// worktree now see the combination that was red after the merge.
|
|
1934
1992
|
yield* appendRevision(
|
|
@@ -1986,11 +2044,17 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1986
2044
|
verdict
|
|
1987
2045
|
)
|
|
1988
2046
|
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
2047
|
+
/**
|
|
2048
|
+
* One judge round over the story's branch: the verdict, published on the
|
|
2049
|
+
* lane, logged in the findings and kept beside a fingerprint of what was
|
|
2050
|
+
* judged. `empty` says the branch had no changes, so its owned code was
|
|
2051
|
+
* judged instead.
|
|
2052
|
+
*/
|
|
2053
|
+
const judgeOnce = (
|
|
2054
|
+
judge: NonNullable<StoriesOptions["judge"]>,
|
|
2055
|
+
round: number
|
|
2056
|
+
): Effect.Effect<{ readonly verdict: ReviewResult; readonly empty: boolean }, FlowError> =>
|
|
2057
|
+
Effect.gen(function* () {
|
|
1994
2058
|
const diff = yield* git.diffVsBase(epicBranch)
|
|
1995
2059
|
// No changes is not a failure by itself: the story's code may already
|
|
1996
2060
|
// be on the epic (an earlier story or run put it there). Judge that
|
|
@@ -2045,7 +2109,9 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
2045
2109
|
story.id,
|
|
2046
2110
|
story.title,
|
|
2047
2111
|
story.description,
|
|
2048
|
-
story.provides.join("\n")
|
|
2112
|
+
story.provides.join("\n"),
|
|
2113
|
+
// Absent for the default bar: verdicts stored before 2.52 stay valid.
|
|
2114
|
+
...(options.judgeBar === undefined ? [] : [options.judgeBar])
|
|
2049
2115
|
]),
|
|
2050
2116
|
withTimedRole(
|
|
2051
2117
|
"judge",
|
|
@@ -2102,6 +2168,183 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
2102
2168
|
dimensions: verdict instanceof StoryVerdict ? verdict.dimensions : []
|
|
2103
2169
|
})
|
|
2104
2170
|
)
|
|
2171
|
+
return { verdict, empty }
|
|
2172
|
+
})
|
|
2173
|
+
|
|
2174
|
+
/**
|
|
2175
|
+
* The story review (ADR 0044): every lens and the judge on the story's
|
|
2176
|
+
* diff, one fix turn per round, re-checks after the first. Returns the
|
|
2177
|
+
* judge's note; fails the story when the judge is still not cleared.
|
|
2178
|
+
*/
|
|
2179
|
+
const reviewStory = (): Effect.Effect<string | undefined, FlowError> =>
|
|
2180
|
+
Effect.gen(function* () {
|
|
2181
|
+
const statePath = reviewStatePath(options.stateDir, story.id)
|
|
2182
|
+
const repoRules = yield* loadRepoReviewRules(files, context.workDir)
|
|
2183
|
+
const lenses = [
|
|
2184
|
+
...(options.reviewers ?? minimalReviewers),
|
|
2185
|
+
...(repoRules === undefined ? [] : [repoRules])
|
|
2186
|
+
]
|
|
2187
|
+
const judge = options.judge
|
|
2188
|
+
const latest = yield* Ref.make<ReviewResult | undefined>(undefined)
|
|
2189
|
+
const lastStanding = yield* Ref.make<ReviewResult | undefined>(undefined)
|
|
2190
|
+
// The fix turn is a fresh chat: it gets the story and where each finding points.
|
|
2191
|
+
const storyFixPrompt = (prompt: string, standing: ReviewResult | undefined): string => {
|
|
2192
|
+
const located = (standing?.issues ?? []).flatMap((issue) =>
|
|
2193
|
+
issue.file === undefined
|
|
2194
|
+
? []
|
|
2195
|
+
: [
|
|
2196
|
+
`- ${issue.title} (${issue.lens ?? "gates"}): ${issue.file}${
|
|
2197
|
+
issue.line === undefined ? "" : `:${issue.line}`
|
|
2198
|
+
}`
|
|
2199
|
+
]
|
|
2200
|
+
)
|
|
2201
|
+
return [
|
|
2202
|
+
`Story ${story.id}: ${story.title}`,
|
|
2203
|
+
story.description,
|
|
2204
|
+
...(story.acceptance.length === 0
|
|
2205
|
+
? []
|
|
2206
|
+
: ["Done when:", ...story.acceptance.map((line) => `- ${line}`)]),
|
|
2207
|
+
"",
|
|
2208
|
+
prompt,
|
|
2209
|
+
...(located.length === 0 ? [] : ["", "Where the findings point:", ...located])
|
|
2210
|
+
].join("\n")
|
|
2211
|
+
}
|
|
2212
|
+
const judgeRuns = yield* Ref.make(0)
|
|
2213
|
+
const lastRound = yield* Ref.make(0)
|
|
2214
|
+
// A judge that scores `provides` at 0 is owed a piece no fix turn writes.
|
|
2215
|
+
const missingPiece = (verdict: ReviewResult | undefined): verdict is StoryVerdict =>
|
|
2216
|
+
verdict instanceof StoryVerdict &&
|
|
2217
|
+
verdict.dimensions.some((d) => d.id === "provides" && d.score <= 0)
|
|
2218
|
+
const result = yield* reviewAndFixLoop({
|
|
2219
|
+
reviewers: lenses,
|
|
2220
|
+
reviewerService: flowReviewer(storyContext),
|
|
2221
|
+
coder: yield* makeChat(storyContext.coder, { system, events, agent: "coder" }),
|
|
2222
|
+
taskTitle: `story ${story.id}: ${story.title}`,
|
|
2223
|
+
currentDiff: git.diffVsBase(epicBranch),
|
|
2224
|
+
events: storyContext.events,
|
|
2225
|
+
lint: gates,
|
|
2226
|
+
triage,
|
|
2227
|
+
maxRounds: reviewRounds,
|
|
2228
|
+
...(deferring ? { settle: "blocking" as const } : {}),
|
|
2229
|
+
...(options.fix === undefined ? {} : { fix: options.fix }),
|
|
2230
|
+
cache: { files, dir: reviewCacheDir(story) },
|
|
2231
|
+
fixWith: (prompt) =>
|
|
2232
|
+
Effect.gen(function* () {
|
|
2233
|
+
const verdict = yield* Ref.get(latest)
|
|
2234
|
+
if (missingPiece(verdict)) {
|
|
2235
|
+
// Used once: a later round's fix answers that round, not this verdict.
|
|
2236
|
+
yield* Ref.set(latest, undefined)
|
|
2237
|
+
yield* addRevision(verdict, false)
|
|
2238
|
+
yield* guarded(implementTasks, implementTasks)
|
|
2239
|
+
yield* catchUp(story, git)
|
|
2240
|
+
yield* repairPerimeter
|
|
2241
|
+
return ""
|
|
2242
|
+
}
|
|
2243
|
+
// A BLOCKED_ON from the fix turn stops the story like a task's would.
|
|
2244
|
+
yield* guarded(
|
|
2245
|
+
coderTurn(storyFixPrompt(prompt, yield* Ref.get(lastStanding))),
|
|
2246
|
+
Effect.void
|
|
2247
|
+
)
|
|
2248
|
+
yield* git.commitAll(`${story.id}: story review fixes`)
|
|
2249
|
+
return ""
|
|
2250
|
+
}),
|
|
2251
|
+
onRound: (round, standing, settled) =>
|
|
2252
|
+
Effect.andThen(
|
|
2253
|
+
Effect.andThen(Ref.set(lastRound, round), Ref.set(lastStanding, standing)),
|
|
2254
|
+
appendFindings(
|
|
2255
|
+
story,
|
|
2256
|
+
`story review round ${round} — ${
|
|
2257
|
+
standing.isClean ? "clean" : `${standing.issues.length} issue(s)`
|
|
2258
|
+
}${settled ? "" : ", fixing"}`,
|
|
2259
|
+
standing
|
|
2260
|
+
)
|
|
2261
|
+
),
|
|
2262
|
+
recheck: {
|
|
2263
|
+
extra:
|
|
2264
|
+
judge === undefined
|
|
2265
|
+
? []
|
|
2266
|
+
: [
|
|
2267
|
+
{
|
|
2268
|
+
name: "judge",
|
|
2269
|
+
review: () =>
|
|
2270
|
+
Effect.gen(function* () {
|
|
2271
|
+
const round = yield* Ref.updateAndGet(judgeRuns, (n) => n + 1)
|
|
2272
|
+
const { verdict } = yield* judgeOnce(judge, round)
|
|
2273
|
+
yield* Ref.set(latest, verdict)
|
|
2274
|
+
return verdict
|
|
2275
|
+
})
|
|
2276
|
+
}
|
|
2277
|
+
],
|
|
2278
|
+
memory: {
|
|
2279
|
+
load: Effect.map(files.read(statePath), (text) => {
|
|
2280
|
+
if (text === undefined) return undefined
|
|
2281
|
+
try {
|
|
2282
|
+
return Schema.decodeUnknownSync(ReviewMemory)(JSON.parse(text))
|
|
2283
|
+
} catch {
|
|
2284
|
+
// An unreadable state starts the review over.
|
|
2285
|
+
return undefined
|
|
2286
|
+
}
|
|
2287
|
+
}),
|
|
2288
|
+
save: (memory) =>
|
|
2289
|
+
files.writeAtomic(
|
|
2290
|
+
statePath,
|
|
2291
|
+
JSON.stringify(Schema.encodeSync(ReviewMemory)(memory), null, 2)
|
|
2292
|
+
)
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
})
|
|
2296
|
+
const rounds = yield* Ref.get(lastRound)
|
|
2297
|
+
const left = result.issues
|
|
2298
|
+
// Lenses and the judge always name themselves: a finding with no
|
|
2299
|
+
// lens is a gate's (target gates, oracle, perimeter, frozen tests).
|
|
2300
|
+
const gateLeft = left.filter((issue) => issue.lens === undefined)
|
|
2301
|
+
if (gateLeft.length > 0) {
|
|
2302
|
+
return yield* failed(
|
|
2303
|
+
story,
|
|
2304
|
+
`its gates are still red after ${rounds} story review round${rounds === 1 ? "" : "s"}:\n${issueLines(
|
|
2305
|
+
ReviewResult.make({ issues: gateLeft })
|
|
2306
|
+
)}`
|
|
2307
|
+
)
|
|
2308
|
+
}
|
|
2309
|
+
const judgeLeft = left.filter(
|
|
2310
|
+
(issue) => issue.lens === "judge" && (!deferring || isBlocking(issue))
|
|
2311
|
+
)
|
|
2312
|
+
if (judgeLeft.length > 0) {
|
|
2313
|
+
return yield* failed(
|
|
2314
|
+
story,
|
|
2315
|
+
`judge not cleared after ${rounds} story review round${rounds === 1 ? "" : "s"}:\n${issueLines(
|
|
2316
|
+
ReviewResult.make({ issues: judgeLeft })
|
|
2317
|
+
)}`
|
|
2318
|
+
)
|
|
2319
|
+
}
|
|
2320
|
+
const waiting = deferring ? left : []
|
|
2321
|
+
for (const issue of waiting) {
|
|
2322
|
+
deferred.push(deferredLine("story review", issue))
|
|
2323
|
+
}
|
|
2324
|
+
const carried = deferring ? [] : left.filter((issue) => issue.lens !== "judge")
|
|
2325
|
+
if (carried.length > 0) {
|
|
2326
|
+
yield* appendFindings(
|
|
2327
|
+
story,
|
|
2328
|
+
"carried: review rounds ran out",
|
|
2329
|
+
ReviewResult.make({ issues: carried })
|
|
2330
|
+
)
|
|
2331
|
+
}
|
|
2332
|
+
return judge === undefined
|
|
2333
|
+
? undefined
|
|
2334
|
+
: `judge cleared (story review round ${rounds}${
|
|
2335
|
+
waiting.length === 0 ? "" : `, ${waiting.length} finding(s) deferred`
|
|
2336
|
+
})`
|
|
2337
|
+
})
|
|
2338
|
+
|
|
2339
|
+
let judgeNote: string | undefined
|
|
2340
|
+
let inPlace = false
|
|
2341
|
+
const storyDiff = reviewAtStory ? yield* git.diffVsBase(epicBranch) : ""
|
|
2342
|
+
if (reviewAtStory && storyDiff.trim().length > 0) {
|
|
2343
|
+
judgeNote = yield* stage(laneOf(story), `story ${story.id}: review`, reviewStory())
|
|
2344
|
+
} else if (options.judge !== undefined) {
|
|
2345
|
+
const judge = options.judge
|
|
2346
|
+
for (let round = 1; round <= judgeRounds; round += 1) {
|
|
2347
|
+
const { verdict, empty } = yield* judgeOnce(judge, round)
|
|
2105
2348
|
// Deferring, a verdict with nothing blocking clears: what it found
|
|
2106
2349
|
// waits for the follow-up round instead of another revision.
|
|
2107
2350
|
if (verdict.isClean || (deferring && !verdict.issues.some(isBlocking))) {
|
|
@@ -2218,6 +2461,7 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
2218
2461
|
...(last === undefined ? {} : { executor: last })
|
|
2219
2462
|
})
|
|
2220
2463
|
)
|
|
2464
|
+
yield* files.remove(reviewStatePath(options.stateDir, story.id))
|
|
2221
2465
|
const inherited = inheritedByStory.get(story.id)
|
|
2222
2466
|
return StoryOutcome.make({
|
|
2223
2467
|
id: story.id,
|
|
@@ -2254,11 +2498,19 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
2254
2498
|
const attempt = (story: Story): Effect.Effect<Completion> =>
|
|
2255
2499
|
annotated(
|
|
2256
2500
|
story,
|
|
2257
|
-
stage(
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2501
|
+
stage(
|
|
2502
|
+
laneOf(story),
|
|
2503
|
+
`story ${story.id}`,
|
|
2504
|
+
// A story that ends failed starts its review over next time (ADR 0044).
|
|
2505
|
+
Effect.tapError(runStory(story), () =>
|
|
2506
|
+
Effect.ignore(files.remove(reviewStatePath(options.stateDir, story.id)))
|
|
2507
|
+
),
|
|
2508
|
+
{
|
|
2509
|
+
kind: "AGENT",
|
|
2510
|
+
root: true,
|
|
2511
|
+
attributes: storySpanAttributes(story)
|
|
2512
|
+
}
|
|
2513
|
+
)
|
|
2262
2514
|
).pipe(
|
|
2263
2515
|
Effect.map((outcome): Completion => ({ story, outcome })),
|
|
2264
2516
|
Effect.catch((error) =>
|