@llm4ts/flow 1.0.0 → 2.1.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/Approval.d.ts.map +1 -1
- package/dist/Approval.js +2 -2
- package/dist/Approval.js.map +1 -1
- package/dist/BoardSync.d.ts +12 -5
- package/dist/BoardSync.d.ts.map +1 -1
- package/dist/BoardSync.js +27 -6
- package/dist/BoardSync.js.map +1 -1
- package/dist/Context.d.ts +1 -4
- package/dist/Context.d.ts.map +1 -1
- package/dist/Context.js +2 -5
- package/dist/Context.js.map +1 -1
- package/dist/FlowError.d.ts.map +1 -1
- package/dist/FlowError.js +17 -17
- package/dist/FlowError.js.map +1 -1
- package/dist/Pack.d.ts.map +1 -1
- package/dist/Pack.js +7 -1
- package/dist/Pack.js.map +1 -1
- package/dist/PageSpec.d.ts +1 -0
- package/dist/PageSpec.d.ts.map +1 -1
- package/dist/PageSpec.js +3 -2
- package/dist/PageSpec.js.map +1 -1
- package/dist/ProgramJudge.d.ts +1 -1
- package/dist/ProgramJudge.js +3 -3
- package/dist/ProgramJudge.js.map +1 -1
- package/dist/Stories.d.ts +4 -3
- package/dist/Stories.d.ts.map +1 -1
- package/dist/Stories.js +20 -10
- package/dist/Stories.js.map +1 -1
- package/dist/StoryPlan.d.ts +2 -1
- package/dist/StoryPlan.d.ts.map +1 -1
- package/dist/StoryPlan.js +1 -1
- package/dist/StoryPlan.js.map +1 -1
- package/package.json +4 -4
- package/src/Approval.ts +5 -8
- package/src/BoardSync.ts +35 -6
- package/src/Context.ts +2 -5
- package/src/FlowError.ts +22 -28
- package/src/Pack.ts +8 -1
- package/src/PageSpec.ts +3 -2
- package/src/ProgramJudge.ts +3 -3
- package/src/Stories.ts +20 -10
- package/src/StoryPlan.ts +3 -2
package/src/BoardSync.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { FlowAborted, type FlowError } from "./FlowError.ts"
|
|
|
7
7
|
import { loadVersioned, saveVersioned, type PlainFileStoreShape } from "./Persistence.ts"
|
|
8
8
|
|
|
9
9
|
// The progress board of the conversion scenario: a port with the work-item
|
|
10
|
-
// lifecycle (plan → start → complete/fail/skip) and two adapters — a local
|
|
10
|
+
// lifecycle (plan → start → complete/fail/wait/skip) and two adapters — a local
|
|
11
11
|
// file board (the default, fully offline: board.json + rendered board.md)
|
|
12
12
|
// and an Azure DevOps adapter (the stretch goal) mapping the same lifecycle
|
|
13
13
|
// onto work items. Flows depend only on the port; which board the client
|
|
@@ -15,7 +15,14 @@ import { loadVersioned, saveVersioned, type PlainFileStoreShape } from "./Persis
|
|
|
15
15
|
|
|
16
16
|
export const BoardVersion = 1
|
|
17
17
|
|
|
18
|
-
export const BoardStatus = Schema.Literals([
|
|
18
|
+
export const BoardStatus = Schema.Literals([
|
|
19
|
+
"planned",
|
|
20
|
+
"active",
|
|
21
|
+
"waiting",
|
|
22
|
+
"done",
|
|
23
|
+
"failed",
|
|
24
|
+
"skipped"
|
|
25
|
+
])
|
|
19
26
|
export type BoardStatus = typeof BoardStatus.Type
|
|
20
27
|
|
|
21
28
|
export class BoardItem extends Schema.Class<BoardItem>("BoardItem")({
|
|
@@ -25,7 +32,7 @@ export class BoardItem extends Schema.Class<BoardItem>("BoardItem")({
|
|
|
25
32
|
status: BoardStatus,
|
|
26
33
|
branch: Schema.optionalKey(Schema.String),
|
|
27
34
|
reportPath: Schema.optionalKey(Schema.String),
|
|
28
|
-
/** Free-form note: a failure reason,
|
|
35
|
+
/** Free-form note: a failure reason, what an item waits for, a triage disposition. */
|
|
29
36
|
detail: Schema.optionalKey(Schema.String),
|
|
30
37
|
/** ESTIMATES, never measurements — see EstimatedUsage (ADR 0012). */
|
|
31
38
|
estimatedTokens: Schema.optionalKey(Schema.Int),
|
|
@@ -51,13 +58,20 @@ export interface BoardSyncShape {
|
|
|
51
58
|
readonly start: (id: string) => Effect.Effect<void, FlowError>
|
|
52
59
|
readonly complete: (id: string, result: BoardItemResult) => Effect.Effect<void, FlowError>
|
|
53
60
|
readonly fail: (id: string, reason: string) => Effect.Effect<void, FlowError>
|
|
61
|
+
/** Final: the item is deliberately left out (a page triaged as dead, say). */
|
|
54
62
|
readonly skip: (id: string, reason: string) => Effect.Effect<void, FlowError>
|
|
63
|
+
/**
|
|
64
|
+
* On hold: the item waits for something that failed (a predecessor story)
|
|
65
|
+
* and runs once that is fixed — unlike `skip`, it is not final.
|
|
66
|
+
*/
|
|
67
|
+
readonly wait: (id: string, reason: string) => Effect.Effect<void, FlowError>
|
|
55
68
|
readonly snapshot: Effect.Effect<Board, FlowError>
|
|
56
69
|
}
|
|
57
70
|
|
|
58
71
|
const sectionOrder: ReadonlyArray<readonly [BoardStatus, string]> = [
|
|
59
72
|
["active", "Active"],
|
|
60
73
|
["planned", "Planned"],
|
|
74
|
+
["waiting", "Waiting"],
|
|
61
75
|
["done", "Done"],
|
|
62
76
|
["failed", "Failed"],
|
|
63
77
|
["skipped", "Skipped"]
|
|
@@ -193,6 +207,8 @@ export const makeLocalBoardSync = (
|
|
|
193
207
|
update(id, (item) => BoardItem.make({ ...item, status: "failed", detail: reason })),
|
|
194
208
|
skip: (id, reason) =>
|
|
195
209
|
update(id, (item) => BoardItem.make({ ...item, status: "skipped", detail: reason })),
|
|
210
|
+
wait: (id, reason) =>
|
|
211
|
+
update(id, (item) => BoardItem.make({ ...item, status: "waiting", detail: reason })),
|
|
196
212
|
snapshot: load
|
|
197
213
|
}
|
|
198
214
|
}
|
|
@@ -213,6 +229,7 @@ export const composeBoardSync = (boards: ReadonlyArray<BoardSyncShape>): BoardSy
|
|
|
213
229
|
complete: (id, result) => each((board) => board.complete(id, result)),
|
|
214
230
|
fail: (id, reason) => each((board) => board.fail(id, reason)),
|
|
215
231
|
skip: (id, reason) => each((board) => board.skip(id, reason)),
|
|
232
|
+
wait: (id, reason) => each((board) => board.wait(id, reason)),
|
|
216
233
|
snapshot:
|
|
217
234
|
first === undefined
|
|
218
235
|
? Effect.succeed(Board.make({ title: "empty", items: [] }))
|
|
@@ -242,8 +259,9 @@ const markerQuery = (tag: string, id: string): string =>
|
|
|
242
259
|
* The stretch adapter (ADR 0012): the same lifecycle mapped onto Azure DevOps
|
|
243
260
|
* work items through the az-CLI AzureDevOpsTool (ADR 0011). Idempotent by
|
|
244
261
|
* title marker — `[<id>]` plus the board tag — so a re-run finds its items
|
|
245
|
-
* instead of duplicating them. Failure and skip are tags plus a
|
|
246
|
-
* never invented states
|
|
262
|
+
* instead of duplicating them. Failure, waiting and skip are tags plus a
|
|
263
|
+
* comment, never invented states; a waiting tag is lifted when the item is
|
|
264
|
+
* started again.
|
|
247
265
|
*/
|
|
248
266
|
export const makeAdoBoardSync = Effect.fn("@llm4ts/flow/BoardSync.makeAdo")(function* (
|
|
249
267
|
ado: AzureDevOpsToolShape,
|
|
@@ -334,6 +352,12 @@ export const makeAdoBoardSync = Effect.fn("@llm4ts/flow/BoardSync.makeAdo")(func
|
|
|
334
352
|
yield* ado.editTags(workItem, [`${tag}-skipped`], [])
|
|
335
353
|
yield* ado.writeComment(workItem, `SKIPPED: ${reason}`)
|
|
336
354
|
}),
|
|
355
|
+
wait: (id, reason) =>
|
|
356
|
+
Effect.gen(function* () {
|
|
357
|
+
const workItem = yield* lookup(id)
|
|
358
|
+
yield* ado.editTags(workItem, [`${tag}-waiting`], [])
|
|
359
|
+
yield* ado.writeComment(workItem, `WAITING: ${reason}`)
|
|
360
|
+
}),
|
|
337
361
|
snapshot: Effect.gen(function* () {
|
|
338
362
|
const found = yield* ado.wiqlIds(
|
|
339
363
|
`SELECT [System.Id] FROM WorkItems WHERE [System.Tags] CONTAINS ${quoteWiql(tag)}`
|
|
@@ -344,6 +368,9 @@ export const makeAdoBoardSync = Effect.fn("@llm4ts/flow/BoardSync.makeAdo")(func
|
|
|
344
368
|
const match = /^\[([^\]]+)\] (.*)$/.exec(workItem.title)
|
|
345
369
|
const failed = workItem.tags.includes(`${tag}-failed`)
|
|
346
370
|
const skipped = workItem.tags.includes(`${tag}-skipped`)
|
|
371
|
+
const waiting = workItem.tags.includes(`${tag}-waiting`)
|
|
372
|
+
// A live state outranks a waiting tag: an item started again after
|
|
373
|
+
// its predecessor was fixed is active, whatever tag it still carries.
|
|
347
374
|
const status: BoardStatus = failed
|
|
348
375
|
? "failed"
|
|
349
376
|
: skipped
|
|
@@ -352,7 +379,9 @@ export const makeAdoBoardSync = Effect.fn("@llm4ts/flow/BoardSync.makeAdo")(func
|
|
|
352
379
|
? "done"
|
|
353
380
|
: workItem.state === states.active
|
|
354
381
|
? "active"
|
|
355
|
-
:
|
|
382
|
+
: waiting
|
|
383
|
+
? "waiting"
|
|
384
|
+
: "planned"
|
|
356
385
|
items.push(
|
|
357
386
|
BoardItem.make({
|
|
358
387
|
id: match?.[1] ?? String(workItemId),
|
package/src/Context.ts
CHANGED
|
@@ -44,14 +44,11 @@ export const cap = (text: string, limit: number): CappedText => {
|
|
|
44
44
|
|
|
45
45
|
export const defaultContextBudget = 400_000
|
|
46
46
|
|
|
47
|
-
/**
|
|
48
|
-
* The default character budget: `LLM4TS_CONTEXT_BUDGET`, else the deprecated
|
|
49
|
-
* `LLM4TS_JUDGE_SOURCES_LIMIT`, else 400_000.
|
|
50
|
-
*/
|
|
47
|
+
/** The default character budget: `LLM4TS_CONTEXT_BUDGET`, else 400_000. */
|
|
51
48
|
export const budget = (
|
|
52
49
|
environment: Readonly<Record<string, string | undefined>> = process.env
|
|
53
50
|
): number => {
|
|
54
|
-
const raw = environment["LLM4TS_CONTEXT_BUDGET"]
|
|
51
|
+
const raw = environment["LLM4TS_CONTEXT_BUDGET"]
|
|
55
52
|
if (raw === undefined) {
|
|
56
53
|
return defaultContextBudget
|
|
57
54
|
}
|
package/src/FlowError.ts
CHANGED
|
@@ -2,16 +2,16 @@ import * as Schema from "effect/Schema"
|
|
|
2
2
|
import { Capability } from "@llm4ts/core/Capability"
|
|
3
3
|
import { LlmError } from "@llm4ts/core/Errors"
|
|
4
4
|
|
|
5
|
-
export class PersistenceError extends Schema.
|
|
5
|
+
export class PersistenceError extends Schema.TaggedError<PersistenceError>()("Persistence", {
|
|
6
6
|
message: Schema.String,
|
|
7
7
|
cause: Schema.optionalKey(Schema.Defect())
|
|
8
8
|
}) {}
|
|
9
9
|
|
|
10
|
-
export class PlanParseError extends Schema.
|
|
10
|
+
export class PlanParseError extends Schema.TaggedError<PlanParseError>()("PlanParse", {
|
|
11
11
|
message: Schema.String
|
|
12
12
|
}) {}
|
|
13
13
|
|
|
14
|
-
export class UnsupportedSchemaVersion extends Schema.
|
|
14
|
+
export class UnsupportedSchemaVersion extends Schema.TaggedError<UnsupportedSchemaVersion>()(
|
|
15
15
|
"UnsupportedSchemaVersion",
|
|
16
16
|
{
|
|
17
17
|
path: Schema.String,
|
|
@@ -24,15 +24,12 @@ export class UnsupportedSchemaVersion extends Schema.TaggedErrorClass<Unsupporte
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export class WorkspacePathError extends Schema.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
message: Schema.String
|
|
32
|
-
}
|
|
33
|
-
) {}
|
|
27
|
+
export class WorkspacePathError extends Schema.TaggedError<WorkspacePathError>()("WorkspacePath", {
|
|
28
|
+
path: Schema.String,
|
|
29
|
+
message: Schema.String
|
|
30
|
+
}) {}
|
|
34
31
|
|
|
35
|
-
export class WorkspaceLimitError extends Schema.
|
|
32
|
+
export class WorkspaceLimitError extends Schema.TaggedError<WorkspaceLimitError>()(
|
|
36
33
|
"WorkspaceLimit",
|
|
37
34
|
{
|
|
38
35
|
operation: Schema.String,
|
|
@@ -47,18 +44,18 @@ export class WorkspaceLimitError extends Schema.TaggedErrorClass<WorkspaceLimitE
|
|
|
47
44
|
}
|
|
48
45
|
}
|
|
49
46
|
|
|
50
|
-
export class WorkspaceIoError extends Schema.
|
|
47
|
+
export class WorkspaceIoError extends Schema.TaggedError<WorkspaceIoError>()("WorkspaceIo", {
|
|
51
48
|
operation: Schema.String,
|
|
52
49
|
path: Schema.String,
|
|
53
50
|
message: Schema.String,
|
|
54
51
|
cause: Schema.optionalKey(Schema.Defect())
|
|
55
52
|
}) {}
|
|
56
53
|
|
|
57
|
-
export class FlowAborted extends Schema.
|
|
54
|
+
export class FlowAborted extends Schema.TaggedError<FlowAborted>()("Aborted", {
|
|
58
55
|
message: Schema.String
|
|
59
56
|
}) {}
|
|
60
57
|
|
|
61
|
-
export class ProcessError extends Schema.
|
|
58
|
+
export class ProcessError extends Schema.TaggedError<ProcessError>()("Process", {
|
|
62
59
|
message: Schema.String,
|
|
63
60
|
detail: Schema.String
|
|
64
61
|
}) {}
|
|
@@ -80,7 +77,7 @@ export const describeFlowError = (error: unknown): string => {
|
|
|
80
77
|
return detail.length === 0 || base.includes(detail) ? base : `${base}: ${detail}`
|
|
81
78
|
}
|
|
82
79
|
|
|
83
|
-
export class FlowLlmError extends Schema.
|
|
80
|
+
export class FlowLlmError extends Schema.TaggedError<FlowLlmError>()("Llm", {
|
|
84
81
|
message: Schema.String,
|
|
85
82
|
cause: Schema.optionalKey(LlmError)
|
|
86
83
|
}) {
|
|
@@ -91,7 +88,7 @@ export class FlowLlmError extends Schema.TaggedErrorClass<FlowLlmError>()("Llm",
|
|
|
91
88
|
})
|
|
92
89
|
}
|
|
93
90
|
|
|
94
|
-
export class FlowCapabilityDenied extends Schema.
|
|
91
|
+
export class FlowCapabilityDenied extends Schema.TaggedError<FlowCapabilityDenied>()(
|
|
95
92
|
"CapabilityDenied",
|
|
96
93
|
{
|
|
97
94
|
capability: Capability,
|
|
@@ -105,7 +102,7 @@ export class FlowCapabilityDenied extends Schema.TaggedErrorClass<FlowCapability
|
|
|
105
102
|
}
|
|
106
103
|
}
|
|
107
104
|
|
|
108
|
-
export class ColumnNotFound extends Schema.
|
|
105
|
+
export class ColumnNotFound extends Schema.TaggedError<ColumnNotFound>()("ColumnNotFound", {
|
|
109
106
|
title: Schema.String,
|
|
110
107
|
available: Schema.Array(Schema.String)
|
|
111
108
|
}) {
|
|
@@ -114,7 +111,7 @@ export class ColumnNotFound extends Schema.TaggedErrorClass<ColumnNotFound>()("C
|
|
|
114
111
|
}
|
|
115
112
|
}
|
|
116
113
|
|
|
117
|
-
export class BudgetExceeded extends Schema.
|
|
114
|
+
export class BudgetExceeded extends Schema.TaggedError<BudgetExceeded>()("BudgetExceeded", {
|
|
118
115
|
metric: Schema.Literals(["tokens", "costUsd"]),
|
|
119
116
|
limit: Schema.Number,
|
|
120
117
|
actual: Schema.Number
|
|
@@ -125,19 +122,16 @@ export class BudgetExceeded extends Schema.TaggedErrorClass<BudgetExceeded>()("B
|
|
|
125
122
|
}
|
|
126
123
|
|
|
127
124
|
/** A story plan that failed deterministic validation — every violation, not the first (ADR 0013). */
|
|
128
|
-
export class StoryPlanInvalid extends Schema.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
violations: Schema.Array(Schema.String)
|
|
132
|
-
}
|
|
133
|
-
) {
|
|
125
|
+
export class StoryPlanInvalid extends Schema.TaggedError<StoryPlanInvalid>()("StoryPlanInvalid", {
|
|
126
|
+
violations: Schema.Array(Schema.String)
|
|
127
|
+
}) {
|
|
134
128
|
get message(): string {
|
|
135
129
|
return `story plan invalid:\n${this.violations.map((violation) => `- ${violation}`).join("\n")}`
|
|
136
130
|
}
|
|
137
131
|
}
|
|
138
132
|
|
|
139
133
|
/** A story branch changed paths outside the story's declared `owned` set. */
|
|
140
|
-
export class PerimeterViolation extends Schema.
|
|
134
|
+
export class PerimeterViolation extends Schema.TaggedError<PerimeterViolation>()(
|
|
141
135
|
"PerimeterViolation",
|
|
142
136
|
{
|
|
143
137
|
story: Schema.String,
|
|
@@ -158,7 +152,7 @@ export class PerimeterViolation extends Schema.TaggedErrorClass<PerimeterViolati
|
|
|
158
152
|
}
|
|
159
153
|
|
|
160
154
|
/** The coder ended a story with `BLOCKED_ON:` — unplanned work belongs to another story. */
|
|
161
|
-
export class MissingDependency extends Schema.
|
|
155
|
+
export class MissingDependency extends Schema.TaggedError<MissingDependency>()(
|
|
162
156
|
"MissingDependency",
|
|
163
157
|
{
|
|
164
158
|
story: Schema.String,
|
|
@@ -171,7 +165,7 @@ export class MissingDependency extends Schema.TaggedErrorClass<MissingDependency
|
|
|
171
165
|
}
|
|
172
166
|
|
|
173
167
|
/** A story branch did not merge cleanly into the epic branch; the merge was aborted. */
|
|
174
|
-
export class MergeConflict extends Schema.
|
|
168
|
+
export class MergeConflict extends Schema.TaggedError<MergeConflict>()("MergeConflict", {
|
|
175
169
|
branch: Schema.String,
|
|
176
170
|
into: Schema.String,
|
|
177
171
|
paths: Schema.Array(Schema.String)
|
|
@@ -183,7 +177,7 @@ export class MergeConflict extends Schema.TaggedErrorClass<MergeConflict>()("Mer
|
|
|
183
177
|
}
|
|
184
178
|
|
|
185
179
|
/** One story failed; carries the story id so a fail-fast run names its cause. */
|
|
186
|
-
export class StoryFailed extends Schema.
|
|
180
|
+
export class StoryFailed extends Schema.TaggedError<StoryFailed>()("StoryFailed", {
|
|
187
181
|
story: Schema.String,
|
|
188
182
|
reason: Schema.String
|
|
189
183
|
}) {
|
package/src/Pack.ts
CHANGED
|
@@ -202,7 +202,14 @@ export const loadPack = Effect.fn("@llm4ts/flow/Pack.load")(function* (
|
|
|
202
202
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
203
203
|
.map(([name, text]) => parseReviewer(name, text))
|
|
204
204
|
const fields = manifest.fields
|
|
205
|
-
|
|
205
|
+
if (fields["programFiles"] !== undefined) {
|
|
206
|
+
return yield* PlanParseError.make({
|
|
207
|
+
message:
|
|
208
|
+
"pack manifest 'programFiles:' was renamed 'program-files:' in llm4ts 2.0 — " +
|
|
209
|
+
"rename the field (the value is unchanged)"
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
const programFiles = fields["program-files"]
|
|
206
213
|
// Validated at load so a mis-typed template fails the pack, not a later
|
|
207
214
|
// phase; substitution cannot introduce invalid syntax because the fallback
|
|
208
215
|
// probe uses an alphanumeric stand-in and real substitutions are escaped
|
package/src/PageSpec.ts
CHANGED
|
@@ -68,7 +68,8 @@ export class PageApiCall extends Schema.Class<PageApiCall>("PageApiCall")({
|
|
|
68
68
|
method: Schema.String,
|
|
69
69
|
path: Schema.String,
|
|
70
70
|
/** The ESB service behind the legacy endpoint, when known. */
|
|
71
|
-
|
|
71
|
+
/** An ESB service identifier (`ESB_ACCT_LIST`), never prose: unknown means omit. */
|
|
72
|
+
esbService: Schema.optionalKey(Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_.:/-]+$/))),
|
|
72
73
|
request: Schema.Array(FieldMapping).pipe(
|
|
73
74
|
Schema.withConstructorDefault(Effect.succeed(emptyMappings)),
|
|
74
75
|
Schema.withDecodingDefaultKey(Effect.succeed(emptyMappings))
|
|
@@ -154,7 +155,7 @@ export const pageSpecShapeHint =
|
|
|
154
155
|
"The block must be exactly: { page, route, title, complexity: low|medium|high, " +
|
|
155
156
|
"forms: [{ name, action, fields: [{ name, label, type, required?, validations: [{ rule, message?, enforcedAt: client|server|both }] }] }], " +
|
|
156
157
|
"dtos: [{ legacyName, domainName, fields: [{ legacyName, domainName, type }] }], " +
|
|
157
|
-
"apiCalls: [{ operation, method, path, esbService (the ESB service the legacy call goes through — omit
|
|
158
|
+
"apiCalls: [{ operation, method, path, esbService (the identifier of the ESB service the legacy call goes through, such as ESB_ACCT_LIST — omit it when unknown or absent; never a sentence), request: [{ legacyName, domainName, type }], response: [{ legacyName, domainName, type }] for an ad-hoc object, or responseDto: <domainName of one of the dtos> with responseShape: single|list when the endpoint returns that DTO or a list of it (a table screen is a list) }], " +
|
|
158
159
|
"navigation: { inbound: [string], outbound: [string], steps: [string] }, sessionState: [string], openQuestions: [string] }. " +
|
|
159
160
|
"No other keys (no id, url, queryParams, esbCall, trigger, serverController); every apiCalls entry is an object with operation/method/path; " +
|
|
160
161
|
"put anything that does not fit into the prose sections or openQuestions."
|
package/src/ProgramJudge.ts
CHANGED
|
@@ -101,7 +101,7 @@ const issues = (
|
|
|
101
101
|
* A spec'd program with NO matching changed file is a deterministic gate
|
|
102
102
|
* failure, not a silent pass. Skipping it would let the branch clear a bar the
|
|
103
103
|
* old whole-branch judge would have failed. It also surfaces a mis-set
|
|
104
|
-
* `
|
|
104
|
+
* `program-files:` immediately — the top documented risk of the per-program
|
|
105
105
|
* design — instead of quietly degrading coverage.
|
|
106
106
|
*/
|
|
107
107
|
export const unimplemented = (programs: ReadonlyArray<string>): ReviewResult =>
|
|
@@ -112,8 +112,8 @@ export const unimplemented = (programs: ReadonlyArray<string>): ReviewResult =>
|
|
|
112
112
|
title: `judge[${program}]: spec'd but no implementation files changed`,
|
|
113
113
|
description:
|
|
114
114
|
`${program} has a committed spec but no file on this branch matches the pack's ` +
|
|
115
|
-
"
|
|
116
|
-
"`
|
|
115
|
+
"program-files regex for it. Either the program is unimplemented, or the pack's " +
|
|
116
|
+
"`program-files:` template does not match this repo's layout — check that before " +
|
|
117
117
|
"assuming the former."
|
|
118
118
|
})
|
|
119
119
|
),
|
package/src/Stories.ts
CHANGED
|
@@ -73,7 +73,8 @@ export class StoryState extends Schema.Class<StoryState>("StoryState")({
|
|
|
73
73
|
|
|
74
74
|
export const EpicReportVersion = 1
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
/** `waiting`: on hold behind a failed predecessor, runs once that is fixed. */
|
|
77
|
+
export const OutcomeStatus = Schema.Literals(["done", "failed", "waiting"])
|
|
77
78
|
export type OutcomeStatus = typeof OutcomeStatus.Type
|
|
78
79
|
|
|
79
80
|
export class StoryOutcome extends Schema.Class<StoryOutcome>("StoryOutcome")({
|
|
@@ -81,7 +82,7 @@ export class StoryOutcome extends Schema.Class<StoryOutcome>("StoryOutcome")({
|
|
|
81
82
|
title: Schema.String,
|
|
82
83
|
status: OutcomeStatus,
|
|
83
84
|
branch: Schema.optionalKey(Schema.String),
|
|
84
|
-
/** Failure or
|
|
85
|
+
/** Failure reason, or what a waiting story waits for. */
|
|
85
86
|
reason: Schema.optionalKey(Schema.String),
|
|
86
87
|
judge: Schema.optionalKey(Schema.String),
|
|
87
88
|
/** ESTIMATES, never measurements (ADR 0012). */
|
|
@@ -111,7 +112,7 @@ export const renderEpicReport = (report: EpicReport): string => {
|
|
|
111
112
|
"> the CLI seats report no usage. They are not measurements.",
|
|
112
113
|
"",
|
|
113
114
|
`- Epic branch: \`${report.epicBranch}\``,
|
|
114
|
-
`- Stories: ${report.stories.length} (done ${report.count("done")}, failed ${report.count("failed")},
|
|
115
|
+
`- Stories: ${report.stories.length} (done ${report.count("done")}, failed ${report.count("failed")}, waiting ${report.count("waiting")})`,
|
|
115
116
|
"",
|
|
116
117
|
"| Story | Status | Branch | Est. tokens | Est. cost | Note |",
|
|
117
118
|
"| --- | --- | --- | --- | --- | --- |"
|
|
@@ -393,6 +394,9 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
393
394
|
if (yield* context.git.branchExists(stored.branch)) {
|
|
394
395
|
yield* context.git.deleteBranch(stored.branch)
|
|
395
396
|
}
|
|
397
|
+
// The task checkpoint belongs to the old branch: left in place, the
|
|
398
|
+
// fresh branch would inherit "every task complete" and skip the coder.
|
|
399
|
+
yield* files.remove(planPath(story))
|
|
396
400
|
stored = undefined
|
|
397
401
|
}
|
|
398
402
|
if (stored !== undefined && stored.status === "merged") {
|
|
@@ -477,6 +481,11 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
477
481
|
const judge = options.judge
|
|
478
482
|
for (let round = 1; round <= judgeRounds; round += 1) {
|
|
479
483
|
const diff = yield* storyContext.git.diffVsBase(epicBranch)
|
|
484
|
+
if (diff.trim().length === 0) {
|
|
485
|
+
// Nothing to judge is a deterministic failure, not a model call:
|
|
486
|
+
// a model asked to score an empty diff scores the prompt instead.
|
|
487
|
+
return yield* failed(story, "the story branch has no changes against the epic branch")
|
|
488
|
+
}
|
|
480
489
|
const verdict = yield* judge(story, diff)
|
|
481
490
|
if (verdict.isClean) {
|
|
482
491
|
judgeNote = `judge cleared (round ${round})`
|
|
@@ -602,7 +611,7 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
602
611
|
return {
|
|
603
612
|
done: byStatus("done"),
|
|
604
613
|
failed: byStatus("failed"),
|
|
605
|
-
|
|
614
|
+
waiting: byStatus("waiting"),
|
|
606
615
|
running: yield* Ref.get(running)
|
|
607
616
|
}
|
|
608
617
|
})
|
|
@@ -610,7 +619,8 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
610
619
|
const record = (outcome: StoryOutcome): Effect.Effect<void> =>
|
|
611
620
|
Ref.update(outcomes, (current) => [...current, outcome])
|
|
612
621
|
|
|
613
|
-
|
|
622
|
+
/** Dependents of a failed story go on hold: they run once it is fixed and rerun. */
|
|
623
|
+
const holdDependents = (story: Story): Effect.Effect<void, FlowError> =>
|
|
614
624
|
Effect.gen(function* () {
|
|
615
625
|
const current = yield* progress
|
|
616
626
|
for (const id of dependentsOf(plan, story.id)) {
|
|
@@ -619,13 +629,13 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
619
629
|
dependent === undefined ||
|
|
620
630
|
current.done.has(id) ||
|
|
621
631
|
current.failed.has(id) ||
|
|
622
|
-
current.
|
|
632
|
+
current.waiting.has(id)
|
|
623
633
|
) {
|
|
624
634
|
continue
|
|
625
635
|
}
|
|
626
|
-
const reason = `
|
|
627
|
-
yield* record(StoryOutcome.make({ id, title: dependent.title, status: "
|
|
628
|
-
yield* board.
|
|
636
|
+
const reason = `waiting for ${story.id}`
|
|
637
|
+
yield* record(StoryOutcome.make({ id, title: dependent.title, status: "waiting", reason }))
|
|
638
|
+
yield* board.wait(id, reason)
|
|
629
639
|
}
|
|
630
640
|
})
|
|
631
641
|
|
|
@@ -673,7 +683,7 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
673
683
|
reason: outcome.reason ?? "failed"
|
|
674
684
|
})
|
|
675
685
|
}
|
|
676
|
-
yield*
|
|
686
|
+
yield* holdDependents(completion.story)
|
|
677
687
|
}
|
|
678
688
|
}
|
|
679
689
|
})
|
package/src/StoryPlan.ts
CHANGED
|
@@ -182,7 +182,8 @@ export const topologicalWaves = (plan: StoryPlan): ReadonlyArray<ReadonlyArray<s
|
|
|
182
182
|
export interface StoryProgress {
|
|
183
183
|
readonly done: ReadonlySet<string>
|
|
184
184
|
readonly failed: ReadonlySet<string>
|
|
185
|
-
|
|
185
|
+
/** On hold behind a failed predecessor. */
|
|
186
|
+
readonly waiting: ReadonlySet<string>
|
|
186
187
|
readonly running: ReadonlySet<string>
|
|
187
188
|
}
|
|
188
189
|
|
|
@@ -192,7 +193,7 @@ export const readyStories = (plan: StoryPlan, progress: StoryProgress): Readonly
|
|
|
192
193
|
(story) =>
|
|
193
194
|
!progress.done.has(story.id) &&
|
|
194
195
|
!progress.failed.has(story.id) &&
|
|
195
|
-
!progress.
|
|
196
|
+
!progress.waiting.has(story.id) &&
|
|
196
197
|
!progress.running.has(story.id) &&
|
|
197
198
|
story.dependsOn.every((dependency) => progress.done.has(dependency))
|
|
198
199
|
)
|