@llm4ts/flow 2.30.0 → 2.31.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/AutonomyContract.d.ts +12 -0
- package/dist/AutonomyContract.d.ts.map +1 -0
- package/dist/AutonomyContract.js +51 -0
- package/dist/AutonomyContract.js.map +1 -0
- package/dist/CarriedNotes.d.ts +10 -0
- package/dist/CarriedNotes.d.ts.map +1 -1
- package/dist/CarriedNotes.js +27 -2
- package/dist/CarriedNotes.js.map +1 -1
- package/dist/Evidence.d.ts +12 -0
- package/dist/Evidence.d.ts.map +1 -0
- package/dist/Evidence.js +30 -0
- package/dist/Evidence.js.map +1 -0
- package/dist/Flow.d.ts +35 -3
- package/dist/Flow.d.ts.map +1 -1
- package/dist/Flow.js +58 -4
- package/dist/Flow.js.map +1 -1
- package/dist/FlowEvents.d.ts +14 -1
- package/dist/FlowEvents.d.ts.map +1 -1
- package/dist/FlowEvents.js +13 -0
- package/dist/FlowEvents.js.map +1 -1
- package/dist/GateTriage.d.ts +81 -0
- package/dist/GateTriage.d.ts.map +1 -0
- package/dist/GateTriage.js +161 -0
- package/dist/GateTriage.js.map +1 -0
- package/dist/Gates.d.ts +35 -0
- package/dist/Gates.d.ts.map +1 -0
- package/dist/Gates.js +63 -0
- package/dist/Gates.js.map +1 -0
- package/dist/OracleGuard.d.ts +53 -0
- package/dist/OracleGuard.d.ts.map +1 -0
- package/dist/OracleGuard.js +165 -0
- package/dist/OracleGuard.js.map +1 -0
- package/dist/Pack.d.ts +7 -0
- package/dist/Pack.d.ts.map +1 -1
- package/dist/Pack.js +9 -0
- package/dist/Pack.js.map +1 -1
- package/dist/Retro.d.ts.map +1 -1
- package/dist/Retro.js +5 -0
- package/dist/Retro.js.map +1 -1
- package/dist/Review.d.ts +68 -2
- package/dist/Review.d.ts.map +1 -1
- package/dist/Review.js +158 -27
- package/dist/Review.js.map +1 -1
- package/dist/Roster.d.ts +2 -0
- package/dist/Roster.d.ts.map +1 -1
- package/dist/Roster.js +3 -1
- package/dist/Roster.js.map +1 -1
- package/dist/RosterSeats.d.ts +2 -0
- package/dist/RosterSeats.d.ts.map +1 -1
- package/dist/RosterSeats.js.map +1 -1
- package/dist/Stories.d.ts +36 -2
- package/dist/Stories.d.ts.map +1 -1
- package/dist/Stories.js +159 -12
- package/dist/Stories.js.map +1 -1
- package/dist/StoryPlan.d.ts +6 -0
- package/dist/StoryPlan.d.ts.map +1 -1
- package/dist/StoryPlan.js +9 -2
- package/dist/StoryPlan.js.map +1 -1
- package/package.json +7 -2
- package/src/AutonomyContract.ts +61 -0
- package/src/CarriedNotes.ts +37 -2
- package/src/Evidence.ts +43 -0
- package/src/Flow.ts +109 -3
- package/src/FlowEvents.ts +13 -0
- package/src/GateTriage.ts +250 -0
- package/src/Gates.ts +104 -0
- package/src/OracleGuard.ts +205 -0
- package/src/Pack.ts +16 -0
- package/src/Retro.ts +7 -0
- package/src/Review.ts +261 -34
- package/src/Roster.ts +3 -1
- package/src/RosterSeats.ts +2 -0
- package/src/Stories.ts +289 -16
- package/src/StoryPlan.ts +11 -1
package/src/Stories.ts
CHANGED
|
@@ -20,6 +20,16 @@ import type { LlmError } from "@llm4ts/core/Errors"
|
|
|
20
20
|
import { BoardItem, type BoardSyncShape } from "./BoardSync.ts"
|
|
21
21
|
import { makeChat } from "./Chat.ts"
|
|
22
22
|
import { implementPlanFlow } from "./Flow.ts"
|
|
23
|
+
import {
|
|
24
|
+
GateBaseline,
|
|
25
|
+
baselineKey,
|
|
26
|
+
ensureBaseline as ensureStoredBaseline,
|
|
27
|
+
failingLinesOf,
|
|
28
|
+
storyGateLogDir,
|
|
29
|
+
triageGates,
|
|
30
|
+
writeBaseline,
|
|
31
|
+
type GateLogDir
|
|
32
|
+
} from "./Gates.ts"
|
|
23
33
|
import type { ContextOptions, FlowContextShape } from "./FlowContext.ts"
|
|
24
34
|
import {
|
|
25
35
|
describeFlowError,
|
|
@@ -31,6 +41,7 @@ import {
|
|
|
31
41
|
type FlowError
|
|
32
42
|
} from "./FlowError.ts"
|
|
33
43
|
import {
|
|
44
|
+
EvidenceChecked,
|
|
34
45
|
Info,
|
|
35
46
|
JudgedDimension,
|
|
36
47
|
StoryJudged,
|
|
@@ -57,8 +68,23 @@ import {
|
|
|
57
68
|
import { Plan, Task } from "./Plan.ts"
|
|
58
69
|
import { stage } from "./PlanExecution.ts"
|
|
59
70
|
import { planFrom } from "./Planner.ts"
|
|
60
|
-
import {
|
|
71
|
+
import {
|
|
72
|
+
ReviewIssue,
|
|
73
|
+
ReviewResult,
|
|
74
|
+
applyTriage,
|
|
75
|
+
type FixPromptOptions,
|
|
76
|
+
type GateTriageOptions
|
|
77
|
+
} from "./Review.ts"
|
|
78
|
+
import {
|
|
79
|
+
type OracleRules,
|
|
80
|
+
checkOracle,
|
|
81
|
+
defaultOracleRules,
|
|
82
|
+
parseUnifiedDiff
|
|
83
|
+
} from "./OracleGuard.ts"
|
|
61
84
|
import { cachedValue, fingerprintOf } from "./ReviewCache.ts"
|
|
85
|
+
import { withContract, type ContractProfile } from "./AutonomyContract.ts"
|
|
86
|
+
import { fabricatedStatusIssues, unverifiedClaims } from "./Evidence.ts"
|
|
87
|
+
import type { Trailer } from "./CarriedNotes.ts"
|
|
62
88
|
import type { Reviewer } from "./Reviewer.ts"
|
|
63
89
|
import {
|
|
64
90
|
dependentsOf,
|
|
@@ -133,7 +159,9 @@ export class StoryOutcome extends Schema.Class<StoryOutcome>("StoryOutcome")({
|
|
|
133
159
|
executor: Schema.optionalKey(Schema.String),
|
|
134
160
|
/** ESTIMATES, never measurements (ADR 0012). */
|
|
135
161
|
estimatedTokens: Schema.optionalKey(Schema.Int),
|
|
136
|
-
estimatedCostUsd: Schema.optionalKey(Schema.Number)
|
|
162
|
+
estimatedCostUsd: Schema.optionalKey(Schema.Number),
|
|
163
|
+
/** Gate failures already red on the story's base, not charged to it (ADR 0027). */
|
|
164
|
+
inherited: Schema.optionalKey(Schema.Array(Schema.String))
|
|
137
165
|
}) {}
|
|
138
166
|
|
|
139
167
|
export class EpicReport extends Schema.Class<EpicReport>("EpicReport")({
|
|
@@ -184,6 +212,24 @@ export const renderEpicReport = (report: EpicReport): string => {
|
|
|
184
212
|
if (cost.length > 0) {
|
|
185
213
|
lines.push(`- Estimated cost: ${money(cost.reduce((sum, value) => sum + value, 0))}`)
|
|
186
214
|
}
|
|
215
|
+
const inherited = new Map<string, Array<string>>()
|
|
216
|
+
for (const story of report.stories) {
|
|
217
|
+
for (const line of story.inherited ?? []) {
|
|
218
|
+
inherited.set(line, [...(inherited.get(line) ?? []), story.id])
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (inherited.size > 0) {
|
|
222
|
+
lines.push(
|
|
223
|
+
"",
|
|
224
|
+
"## Inherited gate failures",
|
|
225
|
+
"",
|
|
226
|
+
"Red on the base before the story ran; not charged to it. A cleanup story may own them.",
|
|
227
|
+
""
|
|
228
|
+
)
|
|
229
|
+
for (const [line, stories] of inherited) {
|
|
230
|
+
lines.push(`- ${line} (stories: ${stories.join(", ")})`)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
187
233
|
const coded = report.stories.filter((story) => story.executor !== undefined)
|
|
188
234
|
if (coded.length > 0) {
|
|
189
235
|
// A story is counted for the executor that finished it; a handover chain
|
|
@@ -591,9 +637,43 @@ export interface StoriesOptions {
|
|
|
591
637
|
* gate's timing names its story; a gate may ignore it.
|
|
592
638
|
*/
|
|
593
639
|
readonly gates: (
|
|
640
|
+
workDir: string,
|
|
641
|
+
events?: FlowEventsShape,
|
|
642
|
+
/** Where to write one log per gate command for this run; absent writes none. */
|
|
643
|
+
log?: GateLogDir
|
|
644
|
+
) => Effect.Effect<ReviewResult, FlowError>
|
|
645
|
+
/**
|
|
646
|
+
* The gate commands as configured and the application directory (ADR 0027):
|
|
647
|
+
* with them, the gates' result on each epic head is recorded as a baseline
|
|
648
|
+
* and a story is charged only with the failing lines it added. Without
|
|
649
|
+
* them every red line blocks, as before.
|
|
650
|
+
*/
|
|
651
|
+
readonly gateCommands?: ReadonlyArray<ReadonlyArray<string>>
|
|
652
|
+
readonly appDir?: string
|
|
653
|
+
/** The test gate alone, for one rerun that tells a flaky line from a new one; omit to never rerun. */
|
|
654
|
+
readonly testGate?: (
|
|
594
655
|
workDir: string,
|
|
595
656
|
events?: FlowEventsShape
|
|
596
657
|
) => Effect.Effect<ReviewResult, FlowError>
|
|
658
|
+
/** How gate output reaches the coder in the fix prompt. */
|
|
659
|
+
readonly fix?: FixPromptOptions
|
|
660
|
+
/** Test-file and marker patterns for the oracle guard; default rules when absent (ADR 0027). */
|
|
661
|
+
readonly oracleRules?: OracleRules
|
|
662
|
+
/**
|
|
663
|
+
* The autonomy contract profile for the coder's system prompt (ADR 0027
|
|
664
|
+
* decision 5). Default: the roster executor's `contract`, else `full`.
|
|
665
|
+
*/
|
|
666
|
+
readonly contract?: ContractProfile
|
|
667
|
+
/**
|
|
668
|
+
* The arguments of every tool call the story's coder made since `since`
|
|
669
|
+
* (epoch ms), from the run's transcript; `undefined` when the run keeps
|
|
670
|
+
* none for it. With it, a task's `verified:` claims are checked
|
|
671
|
+
* (ADR 0027 decision 6).
|
|
672
|
+
*/
|
|
673
|
+
readonly toolCalls?: (
|
|
674
|
+
story: Story,
|
|
675
|
+
since: number
|
|
676
|
+
) => Effect.Effect<ReadonlyArray<string> | undefined, FlowError>
|
|
597
677
|
/** Story-level judge over the branch's diff against the epic branch; omit to skip. */
|
|
598
678
|
readonly judge?: (
|
|
599
679
|
story: Story,
|
|
@@ -717,6 +797,49 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
717
797
|
join(options.stateDir, `stories/${story.id}.findings.md`)
|
|
718
798
|
/** What the coder's tasks learned, carried into the next task of the same story. */
|
|
719
799
|
const notesPath = (story: Story): string => join(options.stateDir, `stories/${story.id}.notes.md`)
|
|
800
|
+
// Gates with a memory (ADR 0027): baselines are keyed by the commit a
|
|
801
|
+
// change started from, the app dir and the commands; without commands
|
|
802
|
+
// nothing is recorded and every red line blocks, as before.
|
|
803
|
+
const appDir = options.appDir ?? "."
|
|
804
|
+
const gateCommands = options.gateCommands
|
|
805
|
+
const rootsOf = (workDir: string): ReadonlyArray<string> => [join(workDir, appDir), workDir]
|
|
806
|
+
const keyFor = (commit: string): string =>
|
|
807
|
+
baselineKey({ baseCommit: commit, appDir, commands: gateCommands ?? [] })
|
|
808
|
+
const baselineOf = (
|
|
809
|
+
commit: string,
|
|
810
|
+
result: ReviewResult,
|
|
811
|
+
workDir: string
|
|
812
|
+
): Effect.Effect<GateBaseline> =>
|
|
813
|
+
Effect.map(Clock.currentTimeMillis, (recordedAt) =>
|
|
814
|
+
GateBaseline.make({
|
|
815
|
+
baseCommit: commit,
|
|
816
|
+
appDir,
|
|
817
|
+
commands: (gateCommands ?? []).map((command) => command.join(" ")),
|
|
818
|
+
failingLines: failingLinesOf(result, rootsOf(workDir)),
|
|
819
|
+
...(result.passed === undefined ? {} : { passedCount: result.passed }),
|
|
820
|
+
recordedAt
|
|
821
|
+
})
|
|
822
|
+
)
|
|
823
|
+
/** The baseline for `commit`, recording one from `run` when none is stored. */
|
|
824
|
+
const ensureBaseline = (
|
|
825
|
+
commit: string,
|
|
826
|
+
run: Effect.Effect<ReviewResult, FlowError>,
|
|
827
|
+
workDir: string
|
|
828
|
+
): Effect.Effect<GateBaseline | undefined, FlowError> =>
|
|
829
|
+
gateCommands === undefined
|
|
830
|
+
? Effect.succeed(undefined)
|
|
831
|
+
: ensureStoredBaseline({
|
|
832
|
+
files,
|
|
833
|
+
stateDir: options.stateDir,
|
|
834
|
+
commit,
|
|
835
|
+
appDir,
|
|
836
|
+
commands: gateCommands,
|
|
837
|
+
run,
|
|
838
|
+
roots: rootsOf(workDir),
|
|
839
|
+
now: Clock.currentTimeMillis
|
|
840
|
+
})
|
|
841
|
+
/** What each story inherited, for its outcome. */
|
|
842
|
+
const inheritedByStory = new Map<string, ReadonlyArray<string>>()
|
|
720
843
|
const findingLines = (result: ReviewResult): ReadonlyArray<string> =>
|
|
721
844
|
result.issues.map((issue) => {
|
|
722
845
|
const where =
|
|
@@ -774,6 +897,11 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
774
897
|
|
|
775
898
|
yield* epicCheckoutClean()
|
|
776
899
|
yield* stage(events, "epic branch", context.git.checkoutOrCreate(epicBranch))
|
|
900
|
+
if (gateCommands !== undefined) {
|
|
901
|
+
// The gates as the epic head stands: what every story may inherit.
|
|
902
|
+
const epicHead = yield* context.git.checkpoint
|
|
903
|
+
yield* ensureBaseline(epicHead, options.gates(context.workDir, events), context.workDir)
|
|
904
|
+
}
|
|
777
905
|
|
|
778
906
|
const waves = topologicalWaves(plan)
|
|
779
907
|
const waveOf = (id: string): string | undefined => {
|
|
@@ -814,12 +942,29 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
814
942
|
Timed.make({ kind: "merge", label: "merge", ms, ...(failed ? { failed } : {}) })
|
|
815
943
|
)
|
|
816
944
|
const gate = yield* options.gates(context.workDir, lane)
|
|
817
|
-
|
|
945
|
+
// Charged only with what the merge added: the head before it had
|
|
946
|
+
// a baseline (run start or the previous merge).
|
|
947
|
+
const before = yield* ensureBaseline(
|
|
948
|
+
checkpoint,
|
|
949
|
+
Effect.succeed(ReviewResult.make({ issues: [] })),
|
|
950
|
+
context.workDir
|
|
951
|
+
)
|
|
952
|
+
const verdict = triageGates(gate, before, rootsOf(context.workDir))
|
|
953
|
+
if (!verdict.blocking.isClean) {
|
|
818
954
|
// Never leave a red epic head for the next story to inherit.
|
|
819
955
|
yield* context.git.rollback(checkpoint)
|
|
820
956
|
return yield* failed(
|
|
821
957
|
story,
|
|
822
|
-
`epic gates failed after merging; merge undone:\n${issueLines(
|
|
958
|
+
`epic gates failed after merging; merge undone:\n${issueLines(verdict.blocking)}`
|
|
959
|
+
)
|
|
960
|
+
}
|
|
961
|
+
if (gateCommands !== undefined) {
|
|
962
|
+
const mergedHead = yield* context.git.checkpoint
|
|
963
|
+
yield* writeBaseline(
|
|
964
|
+
files,
|
|
965
|
+
options.stateDir,
|
|
966
|
+
keyFor(mergedHead),
|
|
967
|
+
yield* baselineOf(mergedHead, gate, context.workDir)
|
|
823
968
|
)
|
|
824
969
|
}
|
|
825
970
|
yield* laneOf(story).publish(
|
|
@@ -972,13 +1117,69 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
972
1117
|
const git = storyContext.git
|
|
973
1118
|
const watchedSeats: StorySeats = { ...seats, context: storyContext }
|
|
974
1119
|
const extra = options.system === undefined ? undefined : yield* options.system(story)
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1120
|
+
const contractProfile: ContractProfile =
|
|
1121
|
+
options.contract ??
|
|
1122
|
+
(coderExecutor === undefined ? undefined : roster?.contractOf?.(coderExecutor)) ??
|
|
1123
|
+
"full"
|
|
1124
|
+
const system = withContract(
|
|
1125
|
+
[
|
|
1126
|
+
perimeterRules(story, { plan, worktree: state.worktree, epicCheckout: context.workDir }),
|
|
1127
|
+
gateRules,
|
|
1128
|
+
extra
|
|
1129
|
+
]
|
|
1130
|
+
.filter((part): part is string => part !== undefined && part.trim().length > 0)
|
|
1131
|
+
.join("\n\n"),
|
|
1132
|
+
contractProfile
|
|
1133
|
+
)
|
|
1134
|
+
/** What the coder claimed to have run and did not, task by task, for the judge. */
|
|
1135
|
+
const evidenceNotes: Array<string> = []
|
|
1136
|
+
const checkEvidence = (
|
|
1137
|
+
task: Task,
|
|
1138
|
+
trailer: Trailer,
|
|
1139
|
+
startedAt: number
|
|
1140
|
+
): Effect.Effect<void, FlowError> =>
|
|
1141
|
+
Effect.gen(function* () {
|
|
1142
|
+
const calls =
|
|
1143
|
+
options.toolCalls === undefined ? undefined : yield* options.toolCalls(story, startedAt)
|
|
1144
|
+
const confidence =
|
|
1145
|
+
trailer.confidence === undefined ? {} : { confidence: trailer.confidence }
|
|
1146
|
+
if (calls === undefined) {
|
|
1147
|
+
yield* laneOf(story).publish(
|
|
1148
|
+
EvidenceChecked.make({
|
|
1149
|
+
task: task.title,
|
|
1150
|
+
claimed: trailer.verified.length,
|
|
1151
|
+
unverified: 0,
|
|
1152
|
+
unchecked: true,
|
|
1153
|
+
lane: story.id,
|
|
1154
|
+
...confidence
|
|
1155
|
+
})
|
|
1156
|
+
)
|
|
1157
|
+
return
|
|
1158
|
+
}
|
|
1159
|
+
const unverified = unverifiedClaims(trailer.verified, calls)
|
|
1160
|
+
yield* laneOf(story).publish(
|
|
1161
|
+
EvidenceChecked.make({
|
|
1162
|
+
task: task.title,
|
|
1163
|
+
claimed: trailer.verified.length,
|
|
1164
|
+
unverified: unverified.length,
|
|
1165
|
+
lane: story.id,
|
|
1166
|
+
...confidence
|
|
1167
|
+
})
|
|
1168
|
+
)
|
|
1169
|
+
if (unverified.length > 0) {
|
|
1170
|
+
evidenceNotes.push(
|
|
1171
|
+
`task "${task.title}": claimed ${unverified.map((command) => `\`${command}\``).join(", ")} — no tool call ran it`
|
|
1172
|
+
)
|
|
1173
|
+
yield* appendFindings(
|
|
1174
|
+
story,
|
|
1175
|
+
`evidence "${task.title}" — ${unverified.length} claimed command(s) never ran`,
|
|
1176
|
+
ReviewResult.make({ issues: fabricatedStatusIssues(unverified), summary: "" })
|
|
1177
|
+
)
|
|
1178
|
+
}
|
|
1179
|
+
if (trailer.confidence === "low") {
|
|
1180
|
+
evidenceNotes.push(`task "${task.title}": the coder said confidence: low`)
|
|
1181
|
+
}
|
|
1182
|
+
})
|
|
982
1183
|
const prompt = storyPrompt(story)
|
|
983
1184
|
|
|
984
1185
|
// Catch up before setup: the epic may have changed the manifest too.
|
|
@@ -1024,11 +1225,65 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1024
1225
|
|
|
1025
1226
|
// The target's gates plus the perimeter: a stray path is a gate failure
|
|
1026
1227
|
// the task review loop hands back to the coder before anything commits.
|
|
1228
|
+
// The epic head this worktree started from: what the story may inherit.
|
|
1229
|
+
const storyBase = yield* git.checkpoint
|
|
1230
|
+
const storyBaseline = ensureBaseline(
|
|
1231
|
+
storyBase,
|
|
1232
|
+
options.gates(state.worktree, laneOf(story)),
|
|
1233
|
+
state.worktree
|
|
1234
|
+
)
|
|
1235
|
+
// The oracle guard over the story's whole change against the epic:
|
|
1236
|
+
// committed on the branch and uncommitted in the worktree (ADR 0027).
|
|
1237
|
+
const oracleGate = (target: ReviewResult): Effect.Effect<ReviewResult, FlowError> =>
|
|
1238
|
+
story.testsChange
|
|
1239
|
+
? Effect.succeed(ReviewResult.make({ issues: [] }))
|
|
1240
|
+
: Effect.gen(function* () {
|
|
1241
|
+
const committed = yield* git.diffVsBase(epicBranch)
|
|
1242
|
+
const uncommitted = yield* git.diffAll
|
|
1243
|
+
const base =
|
|
1244
|
+
gateCommands === undefined ? undefined : (yield* storyBaseline)?.passedCount
|
|
1245
|
+
const issues = checkOracle(
|
|
1246
|
+
parseUnifiedDiff(`${committed}\n${uncommitted}`),
|
|
1247
|
+
{ base, current: target.passed },
|
|
1248
|
+
options.oracleRules ?? defaultOracleRules,
|
|
1249
|
+
false
|
|
1250
|
+
)
|
|
1251
|
+
return ReviewResult.make({ issues, summary: issues.length === 0 ? "" : "oracle guard" })
|
|
1252
|
+
})
|
|
1027
1253
|
const gates: Effect.Effect<ReviewResult, FlowError> = Effect.gen(function* () {
|
|
1028
|
-
const target = yield* options.gates(state.worktree, laneOf(story)
|
|
1254
|
+
const target = yield* options.gates(state.worktree, laneOf(story), {
|
|
1255
|
+
files,
|
|
1256
|
+
dir: storyGateLogDir(options.stateDir, story.id)
|
|
1257
|
+
})
|
|
1029
1258
|
const changed = yield* perimeterNow(story, git)
|
|
1030
|
-
return combined(
|
|
1259
|
+
return combined(
|
|
1260
|
+
combined(target, perimeterGate([...changed.sharedReadOnly, ...changed.outside], story)),
|
|
1261
|
+
yield* oracleGate(target)
|
|
1262
|
+
)
|
|
1031
1263
|
})
|
|
1264
|
+
const triage: GateTriageOptions = {
|
|
1265
|
+
baseline: storyBaseline,
|
|
1266
|
+
roots: rootsOf(state.worktree),
|
|
1267
|
+
...(options.testGate === undefined
|
|
1268
|
+
? {}
|
|
1269
|
+
: { rerunTest: options.testGate(state.worktree, laneOf(story)) })
|
|
1270
|
+
}
|
|
1271
|
+
if (gateCommands !== undefined) {
|
|
1272
|
+
const inherited = (yield* storyBaseline)?.failingLines ?? []
|
|
1273
|
+
if (inherited.length > 0) {
|
|
1274
|
+
inheritedByStory.set(story.id, inherited)
|
|
1275
|
+
yield* appendFindings(
|
|
1276
|
+
story,
|
|
1277
|
+
"gate failures inherited from the base (not charged to this story)",
|
|
1278
|
+
ReviewResult.make({
|
|
1279
|
+
issues: inherited.map((line) =>
|
|
1280
|
+
ReviewIssue.make({ severity: "Info", title: line, description: "", origin: "base" })
|
|
1281
|
+
),
|
|
1282
|
+
summary: ""
|
|
1283
|
+
})
|
|
1284
|
+
)
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1032
1287
|
|
|
1033
1288
|
const coderTurn = (text: string): Effect.Effect<string, FlowError> =>
|
|
1034
1289
|
Effect.flatMap(makeChat(storyContext.coder, { system, events, agent: "coder" }), (chat) =>
|
|
@@ -1037,7 +1292,8 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1037
1292
|
|
|
1038
1293
|
const commitGreen = (message: string, why: string): Effect.Effect<void, FlowError> =>
|
|
1039
1294
|
Effect.gen(function* () {
|
|
1040
|
-
const
|
|
1295
|
+
const reported = yield* Ref.make<ReadonlySet<string>>(new Set())
|
|
1296
|
+
const regated = yield* applyTriage(yield* gates, triage, laneOf(story), reported)
|
|
1041
1297
|
if (!regated.isClean) {
|
|
1042
1298
|
return yield* failed(story, `gates broke ${why}:\n${issueLines(regated)}`)
|
|
1043
1299
|
}
|
|
@@ -1171,6 +1427,8 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1171
1427
|
},
|
|
1172
1428
|
checkoutBranch: false,
|
|
1173
1429
|
lint: gates,
|
|
1430
|
+
triage,
|
|
1431
|
+
...(options.fix === undefined ? {} : { fix: options.fix }),
|
|
1174
1432
|
// A story's final state is judged and gated downstream (judge round,
|
|
1175
1433
|
// perimeter check, epic gates), so a task the coder finds already
|
|
1176
1434
|
// satisfied — without saying the exact sentinel — must not sink the
|
|
@@ -1188,6 +1446,7 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1188
1446
|
}${settled ? "" : ", fixing"}`,
|
|
1189
1447
|
result
|
|
1190
1448
|
),
|
|
1449
|
+
onTaskReply: (task, _reply, trailer, startedAt) => checkEvidence(task, trailer, startedAt),
|
|
1191
1450
|
...(options.reviewers === undefined ? {} : { reviewers: options.reviewers }),
|
|
1192
1451
|
...(options.maxRounds === undefined ? {} : { maxRounds: options.maxRounds })
|
|
1193
1452
|
})
|
|
@@ -1239,7 +1498,19 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1239
1498
|
// be on the epic (an earlier story or run put it there). Judge that
|
|
1240
1499
|
// code instead: the owned paths against the empty tree.
|
|
1241
1500
|
const empty = diff.trim().length === 0
|
|
1242
|
-
const
|
|
1501
|
+
const judgedCode = empty ? yield* git.diffVsBaseScoped(emptyTree, story.owned, false) : diff
|
|
1502
|
+
// What the coder claimed and could not show travels with the subject
|
|
1503
|
+
// (ADR 0027 decision 6): the judge weighs the code knowing the
|
|
1504
|
+
// story's own account of its testing is not evidence.
|
|
1505
|
+
const subject =
|
|
1506
|
+
evidenceNotes.length === 0
|
|
1507
|
+
? judgedCode
|
|
1508
|
+
: [
|
|
1509
|
+
"Evidence notes from the run (not part of the diff):",
|
|
1510
|
+
...evidenceNotes.map((note) => `- ${note}`),
|
|
1511
|
+
"",
|
|
1512
|
+
judgedCode
|
|
1513
|
+
].join("\n")
|
|
1243
1514
|
// The verdict is kept beside a fingerprint of what was judged: a
|
|
1244
1515
|
// rerun that finds the same diff gets the same answer without a
|
|
1245
1516
|
// model call, and the last verdict is on disk to read. Only the
|
|
@@ -1406,6 +1677,7 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1406
1677
|
...(last === undefined ? {} : { executor: last })
|
|
1407
1678
|
})
|
|
1408
1679
|
)
|
|
1680
|
+
const inherited = inheritedByStory.get(story.id)
|
|
1409
1681
|
return StoryOutcome.make({
|
|
1410
1682
|
id: story.id,
|
|
1411
1683
|
title: story.title,
|
|
@@ -1414,7 +1686,8 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1414
1686
|
...(result.judge === undefined ? {} : { judge: result.judge }),
|
|
1415
1687
|
...(result.executor === undefined ? {} : { executor: result.executor }),
|
|
1416
1688
|
...(result.totals === undefined ? {} : { estimatedTokens: result.totals.total }),
|
|
1417
|
-
...(result.totals?.costUsd === undefined ? {} : { estimatedCostUsd: result.totals.costUsd })
|
|
1689
|
+
...(result.totals?.costUsd === undefined ? {} : { estimatedCostUsd: result.totals.costUsd }),
|
|
1690
|
+
...(inherited === undefined ? {} : { inherited })
|
|
1418
1691
|
})
|
|
1419
1692
|
})
|
|
1420
1693
|
|
package/src/StoryPlan.ts
CHANGED
|
@@ -44,6 +44,15 @@ export class Story extends Schema.Class<Story>("Story")({
|
|
|
44
44
|
acceptance: Schema.Array(Schema.String).pipe(
|
|
45
45
|
Schema.withConstructorDefault(Effect.succeed([])),
|
|
46
46
|
Schema.withDecodingDefaultKey(Effect.succeed([]))
|
|
47
|
+
),
|
|
48
|
+
/**
|
|
49
|
+
* The story may delete, skip or rewrite tests (ADR 0027): the oracle guard
|
|
50
|
+
* stays silent for it. Absent or false, a deleted test file, an added skip
|
|
51
|
+
* marker or a passed-count drop is a gate failure.
|
|
52
|
+
*/
|
|
53
|
+
testsChange: Schema.Boolean.pipe(
|
|
54
|
+
Schema.withConstructorDefault(Effect.succeed(false)),
|
|
55
|
+
Schema.withDecodingDefaultKey(Effect.succeed(false))
|
|
47
56
|
)
|
|
48
57
|
}) {}
|
|
49
58
|
|
|
@@ -366,7 +375,8 @@ export const storyHash = (story: Story): string =>
|
|
|
366
375
|
...(story.readFirst.length === 0
|
|
367
376
|
? {}
|
|
368
377
|
: { readFirst: [...story.readFirst].map(normalizePath) }),
|
|
369
|
-
...(story.acceptance.length === 0 ? {} : { acceptance: [...story.acceptance] })
|
|
378
|
+
...(story.acceptance.length === 0 ? {} : { acceptance: [...story.acceptance] }),
|
|
379
|
+
...(story.testsChange ? { testsChange: true } : {})
|
|
370
380
|
})
|
|
371
381
|
)
|
|
372
382
|
|