@llm4ts/flow 2.38.0 → 2.40.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/Flow.d.ts +3 -1
- package/dist/Flow.d.ts.map +1 -1
- package/dist/Flow.js +1 -0
- package/dist/Flow.js.map +1 -1
- package/dist/GateTriage.d.ts +1 -1
- package/dist/GateTriage.d.ts.map +1 -1
- package/dist/GateTriage.js +4 -2
- package/dist/GateTriage.js.map +1 -1
- package/dist/Gates.d.ts +2 -0
- package/dist/Gates.d.ts.map +1 -1
- package/dist/Gates.js +11 -7
- package/dist/Gates.js.map +1 -1
- package/dist/Retro.d.ts.map +1 -1
- package/dist/Retro.js +4 -1
- package/dist/Retro.js.map +1 -1
- package/dist/Review.d.ts +12 -0
- package/dist/Review.d.ts.map +1 -1
- package/dist/Review.js +14 -4
- package/dist/Review.js.map +1 -1
- package/dist/Stories.d.ts +23 -0
- package/dist/Stories.d.ts.map +1 -1
- package/dist/Stories.js +131 -46
- package/dist/Stories.js.map +1 -1
- package/package.json +2 -2
- package/src/Flow.ts +5 -1
- package/src/GateTriage.ts +6 -2
- package/src/Gates.ts +13 -7
- package/src/Retro.ts +6 -1
- package/src/Review.ts +28 -4
- package/src/Stories.ts +193 -57
package/src/Stories.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
ensureBaseline as ensureStoredBaseline,
|
|
28
28
|
failingLinesOf,
|
|
29
29
|
storyGateLogDir,
|
|
30
|
-
|
|
30
|
+
mergeGateLogDir,
|
|
31
31
|
writeBaseline,
|
|
32
32
|
type GateLogDir
|
|
33
33
|
} from "./Gates.ts"
|
|
@@ -74,7 +74,9 @@ import {
|
|
|
74
74
|
ReviewIssue,
|
|
75
75
|
ReviewResult,
|
|
76
76
|
applyTriage,
|
|
77
|
+
isBlocking,
|
|
77
78
|
loadRepoReviewRules,
|
|
79
|
+
nonBlockingIssues,
|
|
78
80
|
type FixPromptOptions,
|
|
79
81
|
type GateTriageOptions
|
|
80
82
|
} from "./Review.ts"
|
|
@@ -701,6 +703,13 @@ export interface StoriesOptions {
|
|
|
701
703
|
* story's plan (coder, review, gates, commit). Default 2: one revision.
|
|
702
704
|
*/
|
|
703
705
|
readonly judgeRounds?: number
|
|
706
|
+
/**
|
|
707
|
+
* When the epic gates are red after a story merges (the merge is undone),
|
|
708
|
+
* the story goes back to its worktree this many times: it catches up with
|
|
709
|
+
* the epic, gets a revision task with the failures, and merges again.
|
|
710
|
+
* Default 1; 0 fails the story at once, as before.
|
|
711
|
+
*/
|
|
712
|
+
readonly mergeRevisions?: number
|
|
704
713
|
/**
|
|
705
714
|
* How much of the code a story starts from goes into its coder's system
|
|
706
715
|
* prompt, in characters: the shared read-only files it uses, then its own.
|
|
@@ -722,6 +731,14 @@ export interface StoriesOptions {
|
|
|
722
731
|
readonly failFast?: boolean
|
|
723
732
|
readonly reviewers?: ReadonlyArray<Reviewer>
|
|
724
733
|
readonly maxRounds?: number
|
|
734
|
+
/**
|
|
735
|
+
* Defer what does not block (ADR 0031): a task's review settles once its
|
|
736
|
+
* findings are all non-blocking, the judge clears a story whose findings
|
|
737
|
+
* are all non-blocking, and what is left is written to
|
|
738
|
+
* `stories/<id>.deferred.md` (`deferredPath`) when the story finishes,
|
|
739
|
+
* for a follow-up round. Default false: every finding is fixed or fails.
|
|
740
|
+
*/
|
|
741
|
+
readonly deferNonBlocking?: boolean
|
|
725
742
|
/**
|
|
726
743
|
* Second opinion on a BLOCKED_ON claim the plan cannot settle (a path no
|
|
727
744
|
* story owns, or a parallel story's). `real: false` sends the coder back
|
|
@@ -762,6 +779,24 @@ const combined = (first: ReviewResult, second: ReviewResult): ReviewResult =>
|
|
|
762
779
|
summary: [first.summary, second.summary].filter((part) => part.length > 0).join("; ")
|
|
763
780
|
})
|
|
764
781
|
|
|
782
|
+
/**
|
|
783
|
+
* Where a finished story's deferred findings are kept (ADR 0031): one
|
|
784
|
+
* Markdown list per story under the executor's state directory, absent when
|
|
785
|
+
* nothing was deferred.
|
|
786
|
+
*/
|
|
787
|
+
export const deferredPath = (stateDir: string, storyId: string): string =>
|
|
788
|
+
join(stateDir, `stories/${storyId}.deferred.md`)
|
|
789
|
+
|
|
790
|
+
/** A deferred finding as one Markdown bullet, prefixed with where it was found. */
|
|
791
|
+
export const deferredLine = (source: string, issue: ReviewIssue): string => {
|
|
792
|
+
const where =
|
|
793
|
+
issue.file === undefined
|
|
794
|
+
? ""
|
|
795
|
+
: ` (${issue.file}${issue.line === undefined ? "" : `:${issue.line}`})`
|
|
796
|
+
const detail = issue.description.trim().length === 0 ? "" : `: ${issue.description.trim()}`
|
|
797
|
+
return `- [${issue.severity}] ${source} — ${issue.title}${where}${detail}`
|
|
798
|
+
}
|
|
799
|
+
|
|
765
800
|
const issueLines = (result: ReviewResult): string =>
|
|
766
801
|
result.issues.map((issue) => `- ${issue.title}: ${issue.description}`).join("\n")
|
|
767
802
|
|
|
@@ -843,6 +878,8 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
843
878
|
const epicBranch = options.epicBranch ?? `epic/${plan.epicId}`
|
|
844
879
|
const concurrency = Math.max(1, options.concurrency ?? 3)
|
|
845
880
|
const judgeRounds = Math.max(1, options.judgeRounds ?? 2)
|
|
881
|
+
const mergeRevisions = Math.max(0, options.mergeRevisions ?? 1)
|
|
882
|
+
const deferring = options.deferNonBlocking === true
|
|
846
883
|
const statePath = (story: Story): string => join(options.stateDir, `stories/${story.id}.json`)
|
|
847
884
|
const planPath = (story: Story): string => join(options.stateDir, `stories/${story.id}.plan.md`)
|
|
848
885
|
/** The judge's last verdict beside a fingerprint of what it judged. */
|
|
@@ -981,8 +1018,15 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
981
1018
|
|
|
982
1019
|
const mergeLock = yield* Semaphore.make(1)
|
|
983
1020
|
|
|
984
|
-
/**
|
|
985
|
-
|
|
1021
|
+
/**
|
|
1022
|
+
* Merge the story branch into the epic branch and re-gate the epic head,
|
|
1023
|
+
* one story at a time. Red gates undo the merge and are returned (the
|
|
1024
|
+
* failures the merge added); `undefined` means merged.
|
|
1025
|
+
*/
|
|
1026
|
+
const integrate = (
|
|
1027
|
+
story: Story,
|
|
1028
|
+
branch: string
|
|
1029
|
+
): Effect.Effect<ReviewResult | undefined, FlowError> =>
|
|
986
1030
|
Effect.flatMap(Clock.currentTimeMillis, (requested) =>
|
|
987
1031
|
Effect.andThen(
|
|
988
1032
|
laneOf(story).publish(Began.make({ kind: "wait", label: "merge lock" })),
|
|
@@ -1003,22 +1047,36 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1003
1047
|
Timed.make({ kind: "merge", label: "merge", ms, ...(failed ? { failed } : {}) }),
|
|
1004
1048
|
() => Began.make({ kind: "merge", label: "merge" })
|
|
1005
1049
|
)
|
|
1006
|
-
|
|
1050
|
+
// The whole output is kept: the reason below names only the
|
|
1051
|
+
// failing lines, which is not enough to tell why.
|
|
1052
|
+
const gate = yield* options.gates(context.workDir, lane, {
|
|
1053
|
+
files,
|
|
1054
|
+
dir: mergeGateLogDir(options.stateDir, story.id)
|
|
1055
|
+
})
|
|
1007
1056
|
// Charged only with what the merge added: the head before it had
|
|
1008
|
-
// a baseline (run start or the previous merge).
|
|
1057
|
+
// a baseline (run start or the previous merge). A new line green
|
|
1058
|
+
// on one rerun of the test gate is flaky, as in the worktree.
|
|
1009
1059
|
const before = yield* ensureBaseline(
|
|
1010
1060
|
checkpoint,
|
|
1011
1061
|
Effect.succeed(ReviewResult.make({ issues: [] })),
|
|
1012
1062
|
context.workDir
|
|
1013
1063
|
)
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1064
|
+
const blocking = yield* applyTriage(
|
|
1065
|
+
gate,
|
|
1066
|
+
{
|
|
1067
|
+
baseline: Effect.succeed(before),
|
|
1068
|
+
roots: rootsOf(context.workDir),
|
|
1069
|
+
...(options.testGate === undefined
|
|
1070
|
+
? {}
|
|
1071
|
+
: { rerunTest: options.testGate(context.workDir, lane) })
|
|
1072
|
+
},
|
|
1073
|
+
lane,
|
|
1074
|
+
yield* Ref.make<ReadonlySet<string>>(new Set())
|
|
1075
|
+
)
|
|
1076
|
+
if (!blocking.isClean) {
|
|
1016
1077
|
// Never leave a red epic head for the next story to inherit.
|
|
1017
1078
|
yield* context.git.rollback(checkpoint)
|
|
1018
|
-
return
|
|
1019
|
-
story,
|
|
1020
|
-
`epic gates failed after merging; merge undone:\n${issueLines(verdict.blocking)}`
|
|
1021
|
-
)
|
|
1079
|
+
return blocking
|
|
1022
1080
|
}
|
|
1023
1081
|
if (gateCommands !== undefined) {
|
|
1024
1082
|
const mergedHead = yield* context.git.checkpoint
|
|
@@ -1032,6 +1090,7 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1032
1090
|
yield* laneOf(story).publish(
|
|
1033
1091
|
Info.make({ message: `story ${story.id}: merged into ${epicBranch}` })
|
|
1034
1092
|
)
|
|
1093
|
+
return undefined
|
|
1035
1094
|
})
|
|
1036
1095
|
)
|
|
1037
1096
|
)
|
|
@@ -1144,7 +1203,9 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1144
1203
|
/** Everything that happens inside a story's worktree: plan, implement, judge, perimeter. */
|
|
1145
1204
|
const implementStory = Effect.fn("@llm4ts/flow/Stories.implementStory")(function* (
|
|
1146
1205
|
story: Story,
|
|
1147
|
-
state: StoryState
|
|
1206
|
+
state: StoryState,
|
|
1207
|
+
/** The epic gates' failures after the story's last merge: run as a revision task. */
|
|
1208
|
+
mergeFailure?: ReviewResult
|
|
1148
1209
|
): Effect.fn.Return<
|
|
1149
1210
|
{
|
|
1150
1211
|
readonly judge: string | undefined
|
|
@@ -1196,6 +1257,8 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1196
1257
|
)
|
|
1197
1258
|
/** What the coder claimed to have run and did not, task by task, for the judge. */
|
|
1198
1259
|
const evidenceNotes: Array<string> = []
|
|
1260
|
+
/** Findings left for a follow-up round (ADR 0031), written when the story finishes. */
|
|
1261
|
+
const deferred: Array<string> = []
|
|
1199
1262
|
const checkEvidence = (
|
|
1200
1263
|
task: Task,
|
|
1201
1264
|
trailer: Trailer,
|
|
@@ -1466,6 +1529,34 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1466
1529
|
yield* git.commitAll(`${story.id}: put paths outside the perimeter back`)
|
|
1467
1530
|
})
|
|
1468
1531
|
|
|
1532
|
+
/** A revision task on the story's own plan, run by the task loop like any other. */
|
|
1533
|
+
const appendRevision = (subject: string, lead: string, verdict: ReviewResult) =>
|
|
1534
|
+
Effect.gen(function* () {
|
|
1535
|
+
const store = makePlanStore(files)
|
|
1536
|
+
const current = yield* store.load(planPath(story))
|
|
1537
|
+
if (current === undefined) {
|
|
1538
|
+
return
|
|
1539
|
+
}
|
|
1540
|
+
const number = current.tasks.filter((task) => revisionTitle.test(task.title)).length + 1
|
|
1541
|
+
yield* store.save(
|
|
1542
|
+
planPath(story),
|
|
1543
|
+
Plan.make({
|
|
1544
|
+
...current,
|
|
1545
|
+
tasks: [
|
|
1546
|
+
...current.tasks,
|
|
1547
|
+
Task.make({
|
|
1548
|
+
title: `Revision ${number}: ${subject}`,
|
|
1549
|
+
description: [
|
|
1550
|
+
lead,
|
|
1551
|
+
"Close these gaps without weakening any test and without leaving your owned paths:",
|
|
1552
|
+
issueLines(verdict)
|
|
1553
|
+
].join("\n")
|
|
1554
|
+
})
|
|
1555
|
+
]
|
|
1556
|
+
})
|
|
1557
|
+
)
|
|
1558
|
+
})
|
|
1559
|
+
|
|
1469
1560
|
const planTasks = options.planTasks ?? defaultPlanTasks
|
|
1470
1561
|
// Taken after catch-up and setup: the code as the first task finds it.
|
|
1471
1562
|
const startingCode = yield* startingCodeOf(
|
|
@@ -1513,13 +1604,21 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1513
1604
|
// the story's findings log for people to read.
|
|
1514
1605
|
reviewCache: { files, dir: reviewCacheDir(story) },
|
|
1515
1606
|
onReview: (task, round, result, settled) =>
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1607
|
+
Effect.gen(function* () {
|
|
1608
|
+
// A settled round's leftovers are deferred, not dropped: when the
|
|
1609
|
+
// rounds ran out, blocking ones too, since the task commits anyway.
|
|
1610
|
+
const left = settled && deferring ? result.issues : []
|
|
1611
|
+
for (const issue of left) {
|
|
1612
|
+
deferred.push(deferredLine(`review "${task.title}"`, issue))
|
|
1613
|
+
}
|
|
1614
|
+
yield* appendFindings(
|
|
1615
|
+
story,
|
|
1616
|
+
`review "${task.title}" round ${round} — ${
|
|
1617
|
+
result.isClean ? "clean" : `${result.issues.length} issue(s)`
|
|
1618
|
+
}${settled ? (left.length === 0 ? "" : ", deferred") : ", fixing"}`,
|
|
1619
|
+
result
|
|
1620
|
+
)
|
|
1621
|
+
}),
|
|
1523
1622
|
onTaskReply: (task, _reply, trailer, startedAt) => checkEvidence(task, trailer, startedAt),
|
|
1524
1623
|
// The repository's own review rules ride along as one extra lens
|
|
1525
1624
|
// (ADR 0027 decision 9), read from the epic checkout.
|
|
@@ -1528,8 +1627,22 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1528
1627
|
...(options.fixer === undefined ? {} : { fixer: options.fixer }),
|
|
1529
1628
|
...(options.stall === undefined ? {} : { stall: options.stall }),
|
|
1530
1629
|
...(options.reviewers === undefined ? {} : { reviewers: options.reviewers }),
|
|
1531
|
-
...(options.maxRounds === undefined ? {} : { maxRounds: options.maxRounds })
|
|
1630
|
+
...(options.maxRounds === undefined ? {} : { maxRounds: options.maxRounds }),
|
|
1631
|
+
...(deferring ? { settle: "blocking" as const } : {})
|
|
1532
1632
|
})
|
|
1633
|
+
if (mergeFailure !== undefined) {
|
|
1634
|
+
// The branch already caught up with the epic above: the gates in the
|
|
1635
|
+
// worktree now see the combination that was red after the merge.
|
|
1636
|
+
yield* appendRevision(
|
|
1637
|
+
"make the epic gates green after merging",
|
|
1638
|
+
[
|
|
1639
|
+
`The story "${story.title}" was merged into ${epicBranch} and the epic's gates went red, so the merge was undone.`,
|
|
1640
|
+
`Your branch now includes everything merged into ${epicBranch} since you started: the failures`,
|
|
1641
|
+
"come from your change combined with that work. Fix them on your side."
|
|
1642
|
+
].join("\n"),
|
|
1643
|
+
mergeFailure
|
|
1644
|
+
)
|
|
1645
|
+
}
|
|
1533
1646
|
yield* guarded(implementTasks, implementTasks)
|
|
1534
1647
|
|
|
1535
1648
|
// Other stories may have merged while this one ran: judge the branch
|
|
@@ -1540,33 +1653,13 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1540
1653
|
// A judge finding becomes a revision task on the story's own plan, run by
|
|
1541
1654
|
// the task loop like any other: coder, review, gates, commit.
|
|
1542
1655
|
const addRevision = (verdict: ReviewResult, empty: boolean): Effect.Effect<void, FlowError> =>
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
yield* store.save(
|
|
1551
|
-
planPath(story),
|
|
1552
|
-
Plan.make({
|
|
1553
|
-
...current,
|
|
1554
|
-
tasks: [
|
|
1555
|
-
...current.tasks,
|
|
1556
|
-
Task.make({
|
|
1557
|
-
title: `Revision ${number}: close the judge's findings`,
|
|
1558
|
-
description: [
|
|
1559
|
-
empty
|
|
1560
|
-
? `The story "${story.title}" has no changes yet, and what it must provide is not all in place on the epic branch.`
|
|
1561
|
-
: `The story "${story.title}" was judged short of done.`,
|
|
1562
|
-
"Close these gaps without weakening any test and without leaving your owned paths:",
|
|
1563
|
-
issueLines(verdict)
|
|
1564
|
-
].join("\n")
|
|
1565
|
-
})
|
|
1566
|
-
]
|
|
1567
|
-
})
|
|
1568
|
-
)
|
|
1569
|
-
})
|
|
1656
|
+
appendRevision(
|
|
1657
|
+
"close the judge's findings",
|
|
1658
|
+
empty
|
|
1659
|
+
? `The story "${story.title}" has no changes yet, and what it must provide is not all in place on the epic branch.`
|
|
1660
|
+
: `The story "${story.title}" was judged short of done.`,
|
|
1661
|
+
verdict
|
|
1662
|
+
)
|
|
1570
1663
|
|
|
1571
1664
|
let judgeNote: string | undefined
|
|
1572
1665
|
let inPlace = false
|
|
@@ -1684,11 +1777,17 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1684
1777
|
dimensions: verdict instanceof StoryVerdict ? verdict.dimensions : []
|
|
1685
1778
|
})
|
|
1686
1779
|
)
|
|
1687
|
-
|
|
1780
|
+
// Deferring, a verdict with nothing blocking clears: what it found
|
|
1781
|
+
// waits for the follow-up round instead of another revision.
|
|
1782
|
+
if (verdict.isClean || (deferring && !verdict.issues.some(isBlocking))) {
|
|
1783
|
+
const waiting = nonBlockingIssues(verdict)
|
|
1784
|
+
for (const issue of waiting) {
|
|
1785
|
+
deferred.push(deferredLine(`judge round ${round}`, issue))
|
|
1786
|
+
}
|
|
1688
1787
|
inPlace = empty
|
|
1689
|
-
judgeNote = empty
|
|
1690
|
-
|
|
1691
|
-
|
|
1788
|
+
judgeNote = `${empty ? "verified already in place" : "judge cleared"} (round ${round}${
|
|
1789
|
+
waiting.length === 0 ? "" : `, ${waiting.length} finding(s) deferred`
|
|
1790
|
+
})`
|
|
1692
1791
|
break
|
|
1693
1792
|
}
|
|
1694
1793
|
if (round >= judgeRounds) {
|
|
@@ -1709,6 +1808,19 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1709
1808
|
|
|
1710
1809
|
const changed = yield* git.changedFilesVsBase(epicBranch)
|
|
1711
1810
|
yield* enforcePerimeter(changed, story)
|
|
1811
|
+
if (deferring) {
|
|
1812
|
+
// A merge revision adds to what the story's first run deferred.
|
|
1813
|
+
if (mergeFailure !== undefined) {
|
|
1814
|
+
const earlier = yield* files.read(deferredPath(options.stateDir, story.id))
|
|
1815
|
+
deferred.unshift(...(earlier ?? "").split("\n").filter((line) => line.startsWith("- [")))
|
|
1816
|
+
}
|
|
1817
|
+
yield* deferred.length === 0
|
|
1818
|
+
? files.remove(deferredPath(options.stateDir, story.id))
|
|
1819
|
+
: files.writeAtomic(
|
|
1820
|
+
deferredPath(options.stateDir, story.id),
|
|
1821
|
+
[`# Deferred from ${story.id}: ${story.title}`, "", ...new Set(deferred), ""].join("\n")
|
|
1822
|
+
)
|
|
1823
|
+
}
|
|
1712
1824
|
const totals = seats.totals === undefined ? undefined : yield* seats.totals
|
|
1713
1825
|
const history = roster === undefined ? [] : yield* roster.history
|
|
1714
1826
|
return {
|
|
@@ -1736,14 +1848,38 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1736
1848
|
judge: "merged on a previous run"
|
|
1737
1849
|
})
|
|
1738
1850
|
}
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1851
|
+
let result = yield* Effect.scoped(implementStory(story, state))
|
|
1852
|
+
for (let revision = 0; ; revision += 1) {
|
|
1853
|
+
if (result.inPlace) {
|
|
1854
|
+
// Verified already on the epic: there is nothing to merge or re-gate.
|
|
1855
|
+
yield* laneOf(story).publish(
|
|
1856
|
+
Info.make({ message: `story ${story.id}: verified already in place; nothing to merge` })
|
|
1857
|
+
)
|
|
1858
|
+
break
|
|
1859
|
+
}
|
|
1860
|
+
const red = yield* integrate(story, state.branch)
|
|
1861
|
+
if (red === undefined) {
|
|
1862
|
+
break
|
|
1863
|
+
}
|
|
1864
|
+
const logs = red.issues.flatMap((issue) =>
|
|
1865
|
+
issue.logPath === undefined ? [] : [issue.logPath]
|
|
1866
|
+
)
|
|
1867
|
+
const why = `${issueLines(red)}${logs.length === 0 ? "" : `\nfull output: ${logs.join(", ")}`}`
|
|
1868
|
+
if (revision >= mergeRevisions) {
|
|
1869
|
+
return yield* failed(
|
|
1870
|
+
story,
|
|
1871
|
+
`epic gates failed after merging${
|
|
1872
|
+
revision === 0 ? "" : ` (after ${revision} merge revision${revision === 1 ? "" : "s"})`
|
|
1873
|
+
}; merge undone:\n${why}`
|
|
1874
|
+
)
|
|
1875
|
+
}
|
|
1876
|
+
yield* appendFindings(story, `epic gates after merging — red, merge undone; revising`, red)
|
|
1742
1877
|
yield* laneOf(story).publish(
|
|
1743
|
-
Info.make({
|
|
1878
|
+
Info.make({
|
|
1879
|
+
message: `story ${story.id}: epic gates red after merging; merge undone, sending it back to catch up and fix (merge revision ${revision + 1} of ${mergeRevisions})`
|
|
1880
|
+
})
|
|
1744
1881
|
)
|
|
1745
|
-
|
|
1746
|
-
yield* integrate(story, state.branch)
|
|
1882
|
+
result = yield* Effect.scoped(implementStory(story, state, red))
|
|
1747
1883
|
}
|
|
1748
1884
|
const last = result.executor?.split(" → ").at(-1)
|
|
1749
1885
|
yield* saveVersioned(
|