@trailstep/create-flows 0.3.1 → 0.3.3

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.
@@ -0,0 +1,2594 @@
1
+ // src/feature-implementation/story-router/step.ts
2
+ import { fail as fail3, state as state11, step as step10 } from "@trailstep/authoring";
3
+
4
+ // src/feature-implementation/implement-green/step.ts
5
+ import { state as state6, step as step5 } from "@trailstep/authoring";
6
+
7
+ // src/feature-implementation/shared/story-state.ts
8
+ import { state } from "@trailstep/authoring";
9
+
10
+ // src/feature-implementation/commit-reviewed-story/run-git.ts
11
+ import { execFile } from "child_process";
12
+ import { promisify } from "util";
13
+ var execFileAsync = promisify(execFile);
14
+ async function runGit(args, cwd) {
15
+ try {
16
+ const { stdout } = await execFileAsync("git", [...args], {
17
+ cwd,
18
+ maxBuffer: 1024 * 1024 * 10
19
+ });
20
+ return { ok: true, stdout: stdout.trimEnd() };
21
+ } catch (error) {
22
+ const message = error instanceof Error ? error.message : String(error);
23
+ return { ok: false, error: message };
24
+ }
25
+ }
26
+
27
+ // src/feature-implementation/shared/story-state.ts
28
+ var STORY_STATE_KEYS = {
29
+ activePhase: "activePhase",
30
+ activeStory: "activeStory",
31
+ activeStoryContext: "activeStoryContext",
32
+ activeStoryStartCommit: "activeStoryStartCommit",
33
+ attemptsByPhase: "attemptsByPhase",
34
+ completedStories: "completedStories",
35
+ blockedReason: "blockedReason",
36
+ latestExplorationBrief: "latestExplorationBrief",
37
+ latestImplementationSummary: "latestImplementationSummary",
38
+ latestPreflightStatus: "latestPreflightStatus",
39
+ planningCheckpointCount: "planningCheckpointCount",
40
+ skipNextPlanningCheckpoint: "skipNextPlanningCheckpoint",
41
+ latestRedTestSummary: "latestRedTestSummary",
42
+ latestReviewResult: "latestReviewResult",
43
+ latestStoryRouterState: "latestStoryRouterState",
44
+ latestValidationSummary: "latestValidationSummary",
45
+ storyBaseline: "storyBaseline",
46
+ storyContextQueue: "storyContextQueue",
47
+ storyQueue: "storyQueue",
48
+ workflowOptions: "workflowOptions",
49
+ workflowWarnings: "workflowWarnings"
50
+ };
51
+ async function loadStoryPhaseContext(phase) {
52
+ const context = await state.get(
53
+ STORY_STATE_KEYS.activeStoryContext
54
+ );
55
+ if (!context) {
56
+ return void 0;
57
+ }
58
+ if (typeof context === "string") {
59
+ return context.length > 0 ? context : void 0;
60
+ }
61
+ const scopedContext = context[phase];
62
+ return scopedContext && scopedContext.length > 0 ? scopedContext : void 0;
63
+ }
64
+ async function incrementStoryPhaseAttempt(phase) {
65
+ const attempts = await state.get(STORY_STATE_KEYS.attemptsByPhase) ?? {};
66
+ const nextAttempt = (attempts[phase] ?? 0) + 1;
67
+ await state.set(STORY_STATE_KEYS.attemptsByPhase, { ...attempts, [phase]: nextAttempt });
68
+ return nextAttempt;
69
+ }
70
+ async function resetActiveStoryStartCommit() {
71
+ await state.set(STORY_STATE_KEYS.activeStoryStartCommit, null);
72
+ }
73
+ async function resetStoryLocalState(options = {}) {
74
+ await resetStoryLocalStateFields(options);
75
+ }
76
+ async function resetStoryLocalStateForNextStory() {
77
+ await resetStoryLocalStateFields({
78
+ preserveActiveStoryStartCommit: false,
79
+ preserveLatestStoryRouterState: false,
80
+ preserveStoryBaseline: false
81
+ });
82
+ }
83
+ async function resetStoryLocalStateFields(options) {
84
+ await state.set(STORY_STATE_KEYS.attemptsByPhase, {});
85
+ if (!options.preserveStoryBaseline) {
86
+ await state.set(STORY_STATE_KEYS.storyBaseline, null);
87
+ }
88
+ if (!options.preserveActiveStoryStartCommit) {
89
+ await resetActiveStoryStartCommit();
90
+ }
91
+ await state.set(STORY_STATE_KEYS.latestPreflightStatus, null);
92
+ await state.set(STORY_STATE_KEYS.latestExplorationBrief, null);
93
+ await state.set(STORY_STATE_KEYS.latestRedTestSummary, null);
94
+ await state.set(STORY_STATE_KEYS.latestImplementationSummary, null);
95
+ await state.set(STORY_STATE_KEYS.latestValidationSummary, null);
96
+ await state.set(STORY_STATE_KEYS.latestReviewResult, null);
97
+ if (!options.preserveLatestStoryRouterState) {
98
+ await state.set(STORY_STATE_KEYS.latestStoryRouterState, null);
99
+ }
100
+ await state.set(STORY_STATE_KEYS.blockedReason, null);
101
+ }
102
+ async function loadStoryReviewGitContext() {
103
+ const baseline = await state.get(
104
+ STORY_STATE_KEYS.activeStoryStartCommit
105
+ );
106
+ const warnings = [];
107
+ if (baseline?.warning) {
108
+ warnings.push(baseline.warning);
109
+ }
110
+ const cwd = state.cwd;
111
+ if (!cwd) {
112
+ warnings.push(
113
+ "Workflow cwd is unavailable; reviewer must treat committed story-change context as missing."
114
+ );
115
+ return {
116
+ storyStartCommit: baseline?.commit,
117
+ changedFiles: [],
118
+ committedChangedFiles: [],
119
+ uncommittedChangedFiles: [],
120
+ warnings
121
+ };
122
+ }
123
+ const status = await runGit(["status", "--short"], cwd);
124
+ if (!status.ok) {
125
+ warnings.push(`Unable to inspect \`git status --short\`: ${status.error}`);
126
+ }
127
+ const uncommittedChangedFilesResult = await runGit(["diff", "--name-only"], cwd);
128
+ if (!uncommittedChangedFilesResult.ok) {
129
+ warnings.push(
130
+ `Unable to inspect uncommitted changed files with \`git diff --name-only\`: ${uncommittedChangedFilesResult.error}`
131
+ );
132
+ }
133
+ const uncommittedDiffStatResult = await runGit(["diff", "--stat"], cwd);
134
+ if (!uncommittedDiffStatResult.ok) {
135
+ warnings.push(
136
+ `Unable to inspect uncommitted diffstat with \`git diff --stat\`: ${uncommittedDiffStatResult.error}`
137
+ );
138
+ }
139
+ const storyStartCommit = baseline?.commit;
140
+ let committedChangedFiles = [];
141
+ let committedDiffStat;
142
+ if (!storyStartCommit) {
143
+ warnings.push(
144
+ "No durable story start commit is recorded; reviewer must not silently review only the current working tree diff."
145
+ );
146
+ } else {
147
+ const baselineReachable = await runGit(["cat-file", "-e", `${storyStartCommit}^{commit}`], cwd);
148
+ if (!baselineReachable.ok) {
149
+ warnings.push(
150
+ `Recorded story start commit \`${storyStartCommit}\` is invalid or unreachable: ${baselineReachable.error}`
151
+ );
152
+ } else {
153
+ const committedChangedFilesResult = await runGit(
154
+ ["diff", "--name-only", `${storyStartCommit}..HEAD`],
155
+ cwd
156
+ );
157
+ if (!committedChangedFilesResult.ok) {
158
+ warnings.push(
159
+ `Unable to inspect committed changed files with \`git diff --name-only ${storyStartCommit}..HEAD\`: ${committedChangedFilesResult.error}`
160
+ );
161
+ } else {
162
+ committedChangedFiles = splitGitLines(committedChangedFilesResult.stdout);
163
+ }
164
+ const committedDiffStatResult = await runGit(
165
+ ["diff", "--stat", `${storyStartCommit}..HEAD`],
166
+ cwd
167
+ );
168
+ if (!committedDiffStatResult.ok) {
169
+ warnings.push(
170
+ `Unable to inspect committed diffstat with \`git diff --stat ${storyStartCommit}..HEAD\`: ${committedDiffStatResult.error}`
171
+ );
172
+ } else {
173
+ committedDiffStat = committedDiffStatResult.stdout;
174
+ }
175
+ }
176
+ }
177
+ const uncommittedTrackedFiles = uncommittedChangedFilesResult.ok ? splitGitLines(uncommittedChangedFilesResult.stdout) : [];
178
+ const uncommittedStatusFiles = status.ok ? parseStatusChangedFiles(status.stdout) : [];
179
+ const uncommittedChangedFiles = uniqueFiles([
180
+ ...uncommittedTrackedFiles,
181
+ ...uncommittedStatusFiles
182
+ ]);
183
+ return {
184
+ storyStartCommit,
185
+ statusShort: status.ok ? status.stdout : void 0,
186
+ changedFiles: uniqueFiles([...committedChangedFiles, ...uncommittedChangedFiles]),
187
+ committedChangedFiles,
188
+ uncommittedChangedFiles,
189
+ committedDiffStat,
190
+ uncommittedDiffStat: uncommittedDiffStatResult.ok ? uncommittedDiffStatResult.stdout : void 0,
191
+ warnings
192
+ };
193
+ }
194
+ function splitGitLines(stdout) {
195
+ return stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
196
+ }
197
+ function parseStatusChangedFiles(stdout) {
198
+ return splitGitLines(stdout).flatMap((line) => {
199
+ const path = line.slice(3).trim();
200
+ if (path.length === 0) {
201
+ return [];
202
+ }
203
+ const renamedPath = path.split(" -> ").at(-1)?.trim();
204
+ return renamedPath ? [renamedPath] : [];
205
+ });
206
+ }
207
+ function uniqueFiles(files) {
208
+ return Array.from(new Set(files));
209
+ }
210
+
211
+ // src/feature-implementation/validate-story/step.ts
212
+ import { state as state5, step as step4 } from "@trailstep/authoring";
213
+
214
+ // src/feature-implementation/review-story-implementation/step.ts
215
+ import { state as state4, step as step3 } from "@trailstep/authoring";
216
+
217
+ // src/feature-implementation/commit-reviewed-story/step.ts
218
+ import { fail, state as state3, step as step2 } from "@trailstep/authoring";
219
+
220
+ // src/feature-implementation/open-pull-request/step.ts
221
+ import { execFile as execFile2 } from "child_process";
222
+ import { promisify as promisify2 } from "util";
223
+ import { done, state as state2, step } from "@trailstep/authoring";
224
+
225
+ // src/feature-implementation/shared/input-schema.ts
226
+ import { jsonSchema } from "@trailstep/authoring";
227
+ var takeItAwayInput = jsonSchema({
228
+ type: "object",
229
+ properties: {
230
+ conversation: { type: "string" },
231
+ autoCommit: { type: "boolean" },
232
+ pullRequest: {
233
+ type: "object",
234
+ properties: {
235
+ enabled: { type: "boolean" },
236
+ base: { type: "string" },
237
+ remote: { type: "string" },
238
+ draft: { type: "boolean" },
239
+ title: { type: "string" },
240
+ body: { type: "string" }
241
+ },
242
+ additionalProperties: false
243
+ },
244
+ planningCheckpoint: {
245
+ type: "object",
246
+ properties: {
247
+ enabled: { type: "boolean" }
248
+ },
249
+ additionalProperties: false
250
+ }
251
+ },
252
+ required: ["conversation"],
253
+ additionalProperties: false
254
+ });
255
+ function defaultTakeItAwayWorkflowOptions() {
256
+ return {
257
+ autoCommit: true,
258
+ pullRequest: {
259
+ enabled: true,
260
+ base: "main",
261
+ remote: "origin",
262
+ draft: false
263
+ },
264
+ planningCheckpoint: {
265
+ enabled: false
266
+ }
267
+ };
268
+ }
269
+ function normalizeTakeItAwayWorkflowOptions(input) {
270
+ const defaults = defaultTakeItAwayWorkflowOptions();
271
+ const title = nonEmptyOptional(input.pullRequest?.title);
272
+ const body = nonEmptyOptional(input.pullRequest?.body);
273
+ return {
274
+ autoCommit: input.autoCommit ?? defaults.autoCommit,
275
+ pullRequest: {
276
+ enabled: input.pullRequest?.enabled ?? defaults.pullRequest.enabled,
277
+ base: nonEmptyOrDefault(input.pullRequest?.base, defaults.pullRequest.base),
278
+ remote: nonEmptyOrDefault(input.pullRequest?.remote, defaults.pullRequest.remote),
279
+ draft: input.pullRequest?.draft ?? defaults.pullRequest.draft,
280
+ ...title === void 0 ? {} : { title },
281
+ ...body === void 0 ? {} : { body }
282
+ },
283
+ planningCheckpoint: {
284
+ enabled: input.planningCheckpoint?.enabled ?? defaults.planningCheckpoint.enabled
285
+ }
286
+ };
287
+ }
288
+ function nonEmptyOrDefault(value, fallback) {
289
+ const trimmed = value?.trim();
290
+ return trimmed && trimmed.length > 0 ? trimmed : fallback;
291
+ }
292
+ function nonEmptyOptional(value) {
293
+ const trimmed = value?.trim();
294
+ return trimmed && trimmed.length > 0 ? trimmed : void 0;
295
+ }
296
+
297
+ // src/feature-implementation/open-pull-request/step.ts
298
+ var execFileAsync2 = promisify2(execFile2);
299
+ var openPullRequestStep = step({ id: "open-pull-request" }).do(
300
+ async (output) => {
301
+ const pullRequest = await openPullRequest(output);
302
+ const warnings = await loadWorkflowWarnings();
303
+ const pullRequestWarning = pullRequest.warning;
304
+ const finalWarnings = pullRequestWarning === void 0 ? warnings : [...warnings, pullRequestWarning];
305
+ return done({
306
+ ...output,
307
+ ...finalWarnings.length === 0 ? {} : { warnings: finalWarnings },
308
+ pullRequest
309
+ });
310
+ }
311
+ );
312
+ async function openPullRequest(output) {
313
+ const options = await loadWorkflowOptions();
314
+ const prOptions = options.pullRequest;
315
+ if (!prOptions.enabled) {
316
+ return { status: "disabled" };
317
+ }
318
+ const cwd = state2.cwd;
319
+ if (!cwd) {
320
+ return skipped("Workflow cwd is unavailable, so TrailStep could not safely create a PR.", {
321
+ branch: "<branch>",
322
+ base: prOptions.base,
323
+ remote: prOptions.remote
324
+ });
325
+ }
326
+ const insideWorkTree = await runGit(["rev-parse", "--is-inside-work-tree"], cwd);
327
+ if (!insideWorkTree.ok || insideWorkTree.stdout !== "true") {
328
+ return skipped(
329
+ "The workflow cwd is not a git worktree, so TrailStep could not safely create a PR.",
330
+ { branch: "<branch>", base: prOptions.base, remote: prOptions.remote }
331
+ );
332
+ }
333
+ const branch = await currentBranch(cwd);
334
+ if (!branch.ok) {
335
+ return skipped(branch.warning, {
336
+ branch: "<branch>",
337
+ base: prOptions.base,
338
+ remote: prOptions.remote
339
+ });
340
+ }
341
+ const status = await runGit(["status", "--short"], cwd);
342
+ if (!status.ok) {
343
+ return skipped(
344
+ `TrailStep could not inspect \`git status --short\`, so it did not create a PR: ${status.error}`,
345
+ { branch: branch.name, base: prOptions.base, remote: prOptions.remote }
346
+ );
347
+ }
348
+ if (status.stdout.trim().length > 0) {
349
+ return skipped(
350
+ "The working tree is dirty, so TrailStep did not create a PR. Review and commit the remaining changes, then run the suggested commands.",
351
+ { branch: branch.name, base: prOptions.base, remote: prOptions.remote }
352
+ );
353
+ }
354
+ const remote = await runGit(["remote", "get-url", prOptions.remote], cwd);
355
+ if (!remote.ok) {
356
+ return skipped(
357
+ `Git remote \`${prOptions.remote}\` is not configured, so TrailStep could not push and create a PR: ${remote.error}`,
358
+ { branch: branch.name, base: prOptions.base, remote: prOptions.remote }
359
+ );
360
+ }
361
+ const existing = await existingPullRequest(cwd, branch.name);
362
+ if (existing.ok && existing.url) {
363
+ return { status: "existing", url: existing.url };
364
+ }
365
+ const push = await runGit(["push", "--set-upstream", prOptions.remote, branch.name], cwd);
366
+ if (!push.ok) {
367
+ return skipped(`TrailStep could not push \`${branch.name}\`: ${push.error}`, {
368
+ branch: branch.name,
369
+ base: prOptions.base,
370
+ remote: prOptions.remote
371
+ });
372
+ }
373
+ const title = prOptions.title ?? await defaultPullRequestTitle(output);
374
+ const body = prOptions.body ?? defaultPullRequestBody(output);
375
+ const createArgs = [
376
+ "pr",
377
+ "create",
378
+ "--base",
379
+ prOptions.base,
380
+ "--head",
381
+ branch.name,
382
+ "--title",
383
+ title,
384
+ "--body",
385
+ body
386
+ ];
387
+ if (prOptions.draft) {
388
+ createArgs.push("--draft");
389
+ }
390
+ const created = await runGh(createArgs, cwd);
391
+ if (!created.ok) {
392
+ return skipped(
393
+ `TrailStep pushed \`${branch.name}\` but could not create a PR: ${created.error}`,
394
+ {
395
+ branch: branch.name,
396
+ base: prOptions.base,
397
+ remote: prOptions.remote
398
+ }
399
+ );
400
+ }
401
+ return { status: "created", url: extractUrl(created.stdout) };
402
+ }
403
+ async function loadWorkflowOptions() {
404
+ return await state2.get(STORY_STATE_KEYS.workflowOptions) ?? defaultTakeItAwayWorkflowOptions();
405
+ }
406
+ async function loadWorkflowWarnings() {
407
+ return await state2.get(STORY_STATE_KEYS.workflowWarnings) ?? [];
408
+ }
409
+ async function currentBranch(cwd) {
410
+ const branch = await runGit(["branch", "--show-current"], cwd);
411
+ if (!branch.ok || branch.stdout.trim().length === 0) {
412
+ return {
413
+ ok: false,
414
+ warning: branch.ok ? "Git is in a detached HEAD state, so TrailStep could not safely create a PR." : `TrailStep could not determine the current branch: ${branch.error}`
415
+ };
416
+ }
417
+ return { ok: true, name: branch.stdout.trim() };
418
+ }
419
+ async function existingPullRequest(cwd, branch) {
420
+ const result = await runGh(
421
+ ["pr", "list", "--head", branch, "--state", "open", "--json", "url", "--jq", '.[0].url // ""'],
422
+ cwd
423
+ );
424
+ if (!result.ok) {
425
+ return { ok: false };
426
+ }
427
+ const url = extractUrl(result.stdout);
428
+ return url ? { ok: true, url } : { ok: true };
429
+ }
430
+ async function defaultPullRequestTitle(output) {
431
+ const featureDoc = await state2.get("featureDoc");
432
+ const featureTitle = extractHeading(featureDoc?.content ?? "");
433
+ if (featureTitle) {
434
+ return truncateTitle(featureTitle);
435
+ }
436
+ return truncateTitle(output.completedStories[0] ?? "TrailStep implementation");
437
+ }
438
+ function defaultPullRequestBody(output) {
439
+ return [
440
+ output.summary,
441
+ "",
442
+ "## TrailStep artifacts",
443
+ `- Feature doc: ${output.featureDocPath}`,
444
+ `- Implementation doc: ${output.implementationDocPath}`,
445
+ "",
446
+ "## Completed stories",
447
+ ...output.completedStories.map((story) => `- ${story}`)
448
+ ].join("\n");
449
+ }
450
+ function skipped(warning, options) {
451
+ return {
452
+ status: "skipped",
453
+ warning,
454
+ commands: suggestedCommands(options)
455
+ };
456
+ }
457
+ function suggestedCommands({
458
+ branch,
459
+ base,
460
+ remote
461
+ }) {
462
+ return [
463
+ "git status --short",
464
+ 'git add -A && git commit -m "trailstep: finalize reviewed changes"',
465
+ `git push --set-upstream ${shellQuote(remote)} ${shellQuote(branch)}`,
466
+ `gh pr create --fill --base ${shellQuote(base)} --head ${shellQuote(branch)}`
467
+ ];
468
+ }
469
+ async function runGh(args, cwd) {
470
+ try {
471
+ const { stdout } = await execFileAsync2("gh", [...args], {
472
+ cwd,
473
+ maxBuffer: 1024 * 1024 * 10
474
+ });
475
+ return { ok: true, stdout: stdout.trimEnd() };
476
+ } catch (error) {
477
+ const message = error instanceof Error ? error.message : String(error);
478
+ return { ok: false, error: message };
479
+ }
480
+ }
481
+ function extractHeading(content) {
482
+ return /^#{1,6}\s+(.+)$/m.exec(content)?.[1]?.trim();
483
+ }
484
+ function truncateTitle(title) {
485
+ return title.length <= 72 ? title : `${title.slice(0, 69)}...`;
486
+ }
487
+ function extractUrl(stdout) {
488
+ return /https?:\/\/\S+/u.exec(stdout)?.[0];
489
+ }
490
+ function shellQuote(value) {
491
+ if (/^[A-Za-z0-9_./:@-]+$/u.test(value)) {
492
+ return value;
493
+ }
494
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
495
+ }
496
+
497
+ // src/feature-implementation/shared/output-schema.ts
498
+ import { jsonSchema as jsonSchema2 } from "@trailstep/authoring";
499
+ var takeItAwayOutput = jsonSchema2({
500
+ type: "object",
501
+ properties: {
502
+ status: { type: "string", const: "implemented" },
503
+ featureDocPath: { type: "string" },
504
+ implementationDocPath: { type: "string" },
505
+ storyCount: { type: "number" },
506
+ completedStories: { type: "array", items: { type: "string" } },
507
+ summary: { type: "string" },
508
+ warnings: { type: "array", items: { type: "string" } },
509
+ pullRequest: {
510
+ type: "object",
511
+ properties: {
512
+ status: { type: "string", enum: ["created", "existing", "skipped", "disabled"] },
513
+ url: { type: "string" },
514
+ warning: { type: "string" },
515
+ commands: { type: "array", items: { type: "string" } }
516
+ },
517
+ required: ["status"],
518
+ additionalProperties: false
519
+ }
520
+ },
521
+ required: [
522
+ "status",
523
+ "featureDocPath",
524
+ "implementationDocPath",
525
+ "storyCount",
526
+ "completedStories",
527
+ "summary"
528
+ ],
529
+ additionalProperties: false
530
+ });
531
+ function extractStoryTitle(storyContent, fallbackIndex) {
532
+ const heading = /^#{1,6}\s+(.+)$/m.exec(storyContent);
533
+ return heading?.[1]?.trim() || `Story ${fallbackIndex}`;
534
+ }
535
+
536
+ // src/feature-implementation/commit-reviewed-story/step.ts
537
+ var commitReviewedStoryStep = step2({ id: "commit-reviewed-story" }).do(
538
+ async (input) => {
539
+ const activeStory = input.currentStory;
540
+ if (!state3.isReplayingCompletedStep && await storyAutoCommitEnabled()) {
541
+ const commitResult = await commitReviewedStoryChanges(
542
+ activeStory,
543
+ input.implementationSummary
544
+ );
545
+ if (commitResult.ok) {
546
+ if (commitResult.warning) {
547
+ await appendWorkflowWarning(commitResult.warning);
548
+ }
549
+ } else {
550
+ const storyQueue = await state3.get(STORY_STATE_KEYS.storyQueue) ?? [];
551
+ if (storyQueue.length > 0) {
552
+ return fail({
553
+ code: commitResult.code,
554
+ message: commitResult.message,
555
+ details: commitResult.details
556
+ });
557
+ }
558
+ await appendWorkflowWarning(commitResult.message);
559
+ }
560
+ }
561
+ return completeReviewedStory(activeStory);
562
+ }
563
+ );
564
+ async function storyAutoCommitEnabled() {
565
+ const options = await state3.get(STORY_STATE_KEYS.workflowOptions) ?? defaultTakeItAwayWorkflowOptions();
566
+ return options.autoCommit;
567
+ }
568
+ async function commitReviewedStoryChanges(activeStory, implementationSummary) {
569
+ const cwd = state3.cwd;
570
+ if (!cwd) {
571
+ return {
572
+ ok: false,
573
+ code: "story_commit_missing_cwd",
574
+ message: "Cannot commit reviewed story because the TrailStep workflow cwd is unavailable.",
575
+ details: { storyPath: activeStory.path }
576
+ };
577
+ }
578
+ const insideWorkTree = await runGit(["rev-parse", "--is-inside-work-tree"], cwd);
579
+ if (!insideWorkTree.ok || insideWorkTree.stdout !== "true") {
580
+ return {
581
+ ok: false,
582
+ code: "story_commit_not_git_worktree",
583
+ message: "Cannot commit reviewed story because the workflow is not running inside a git worktree.",
584
+ details: {
585
+ storyPath: activeStory.path,
586
+ gitError: insideWorkTree.ok ? void 0 : insideWorkTree.error
587
+ }
588
+ };
589
+ }
590
+ const add = await runGit(["add", "-A"], cwd);
591
+ if (!add.ok) {
592
+ return {
593
+ ok: false,
594
+ code: "story_commit_stage_failed",
595
+ message: "Cannot commit reviewed story because `git add -A` failed.",
596
+ details: { storyPath: activeStory.path, gitError: add.error }
597
+ };
598
+ }
599
+ const staged = await runGit(["diff", "--cached", "--name-only"], cwd);
600
+ if (!staged.ok) {
601
+ return {
602
+ ok: false,
603
+ code: "story_commit_status_failed",
604
+ message: "Cannot commit reviewed story because staged changes could not be inspected.",
605
+ details: { storyPath: activeStory.path, gitError: staged.error }
606
+ };
607
+ }
608
+ const storyTitle = extractStoryTitle(activeStory.content, 1);
609
+ if (staged.stdout.trim().length === 0) {
610
+ const baseline = await state3.get(
611
+ STORY_STATE_KEYS.activeStoryStartCommit
612
+ );
613
+ const head = await runGit(["rev-parse", "HEAD"], cwd);
614
+ if (baseline?.commit && head.ok && head.stdout !== baseline.commit) {
615
+ return { ok: true };
616
+ }
617
+ const headSubject = await runGit(["log", "-1", "--format=%s"], cwd);
618
+ if (headSubject.ok && headSubject.stdout === truncateCommitSubject(`trailstep: ${storyTitle}`)) {
619
+ return { ok: true };
620
+ }
621
+ return {
622
+ ok: true,
623
+ warning: "The story passed review, but there were no staged or already-committed changes since the story baseline to commit."
624
+ };
625
+ }
626
+ const commit = await runGit(
627
+ [
628
+ "commit",
629
+ "-m",
630
+ truncateCommitSubject(`trailstep: ${storyTitle}`),
631
+ "-m",
632
+ commitBody(activeStory, implementationSummary, staged.stdout)
633
+ ],
634
+ cwd
635
+ );
636
+ if (!commit.ok) {
637
+ return {
638
+ ok: false,
639
+ code: "story_commit_failed",
640
+ message: "The story passed review, but TrailStep could not create the story commit.",
641
+ details: { storyPath: activeStory.path, gitError: commit.error }
642
+ };
643
+ }
644
+ return { ok: true };
645
+ }
646
+ async function completeReviewedStory(activeStory) {
647
+ const completed = await state3.get(STORY_STATE_KEYS.completedStories) ?? [];
648
+ const completedTitle = extractStoryTitle(activeStory.content, completed.length + 1);
649
+ const storyAlreadyCompleted = completed.includes(completedTitle);
650
+ const updatedCompleted = storyAlreadyCompleted ? completed : [...completed, completedTitle];
651
+ const persistedActiveStory = await state3.get(STORY_STATE_KEYS.activeStory);
652
+ const storyQueue = await state3.get(STORY_STATE_KEYS.storyQueue) ?? [];
653
+ const storyContextQueue = await state3.get(STORY_STATE_KEYS.storyContextQueue) ?? [];
654
+ const queuedStories = storyQueue.map((story, index) => ({ story, context: storyContextQueue[index] ?? "" })).filter(({ story }) => !documentsMatch(story, activeStory));
655
+ const persistedStoryAlreadyAdvanced = persistedActiveStory && !documentsMatch(persistedActiveStory, activeStory);
656
+ const persistedQueuedIndex = persistedStoryAlreadyAdvanced ? queuedStories.findIndex(({ story }) => documentsMatch(story, persistedActiveStory)) : -1;
657
+ const activeStoryContext = persistedStoryAlreadyAdvanced && persistedQueuedIndex < 0 ? await state3.get(
658
+ STORY_STATE_KEYS.activeStoryContext
659
+ ) ?? "" : void 0;
660
+ const nextStory = persistedStoryAlreadyAdvanced ? persistedActiveStory : queuedStories[0]?.story;
661
+ const nextStoryContext = persistedStoryAlreadyAdvanced ? persistedQueuedIndex >= 0 ? queuedStories[persistedQueuedIndex]?.context ?? "" : activeStoryContext : queuedStories[0]?.context ?? "";
662
+ const remainingStoryEntries = persistedStoryAlreadyAdvanced ? queuedStories.filter(({ story }) => !documentsMatch(story, persistedActiveStory)) : queuedStories.slice(1);
663
+ if (!nextStory) {
664
+ await state3.set(STORY_STATE_KEYS.completedStories, updatedCompleted);
665
+ await state3.set(STORY_STATE_KEYS.activeStory, null);
666
+ await state3.set(STORY_STATE_KEYS.activeStoryContext, null);
667
+ await state3.set(STORY_STATE_KEYS.storyContextQueue, []);
668
+ await resetActiveStoryStartCommit();
669
+ const featureDoc = await state3.get("featureDoc");
670
+ const implementationDoc = await state3.get("implementationDoc");
671
+ const output = {
672
+ status: "implemented",
673
+ featureDocPath: featureDoc?.path ?? "",
674
+ implementationDocPath: implementationDoc?.path ?? "",
675
+ storyCount: updatedCompleted.length,
676
+ completedStories: updatedCompleted,
677
+ summary: `Implemented and reviewed ${updatedCompleted.length} ${updatedCompleted.length === 1 ? "story" : "stories"}.`
678
+ };
679
+ return openPullRequestStep(output);
680
+ }
681
+ if (!state3.isReplayingCompletedStep && !await storyAutoCommitEnabled()) {
682
+ const cleanBoundary = await verifyCleanBoundaryBeforeNextStory(activeStory);
683
+ if (!cleanBoundary.ok) {
684
+ return fail({
685
+ code: cleanBoundary.code,
686
+ message: cleanBoundary.message,
687
+ details: cleanBoundary.details
688
+ });
689
+ }
690
+ }
691
+ await resetStoryLocalStateForNextStory();
692
+ await state3.set(STORY_STATE_KEYS.completedStories, updatedCompleted);
693
+ await state3.set(
694
+ STORY_STATE_KEYS.storyQueue,
695
+ remainingStoryEntries.map(({ story }) => story)
696
+ );
697
+ await state3.set(
698
+ STORY_STATE_KEYS.storyContextQueue,
699
+ remainingStoryEntries.map(({ context }) => context)
700
+ );
701
+ await state3.set(STORY_STATE_KEYS.activeStory, nextStory);
702
+ await state3.set(STORY_STATE_KEYS.activeStoryContext, nextStoryContext);
703
+ const { storyRouterStep: storyRouterStep2 } = await import("./step-ZAE3Y7FI.js");
704
+ return storyRouterStep2({ reason: "story-completed", currentStory: nextStory });
705
+ }
706
+ function documentsMatch(left, right) {
707
+ return left.path === right.path && left.content === right.content;
708
+ }
709
+ async function appendWorkflowWarning(warning) {
710
+ const warnings = await state3.get(STORY_STATE_KEYS.workflowWarnings) ?? [];
711
+ await state3.set(STORY_STATE_KEYS.workflowWarnings, [...warnings, warning]);
712
+ }
713
+ async function verifyCleanBoundaryBeforeNextStory(activeStory) {
714
+ const cwd = state3.cwd;
715
+ if (!cwd) {
716
+ return {
717
+ ok: false,
718
+ code: "story_boundary_missing_cwd",
719
+ message: "Cannot advance to the next story because the workflow cwd is unavailable. Commit or restore a clean story boundary from a valid checkout, then retry.",
720
+ details: { storyPath: activeStory.path }
721
+ };
722
+ }
723
+ const status = await runGit(["status", "--short"], cwd);
724
+ if (!status.ok) {
725
+ return {
726
+ ok: false,
727
+ code: "story_boundary_status_failed",
728
+ message: "Cannot advance to the next story because TrailStep could not inspect `git status --short`. Fix git status inspection, commit or restore a clean story boundary, then retry.",
729
+ details: { storyPath: activeStory.path, cwd, gitError: status.error }
730
+ };
731
+ }
732
+ if (status.stdout.trim().length > 0) {
733
+ return {
734
+ ok: false,
735
+ code: "story_boundary_dirty_without_auto_commit",
736
+ message: "Cannot advance to the next story because auto-commit is disabled and the reviewed story left uncommitted changes. Commit, stash, or otherwise restore a clean story boundary, then retry the workflow.",
737
+ details: { storyPath: activeStory.path, cwd, statusShort: status.stdout }
738
+ };
739
+ }
740
+ return { ok: true };
741
+ }
742
+ function truncateCommitSubject(subject) {
743
+ return subject.length <= 72 ? subject : `${subject.slice(0, 69)}...`;
744
+ }
745
+ function commitBody(activeStory, implementationSummary, stagedFiles) {
746
+ return [
747
+ "Committed automatically by TrailStep after a passing story review.",
748
+ "",
749
+ `Story artifact: ${activeStory.path}`,
750
+ "",
751
+ "Implementer summary:",
752
+ implementationSummary?.trim() || "Not provided.",
753
+ "",
754
+ "Staged files:",
755
+ stagedFiles.split("\n").filter((file) => file.length > 0).map((file) => `- ${file}`).join("\n")
756
+ ].join("\n");
757
+ }
758
+
759
+ // src/feature-implementation/shared/review-schema.ts
760
+ import { jsonSchema as jsonSchema3 } from "@trailstep/authoring";
761
+
762
+ // src/feature-implementation/shared/constants.ts
763
+ var STORY_BOUNDARY = "<!-- trailstep-story-boundary -->";
764
+ var STORY_CONTEXT_START = "<context>";
765
+ var STORY_CONTEXT_END = "</context>";
766
+ var REVIEW_PASS_THRESHOLD = 4;
767
+ var MAX_IMPLEMENTATION_DOC_REVIEW_ATTEMPTS = 5;
768
+ var MAX_STORY_REVIEW_ATTEMPTS = 3;
769
+ var MAX_STORY_VALIDATION_ATTEMPTS = 3;
770
+ var STORY_DOCTOR_VALIDATION_FAILURE_THRESHOLD = 2;
771
+
772
+ // src/feature-implementation/shared/review-schema.ts
773
+ var RATING_SCHEMA = { type: "integer", minimum: 1, maximum: 5 };
774
+ var reviewOutput = jsonSchema3({
775
+ type: "object",
776
+ properties: {
777
+ score: RATING_SCHEMA,
778
+ summary: { type: "string" },
779
+ methodologyRatings: {
780
+ type: "object",
781
+ properties: {
782
+ tdd: RATING_SCHEMA,
783
+ verticalSlicing: RATING_SCHEMA,
784
+ tracerBullet: RATING_SCHEMA,
785
+ dependencies: RATING_SCHEMA,
786
+ architecture: RATING_SCHEMA
787
+ },
788
+ required: ["tdd", "verticalSlicing", "tracerBullet", "dependencies", "architecture"],
789
+ additionalProperties: false
790
+ },
791
+ requiredImprovements: { type: "array", items: { type: "string" } }
792
+ },
793
+ required: ["score", "summary", "methodologyRatings", "requiredImprovements"],
794
+ additionalProperties: false
795
+ });
796
+ function reviewPasses(review) {
797
+ return review.score >= REVIEW_PASS_THRESHOLD;
798
+ }
799
+
800
+ // src/feature-implementation/review-story-implementation/prompt.ts
801
+ import { promptSections, section } from "@trailstep/authoring";
802
+
803
+ // src/feature-implementation/shared/story-view.ts
804
+ var STORY_SECTION_TITLES = [
805
+ "Dependencies",
806
+ "Goal",
807
+ "User-Visible / Integration-Visible Slice",
808
+ "Acceptance Criteria",
809
+ "Red Phase",
810
+ "Green Phase",
811
+ "Refactor Phase",
812
+ "Validation Commands",
813
+ "Notes for Implementer"
814
+ ];
815
+ var KNOWN_SECTION_TITLES = new Set(STORY_SECTION_TITLES.map(normalizeHeading));
816
+ function storyViewForTestWriter(storyContent) {
817
+ return storyView(storyContent, [
818
+ "Dependencies",
819
+ "Goal",
820
+ "User-Visible / Integration-Visible Slice",
821
+ "Acceptance Criteria",
822
+ "Red Phase",
823
+ "Validation Commands"
824
+ ]);
825
+ }
826
+ function storyViewForImplementer(storyContent) {
827
+ return storyView(storyContent, [
828
+ "Dependencies",
829
+ "Goal",
830
+ "User-Visible / Integration-Visible Slice",
831
+ "Acceptance Criteria",
832
+ "Green Phase",
833
+ "Refactor Phase",
834
+ "Notes for Implementer"
835
+ ]);
836
+ }
837
+ function storyViewForReviewer(storyContent) {
838
+ return storyView(storyContent, [
839
+ "Dependencies",
840
+ "Goal",
841
+ "User-Visible / Integration-Visible Slice",
842
+ "Acceptance Criteria",
843
+ "Red Phase",
844
+ "Green Phase",
845
+ "Refactor Phase",
846
+ "Validation Commands",
847
+ "Notes for Implementer"
848
+ ]);
849
+ }
850
+ function storyView(storyContent, visibleSections) {
851
+ const parsed = parseStorySections(storyContent);
852
+ const selected = visibleSections.flatMap((title) => {
853
+ const section5 = parsed.sections.get(normalizeHeading(title));
854
+ return section5 ? [section5] : [];
855
+ });
856
+ return [parsed.preamble, ...selected].map((part) => part.trim()).filter((part) => part.length > 0).join("\n\n");
857
+ }
858
+ function parseStorySections(storyContent) {
859
+ const normalized = storyContent.replace(/\r\n?/g, "\n");
860
+ const lines = normalized.split("\n");
861
+ const sections = /* @__PURE__ */ new Map();
862
+ const preambleLines = [];
863
+ let activeSectionTitle;
864
+ let activeSectionLines = [];
865
+ for (const line of lines) {
866
+ const heading = parseKnownSectionHeading(line);
867
+ if (heading) {
868
+ flushActiveSection();
869
+ activeSectionTitle = heading.normalizedTitle;
870
+ activeSectionLines = [line];
871
+ continue;
872
+ }
873
+ if (activeSectionTitle) {
874
+ activeSectionLines.push(line);
875
+ } else {
876
+ preambleLines.push(line);
877
+ }
878
+ }
879
+ flushActiveSection();
880
+ return {
881
+ preamble: preambleLines.join("\n"),
882
+ sections
883
+ };
884
+ function flushActiveSection() {
885
+ if (!activeSectionTitle) {
886
+ return;
887
+ }
888
+ sections.set(activeSectionTitle, activeSectionLines.join("\n"));
889
+ activeSectionTitle = void 0;
890
+ activeSectionLines = [];
891
+ }
892
+ }
893
+ function parseKnownSectionHeading(line) {
894
+ const match = /^(#{2,6})\s+(.+?)\s*$/u.exec(line.trim());
895
+ if (!match) {
896
+ return void 0;
897
+ }
898
+ const normalizedTitle = normalizeHeading(match[2] ?? "");
899
+ return KNOWN_SECTION_TITLES.has(normalizedTitle) ? { normalizedTitle } : void 0;
900
+ }
901
+ function normalizeHeading(value) {
902
+ return value.trim().replace(/\s+/g, " ").toLowerCase();
903
+ }
904
+
905
+ // src/feature-implementation/review-story-implementation/prompt.ts
906
+ var REVIEW_RUBRIC = [
907
+ "Score 4-5 only when the active story is complete, well-tested, scoped, and integrated with local conventions.",
908
+ "Score below 4 for missing acceptance criteria, weak/missing behavioral-red evidence, unsafe diff isolation, broken validation, broad unrelated changes, or poor integration.",
909
+ "Scores below 4 must include specific requiredImprovements for the next retry."
910
+ ].join("\n");
911
+ function reviewStoryImplementationPrompt({
912
+ input
913
+ }) {
914
+ const baseline = input.gitContext.storyStartCommit ?? "MISSING";
915
+ const committedDiffCommand = input.gitContext.storyStartCommit ? `git diff ${input.gitContext.storyStartCommit}..HEAD` : "git diff <missing-story-start-commit>..HEAD";
916
+ return promptSections(
917
+ section(
918
+ "Role",
919
+ "You are the story reviewer: inspect only the isolated active-story change set. This is a read-only review; do not edit, do not stage, do not commit, do not clean, and do not run tests."
920
+ ),
921
+ section("Review rubric", REVIEW_RUBRIC),
922
+ section("Story review view", storyViewForReviewer(input.currentStory.content)),
923
+ section("Implementation context", input.implementationContext),
924
+ section("Exploration summary", input.explorationSummary ?? "Not provided."),
925
+ section("Red-test summary", input.redTestSummary ?? "Not provided."),
926
+ section("Red-test evidence", input.redEvidence ?? "Not provided."),
927
+ section("Implementer summary", input.implementationSummary ?? "Not provided."),
928
+ section("Validation summary", input.validationSummary ?? "Not provided."),
929
+ section("Validation commands/results", formatCommandResults(input.validationCommands ?? [])),
930
+ section(
931
+ "Story review git metadata",
932
+ [
933
+ `Recorded story start commit: ${baseline}`,
934
+ "",
935
+ "Changed files:",
936
+ formatList(input.gitContext.changedFiles),
937
+ "",
938
+ "Committed changed files:",
939
+ formatList(input.gitContext.committedChangedFiles),
940
+ "",
941
+ "Uncommitted changed files:",
942
+ formatList(input.gitContext.uncommittedChangedFiles),
943
+ "",
944
+ "Committed diffstat (`git diff --stat <storyStartCommit>..HEAD`):",
945
+ input.gitContext.committedDiffStat || "Not available.",
946
+ "",
947
+ "Uncommitted diffstat (`git diff --stat`):",
948
+ input.gitContext.uncommittedDiffStat || "Not available.",
949
+ "",
950
+ "Working tree status (`git status --short`):",
951
+ input.gitContext.statusShort || "Not available.",
952
+ input.gitContext.warnings.length > 0 ? [
953
+ "",
954
+ "Warnings / blocked context:",
955
+ ...input.gitContext.warnings.map((warning) => `- ${warning}`)
956
+ ].join("\n") : ""
957
+ ].filter((line) => line.length > 0).join("\n")
958
+ ),
959
+ section(
960
+ "Read-only local inspection commands",
961
+ [
962
+ "Run only read-only inspection commands if local verification is needed; inspect only, and do not edit, do not stage, do not commit, do not clean, and do not run tests:",
963
+ "- `git status --short`",
964
+ `- \`${committedDiffCommand}\``,
965
+ "- `git diff`",
966
+ "Use the captured validation commands/results above; do not run tests yourself."
967
+ ].join("\n")
968
+ ),
969
+ section(
970
+ "Task",
971
+ "Review only the active story slice represented by the recorded story start commit through HEAD, plus the current uncommitted working tree changes, against the story above and the phase summaries. Use the metadata above first; if you need to inspect locally, use read-only commands only (`git status --short`, the listed committed `git diff <storyStartCommit>..HEAD`, and `git diff`) \u2014 do not run tests, do not edit code, do not stage files, do not commit files, do not revert files, do not clean files, and do not try to isolate the diff by changing the repository. If unrelated dirty files, a missing/invalid baseline, or ambiguous context prevents a trustworthy review, say so in the structured review and require the implementer/workflow to fix the isolation; never remove changes yourself. Respond only with the structured review."
972
+ )
973
+ );
974
+ }
975
+ function formatList(items) {
976
+ if (items.length === 0) {
977
+ return "- None";
978
+ }
979
+ return items.map((item) => `- ${item}`).join("\n");
980
+ }
981
+ function formatCommandResults(commands) {
982
+ if (commands.length === 0) {
983
+ return "- Not provided.";
984
+ }
985
+ return commands.map((item) => `- \`${item.command}\`: ${item.result}`).join("\n");
986
+ }
987
+
988
+ // src/feature-implementation/review-story-implementation/step.ts
989
+ var reviewStoryImplementationStep = step3({ id: "review-story-implementation" }).prompt(reviewStoryImplementationPrompt, {
990
+ agent: "storyReviewer",
991
+ output: reviewOutput
992
+ }).do(async (review, input) => {
993
+ await state4.set(STORY_STATE_KEYS.latestReviewResult, review);
994
+ if (!reviewPasses(review)) {
995
+ const { storyRouterStep: storyRouterStep2 } = await import("./step-ZAE3Y7FI.js");
996
+ return storyRouterStep2({ reason: "failed-review", currentStory: input.currentStory });
997
+ }
998
+ return commitReviewedStoryStep({
999
+ currentStory: input.currentStory,
1000
+ implementationSummary: input.implementationSummary
1001
+ });
1002
+ });
1003
+
1004
+ // src/feature-implementation/validate-story/validation-runner.ts
1005
+ import { execFile as execFile3 } from "child_process";
1006
+ import { promisify as promisify3 } from "util";
1007
+ var execFileAsync3 = promisify3(execFile3);
1008
+ var MAX_COMMANDS = 5;
1009
+ var COMMAND_TIMEOUT_MS = 12e4;
1010
+ var OUTPUT_LIMIT = 4e3;
1011
+ var MAX_BUFFER = 256 * 1024;
1012
+ var ALLOWED_COMMANDS = /* @__PURE__ */ new Set(["pnpm", "npm", "node", "git", "./trailstep-dev.sh"]);
1013
+ var BROAD_PACKAGE_MANAGER_SCRIPTS = /* @__PURE__ */ new Set(["test", "typecheck", "lint", "build"]);
1014
+ var SHELL_META_CHARS = /* @__PURE__ */ new Set(["|", "&", ";", "<", ">", "(", ")"]);
1015
+ async function runFocusedStoryValidation(input) {
1016
+ const commands = collectValidationCommands(input.validationInput, input.implementationContext);
1017
+ if (commands.length === 0) {
1018
+ return {
1019
+ blocked: true,
1020
+ blockedReason: "No focused validation commands were found in the story or exploration context.",
1021
+ summary: "Validation blocked: no focused validation commands were found.",
1022
+ commands: [],
1023
+ validationPassed: false
1024
+ };
1025
+ }
1026
+ const results = [];
1027
+ for (const command of commands.slice(0, MAX_COMMANDS)) {
1028
+ results.push(await runValidationCommand(command, input.cwd));
1029
+ }
1030
+ const skipped2 = results.filter((result) => isSkippedResult(result.result));
1031
+ const failed = results.filter((result) => isFailedResult(result.result));
1032
+ const passed = results.filter((result) => isPassedResult(result.result));
1033
+ const validationPassed = failed.length === 0 && passed.length > 0;
1034
+ const blocked = passed.length === 0 && skipped2.length === results.length;
1035
+ return {
1036
+ blocked,
1037
+ blockedReason: blocked ? "All focused validation commands were rejected or skipped." : void 0,
1038
+ summary: summarizeValidationResults({ passed, failed, skipped: skipped2, total: results.length }),
1039
+ commands: results,
1040
+ validationPassed
1041
+ };
1042
+ }
1043
+ function collectValidationCommands(input, implementationContext) {
1044
+ return uniqueCommands([
1045
+ ...extractValidationCommandLines(input.currentStory.content),
1046
+ ...extractValidationCommandLines(implementationContext ?? ""),
1047
+ ...input.explorationBrief?.recommendedValidationCommands ?? []
1048
+ ]);
1049
+ }
1050
+ async function runValidationCommand(command, cwd) {
1051
+ const parsed = parseCommand(command);
1052
+ if (parsed.ok === false) {
1053
+ return { command, result: `skipped: ${parsed.reason}` };
1054
+ }
1055
+ const allowed = validateAllowedCommand(parsed.argv);
1056
+ if (allowed.ok === false) {
1057
+ return { command, result: `skipped: ${allowed.reason}` };
1058
+ }
1059
+ if (!cwd) {
1060
+ return { command, result: "skipped: workflow cwd is unavailable" };
1061
+ }
1062
+ try {
1063
+ const executable = executableForPlatform(parsed.argv[0] ?? "");
1064
+ const { stdout, stderr } = await execFileAsync3(executable, parsed.argv.slice(1), {
1065
+ cwd,
1066
+ timeout: COMMAND_TIMEOUT_MS,
1067
+ maxBuffer: MAX_BUFFER,
1068
+ shell: requiresWindowsCommandShell(executable),
1069
+ windowsHide: true
1070
+ });
1071
+ const output = formatCommandOutput(stdout, stderr);
1072
+ return { command, result: `passed${output ? `: ${output}` : ""}` };
1073
+ } catch (error) {
1074
+ return { command, result: `failed: ${formatExecutionError(error)}` };
1075
+ }
1076
+ }
1077
+ function executableForPlatform(executable) {
1078
+ if (process.platform !== "win32") {
1079
+ return executable;
1080
+ }
1081
+ if (executable === "pnpm" || executable === "npm") {
1082
+ return `${executable}.cmd`;
1083
+ }
1084
+ return executable;
1085
+ }
1086
+ function requiresWindowsCommandShell(executable) {
1087
+ return process.platform === "win32" && executable.toLowerCase().endsWith(".cmd");
1088
+ }
1089
+ function extractValidationCommandLines(content) {
1090
+ const normalized = content.replace(/\r\n?/g, "\n");
1091
+ const lines = normalized.split("\n");
1092
+ const commands = [];
1093
+ let inValidationSection = false;
1094
+ for (const line of lines) {
1095
+ if (/^#{2,6}\s+Validation Commands\s*$/iu.test(line.trim())) {
1096
+ inValidationSection = true;
1097
+ continue;
1098
+ }
1099
+ if (inValidationSection && /^#{2,6}\s+/u.test(line.trim())) {
1100
+ break;
1101
+ }
1102
+ if (!inValidationSection) {
1103
+ continue;
1104
+ }
1105
+ const command = normalizeCommandLine(line);
1106
+ if (command) {
1107
+ commands.push(command);
1108
+ }
1109
+ }
1110
+ return commands;
1111
+ }
1112
+ function normalizeCommandLine(line) {
1113
+ const trimmed = line.trim();
1114
+ if (!trimmed || trimmed === "Not provided.") {
1115
+ return null;
1116
+ }
1117
+ const withoutFence = trimmed.replace(/^```(?:sh|bash|shell)?\s*/iu, "").replace(/```$/u, "");
1118
+ const listMatch = /^(?:[-*+]\s+|\d+[.)]\s+)?(?:`([^`]+)`|(.+))$/u.exec(withoutFence.trim());
1119
+ const command = (listMatch?.[1] ?? listMatch?.[2] ?? withoutFence).trim();
1120
+ return command.length > 0 ? command : null;
1121
+ }
1122
+ function parseCommand(command) {
1123
+ const argv = [];
1124
+ let current = "";
1125
+ let quote = null;
1126
+ for (let index = 0; index < command.length; index += 1) {
1127
+ const char = command[index] ?? "";
1128
+ if (!quote && SHELL_META_CHARS.has(char)) {
1129
+ return { ok: false, reason: `shell metacharacter '${char}' is not allowlisted` };
1130
+ }
1131
+ if (!quote && char === "$" && command[index + 1] === "(") {
1132
+ return { ok: false, reason: "command substitution is not allowlisted" };
1133
+ }
1134
+ if (char === "`") {
1135
+ return { ok: false, reason: "backtick command substitution is not allowlisted" };
1136
+ }
1137
+ if (quote) {
1138
+ if (char === quote) {
1139
+ quote = null;
1140
+ } else {
1141
+ current += char;
1142
+ }
1143
+ continue;
1144
+ }
1145
+ if (char === "'" || char === '"') {
1146
+ quote = char;
1147
+ continue;
1148
+ }
1149
+ if (/\s/u.test(char)) {
1150
+ if (current.length > 0) {
1151
+ argv.push(current);
1152
+ current = "";
1153
+ }
1154
+ continue;
1155
+ }
1156
+ current += char;
1157
+ }
1158
+ if (quote) {
1159
+ return { ok: false, reason: "unterminated quoted argument" };
1160
+ }
1161
+ if (current.length > 0) {
1162
+ argv.push(current);
1163
+ }
1164
+ if (argv.length === 0) {
1165
+ return { ok: false, reason: "empty command" };
1166
+ }
1167
+ return { ok: true, argv };
1168
+ }
1169
+ function validateAllowedCommand(argv) {
1170
+ const executable = argv[0] ?? "";
1171
+ if (!ALLOWED_COMMANDS.has(executable)) {
1172
+ return { ok: false, reason: `command '${executable}' is not allowlisted` };
1173
+ }
1174
+ if (executable === "git") {
1175
+ return validateGitCommand(argv);
1176
+ }
1177
+ if ((executable === "pnpm" || executable === "npm") && isBroadPackageManagerCommand(argv)) {
1178
+ return {
1179
+ ok: false,
1180
+ reason: "broad package validation belongs to final validation; add --filter, a package path, or a specific test file for story validation"
1181
+ };
1182
+ }
1183
+ return { ok: true };
1184
+ }
1185
+ function validateGitCommand(argv) {
1186
+ const subcommand = argv[1];
1187
+ if (subcommand === "status" || subcommand === "diff" || subcommand === "rev-parse") {
1188
+ return { ok: true };
1189
+ }
1190
+ return { ok: false, reason: `git subcommand '${subcommand ?? ""}' is not allowlisted` };
1191
+ }
1192
+ function isBroadPackageManagerCommand(argv) {
1193
+ const scriptIndex = firstPackageManagerScriptIndex(argv);
1194
+ if (scriptIndex < 0) {
1195
+ return false;
1196
+ }
1197
+ const script = argv[scriptIndex];
1198
+ if (!script || !BROAD_PACKAGE_MANAGER_SCRIPTS.has(script)) {
1199
+ return false;
1200
+ }
1201
+ const args = argv.slice(1);
1202
+ if (args.includes("--filter") || args.includes("-F") || args.some((arg) => arg.startsWith("--filter="))) {
1203
+ return false;
1204
+ }
1205
+ const trailing = argv.slice(scriptIndex + 1);
1206
+ return !trailing.some(isFocusedValidationArgument);
1207
+ }
1208
+ function firstPackageManagerScriptIndex(argv) {
1209
+ const runIndex = argv.indexOf("run");
1210
+ if (runIndex >= 0) {
1211
+ return runIndex + 1;
1212
+ }
1213
+ return argv.findIndex((arg, index) => index > 0 && BROAD_PACKAGE_MANAGER_SCRIPTS.has(arg));
1214
+ }
1215
+ function isFocusedValidationArgument(arg) {
1216
+ return arg.includes("/") || arg.includes("\\") || /\.(?:test|spec)\.[cm]?[tj]sx?$/u.test(arg) || arg.endsWith(".ts") || arg.endsWith(".tsx") || arg.startsWith("--testNamePattern") || arg.startsWith("-t");
1217
+ }
1218
+ function uniqueCommands(commands) {
1219
+ const seen = /* @__PURE__ */ new Set();
1220
+ const unique = [];
1221
+ for (const command of commands.map((item) => normalizeCommandLine(item) ?? "")) {
1222
+ if (command && !seen.has(command)) {
1223
+ seen.add(command);
1224
+ unique.push(command);
1225
+ }
1226
+ }
1227
+ return unique;
1228
+ }
1229
+ function formatCommandOutput(stdout, stderr) {
1230
+ return truncate([stdout.trim(), stderr.trim()].filter(Boolean).join("\n"));
1231
+ }
1232
+ function formatExecutionError(error) {
1233
+ if (typeof error !== "object" || error === null) {
1234
+ return truncate(String(error));
1235
+ }
1236
+ const maybeError = error;
1237
+ const prefix = maybeError.killed ? `timed out after ${COMMAND_TIMEOUT_MS}ms` : `exit ${maybeError.code ?? maybeError.signal ?? "unknown"}`;
1238
+ const output = formatCommandOutput(maybeError.stdout ?? "", maybeError.stderr ?? "");
1239
+ return truncate(
1240
+ output ? `${prefix}; ${output}` : `${prefix}; ${maybeError.message ?? "command failed"}`
1241
+ );
1242
+ }
1243
+ function summarizeValidationResults(input) {
1244
+ if (input.failed.length > 0) {
1245
+ return `Focused validation failed: ${input.failed.length}/${input.total} command(s) failed, ${input.passed.length} passed, ${input.skipped.length} skipped.`;
1246
+ }
1247
+ if (input.passed.length > 0) {
1248
+ return `Focused validation passed: ${input.passed.length}/${input.total} command(s) passed, ${input.skipped.length} skipped.`;
1249
+ }
1250
+ return `Focused validation skipped: ${input.skipped.length}/${input.total} command(s) were rejected or skipped.`;
1251
+ }
1252
+ function isPassedResult(result) {
1253
+ return result.startsWith("passed");
1254
+ }
1255
+ function isFailedResult(result) {
1256
+ return result.startsWith("failed");
1257
+ }
1258
+ function isSkippedResult(result) {
1259
+ return result.startsWith("skipped");
1260
+ }
1261
+ function truncate(value) {
1262
+ return value.length > OUTPUT_LIMIT ? `${value.slice(0, OUTPUT_LIMIT)}\u2026 [truncated]` : value;
1263
+ }
1264
+
1265
+ // src/feature-implementation/validate-story/step.ts
1266
+ var validateStoryStep = step4({ id: "validate-story" }).do(
1267
+ async (input) => {
1268
+ const promptOutput = await runFocusedStoryValidation({
1269
+ validationInput: input,
1270
+ cwd: state5.cwd,
1271
+ implementationContext: await loadStoryPhaseContext("validate-story")
1272
+ });
1273
+ await state5.set(STORY_STATE_KEYS.latestValidationSummary, promptOutput);
1274
+ if (promptOutput.blocked) {
1275
+ await state5.set(
1276
+ STORY_STATE_KEYS.blockedReason,
1277
+ promptOutput.blockedReason ?? "Story validation reported a blocked state."
1278
+ );
1279
+ const { storyRouterStep: storyRouterStep2 } = await import("./step-ZAE3Y7FI.js");
1280
+ return storyRouterStep2({
1281
+ reason: {
1282
+ type: "blocked",
1283
+ sourceReason: "failed-validation",
1284
+ blockedPhase: "validate-story",
1285
+ blockedReason: promptOutput.blockedReason ?? "Story validation reported a blocked state.",
1286
+ code: "story_validation_blocked",
1287
+ metadata: { validation: promptOutput }
1288
+ },
1289
+ currentStory: input.currentStory
1290
+ });
1291
+ }
1292
+ if (!promptOutput.validationPassed) {
1293
+ const { storyRouterStep: storyRouterStep2 } = await import("./step-ZAE3Y7FI.js");
1294
+ return storyRouterStep2({ reason: "failed-validation", currentStory: input.currentStory });
1295
+ }
1296
+ await state5.set(STORY_STATE_KEYS.activePhase, "review-story-implementation");
1297
+ await incrementStoryPhaseAttempt("review-story-implementation");
1298
+ return reviewStoryImplementationStep({
1299
+ currentStory: input.currentStory,
1300
+ attempt: input.attempt,
1301
+ implementationContext: await loadStoryPhaseContext("review-story-implementation"),
1302
+ implementationSummary: input.implementationSummary?.summary,
1303
+ explorationSummary: input.explorationBrief?.summary,
1304
+ redTestSummary: input.redTestSummary?.summary,
1305
+ redEvidence: input.redTestSummary?.redEvidence,
1306
+ validationSummary: promptOutput.summary,
1307
+ validationCommands: promptOutput.commands,
1308
+ gitContext: await loadStoryReviewGitContext()
1309
+ });
1310
+ }
1311
+ );
1312
+
1313
+ // src/feature-implementation/implement-green/prompt.ts
1314
+ import { jsonSchema as jsonSchema4, promptSections as promptSections2, section as section2 } from "@trailstep/authoring";
1315
+ var implementGreenOutput = jsonSchema4({
1316
+ type: "object",
1317
+ properties: {
1318
+ blocked: { type: "boolean" },
1319
+ blockedReason: { type: "string" },
1320
+ summary: { type: "string" },
1321
+ changedFiles: { type: "array", items: { type: "string" } }
1322
+ },
1323
+ required: ["blocked", "summary", "changedFiles"],
1324
+ additionalProperties: false
1325
+ });
1326
+ function implementGreenPrompt({ input }) {
1327
+ const validationCommandEvidence = (input.failedValidationCommands ?? []).map(({ command, result }) => `- ${command}: ${result}`).join("\n");
1328
+ return promptSections2(
1329
+ section2(
1330
+ "Role",
1331
+ "You are the green implementer. Make the smallest production change that satisfies the focused red test; do not broaden scope."
1332
+ ),
1333
+ section2(
1334
+ "Active story implementation view",
1335
+ storyViewForImplementer(input.currentStory.content)
1336
+ ),
1337
+ section2("Implementation context", input.implementationContext),
1338
+ section2("Exploration summary", input.explorationBrief?.summary ?? "Not provided."),
1339
+ section2("Red-test summary", input.redTestSummary?.summary ?? "Not provided."),
1340
+ section2("Red evidence", input.redTestSummary?.redEvidence ?? "Not provided."),
1341
+ input.previousReviewSummary ? section2(
1342
+ "Previous review summary",
1343
+ `${input.previousReviewSummary}
1344
+
1345
+ Required improvements:
1346
+ ${(input.requiredImprovements ?? []).map((item) => `- ${item}`).join("\n") || "- None"}`
1347
+ ) : "",
1348
+ input.failedValidationSummary ? section2(
1349
+ "Previous validation failure",
1350
+ `${input.failedValidationSummary}
1351
+
1352
+ Command evidence:
1353
+ ${validationCommandEvidence || "- Not provided."}`
1354
+ ) : "",
1355
+ section2(
1356
+ "Task",
1357
+ [
1358
+ "Implement the smallest production-code slice needed to make the focused behavioral red test pass.",
1359
+ "Preserve the red test and avoid broad refactoring until the focused behavior is green.",
1360
+ "Do not write new unrelated stories, broad polish, or accumulated-diff cleanup.",
1361
+ "Run the focused green test when feasible and summarize files changed plus concise evidence.",
1362
+ "When this prompt includes a previous validation failure, treat that failure as the primary task: fix the root cause and rerun the failing command when feasible before returning.",
1363
+ "Do not claim the story is complete if focused validation is still red; either keep fixing it or set `blocked: true` with a concrete reason.",
1364
+ "If implementation is unsafe or ambiguous, set `blocked: true` and explain why."
1365
+ ].join("\n")
1366
+ )
1367
+ );
1368
+ }
1369
+
1370
+ // src/feature-implementation/implement-green/step.ts
1371
+ var implementGreenStep = step5({ id: "implement-green" }).prompt(implementGreenPrompt, {
1372
+ agent: "storyImplementer",
1373
+ output: implementGreenOutput
1374
+ }).do(async (promptOutput, input) => {
1375
+ if (promptOutput.blocked) {
1376
+ await state6.set(
1377
+ STORY_STATE_KEYS.blockedReason,
1378
+ promptOutput.blockedReason ?? "Green implementation reported a blocked state."
1379
+ );
1380
+ const { storyRouterStep: storyRouterStep2 } = await import("./step-ZAE3Y7FI.js");
1381
+ return storyRouterStep2({
1382
+ reason: {
1383
+ type: "blocked",
1384
+ sourceReason: "failed-implementation",
1385
+ blockedPhase: "implement-green",
1386
+ blockedReason: promptOutput.blockedReason ?? "Green implementation reported a blocked state.",
1387
+ code: "story_green_blocked"
1388
+ },
1389
+ currentStory: input.currentStory
1390
+ });
1391
+ }
1392
+ await state6.set(STORY_STATE_KEYS.latestImplementationSummary, promptOutput);
1393
+ await state6.set(STORY_STATE_KEYS.activePhase, "validate-story");
1394
+ const attempt = await incrementStoryPhaseAttempt("validate-story");
1395
+ return validateStoryStep({
1396
+ currentStory: input.currentStory,
1397
+ explorationBrief: input.explorationBrief,
1398
+ redTestSummary: input.redTestSummary,
1399
+ implementationSummary: promptOutput,
1400
+ attempt,
1401
+ implementationContext: await loadStoryPhaseContext("validate-story")
1402
+ });
1403
+ });
1404
+
1405
+ // src/feature-implementation/story-doctor/step.ts
1406
+ import { state as state7, step as step6 } from "@trailstep/authoring";
1407
+
1408
+ // src/feature-implementation/story-doctor/prompt.ts
1409
+ import { jsonSchema as jsonSchema5, promptSections as promptSections3, section as section3 } from "@trailstep/authoring";
1410
+ var storyDoctorOutput = jsonSchema5({
1411
+ type: "object",
1412
+ properties: {
1413
+ blocked: { type: "boolean" },
1414
+ blockedReason: { type: "string" },
1415
+ summary: { type: "string" },
1416
+ changedFiles: { type: "array", items: { type: "string" } }
1417
+ },
1418
+ required: ["blocked", "summary", "changedFiles"],
1419
+ additionalProperties: false
1420
+ });
1421
+ function storyDoctorPrompt({ input }) {
1422
+ const commandEvidence = input.failedValidationCommands.map(({ command, result }) => `- ${command}: ${result}`).join("\n");
1423
+ return promptSections3(
1424
+ section3(
1425
+ "Role",
1426
+ "You are the story doctor: a senior recovery agent invoked only after repeated validation failures. Diagnose the actual failing behavior, repair the active story implementation, and do not broaden scope."
1427
+ ),
1428
+ section3(
1429
+ "Active story implementation view",
1430
+ storyViewForImplementer(input.currentStory.content)
1431
+ ),
1432
+ section3("Exploration summary", input.explorationBrief?.summary ?? "Not provided."),
1433
+ section3("Red-test summary", input.redTestSummary?.summary ?? "Not provided."),
1434
+ section3("Red evidence", input.redTestSummary?.redEvidence ?? "Not provided."),
1435
+ section3(
1436
+ "Latest implementation summary",
1437
+ input.implementationSummary?.summary ?? "Not provided."
1438
+ ),
1439
+ section3(
1440
+ "Repeated validation failure",
1441
+ [
1442
+ `Validation failure count consumed by the router: ${input.validationRetryCount}/${input.retryLimit}.`,
1443
+ input.failedValidationSummary,
1444
+ "",
1445
+ "Command evidence:",
1446
+ commandEvidence || "- Not provided."
1447
+ ].join("\n")
1448
+ ),
1449
+ section3(
1450
+ "Task",
1451
+ [
1452
+ "Find and fix the root cause of the repeated validation failure for this active story only.",
1453
+ "Run the focused failing validation command when feasible before returning.",
1454
+ "Do not claim success unless the focused validation is green or you clearly explain why it cannot be made green.",
1455
+ "If the story is unsafe, ambiguous, or cannot be repaired without unrelated redesign, set `blocked: true` with a concrete `blockedReason`.",
1456
+ "Return a concise summary, changed files, and no full logs or diff hunks."
1457
+ ].join("\n")
1458
+ )
1459
+ );
1460
+ }
1461
+
1462
+ // src/feature-implementation/story-doctor/step.ts
1463
+ var storyDoctorStep = step6({ id: "story-doctor" }).prompt(storyDoctorPrompt, {
1464
+ agent: "storyDoctor",
1465
+ output: storyDoctorOutput
1466
+ }).do(async (promptOutput, input) => {
1467
+ if (promptOutput.blocked) {
1468
+ await state7.set(
1469
+ STORY_STATE_KEYS.blockedReason,
1470
+ promptOutput.blockedReason ?? "Story doctor reported a blocked state."
1471
+ );
1472
+ const { storyRouterStep: storyRouterStep2 } = await import("./step-ZAE3Y7FI.js");
1473
+ return storyRouterStep2({
1474
+ reason: {
1475
+ type: "blocked",
1476
+ sourceReason: "failed-implementation",
1477
+ blockedPhase: "implement-green",
1478
+ blockedReason: promptOutput.blockedReason ?? "Story doctor reported a blocked state.",
1479
+ code: "story_doctor_blocked"
1480
+ },
1481
+ currentStory: input.currentStory
1482
+ });
1483
+ }
1484
+ await state7.set(STORY_STATE_KEYS.latestImplementationSummary, promptOutput);
1485
+ await state7.set(STORY_STATE_KEYS.latestStoryRouterState, null);
1486
+ await state7.set(STORY_STATE_KEYS.activePhase, "validate-story");
1487
+ const attempt = await incrementStoryPhaseAttempt("validate-story");
1488
+ return validateStoryStep({
1489
+ currentStory: input.currentStory,
1490
+ explorationBrief: input.explorationBrief,
1491
+ redTestSummary: input.redTestSummary,
1492
+ implementationSummary: promptOutput,
1493
+ attempt
1494
+ });
1495
+ });
1496
+
1497
+ // src/feature-implementation/story-isolation-preflight/step.ts
1498
+ import { fail as fail2, state as state10, step as step9 } from "@trailstep/authoring";
1499
+
1500
+ // src/feature-implementation/deterministic-context-preflight/step.ts
1501
+ import { access, readFile } from "fs/promises";
1502
+ import { isAbsolute, join, relative } from "path";
1503
+ import { state as state9, step as step8 } from "@trailstep/authoring";
1504
+
1505
+ // src/feature-implementation/write-red-tests/step.ts
1506
+ import { state as state8, step as step7 } from "@trailstep/authoring";
1507
+
1508
+ // src/feature-implementation/write-red-tests/prompt.ts
1509
+ import { jsonSchema as jsonSchema6, promptSections as promptSections4, section as section4 } from "@trailstep/authoring";
1510
+ var writeRedTestsOutput = jsonSchema6({
1511
+ type: "object",
1512
+ properties: {
1513
+ blocked: { type: "boolean" },
1514
+ blockedReason: { type: "string" },
1515
+ summary: { type: "string" },
1516
+ redEvidence: { type: "string" },
1517
+ changedFiles: { type: "array", items: { type: "string" } }
1518
+ },
1519
+ required: ["blocked", "summary", "redEvidence", "changedFiles"],
1520
+ additionalProperties: false
1521
+ });
1522
+ function writeRedTestsPrompt({ input }) {
1523
+ return promptSections4(
1524
+ section4(
1525
+ "Role",
1526
+ "You are the red-test writer. Create focused behavioral failing tests; do not implement production behavior or review the story."
1527
+ ),
1528
+ section4("Active story test view", storyViewForTestWriter(input.currentStory.content)),
1529
+ section4("Implementation context", input.implementationContext),
1530
+ section4("Exploration summary", input.explorationBrief?.summary ?? "Not provided."),
1531
+ section4(
1532
+ "Exploration test seams",
1533
+ input.explorationBrief?.testSeams?.map((item) => `- ${item}`).join("\n") ?? "Not provided."
1534
+ ),
1535
+ section4(
1536
+ "Task",
1537
+ [
1538
+ "Write or update the focused behavioral red test for only this active story.",
1539
+ "The test must fail for the intended product/integration behavior before green implementation.",
1540
+ "Do not implement production code except the absolute minimum needed to compile the red test harness.",
1541
+ "Run the focused red test when feasible and report only the command plus concise failing evidence in `redEvidence`.",
1542
+ "Do not include unrelated stories, green implementation work, or review scoring.",
1543
+ "If a safe red test cannot be written, set `blocked: true` and explain why."
1544
+ ].join("\n")
1545
+ )
1546
+ );
1547
+ }
1548
+
1549
+ // src/feature-implementation/write-red-tests/step.ts
1550
+ var writeRedTestsStep = step7({ id: "write-red-tests" }).prompt(writeRedTestsPrompt, {
1551
+ agent: "testWriter",
1552
+ output: writeRedTestsOutput
1553
+ }).do(async (promptOutput, input) => {
1554
+ if (promptOutput.blocked) {
1555
+ await state8.set(
1556
+ STORY_STATE_KEYS.blockedReason,
1557
+ promptOutput.blockedReason ?? "Red-test phase reported a blocked state."
1558
+ );
1559
+ const { storyRouterStep: storyRouterStep2 } = await import("./step-ZAE3Y7FI.js");
1560
+ return storyRouterStep2({
1561
+ reason: {
1562
+ type: "blocked",
1563
+ sourceReason: "failed-red-tests",
1564
+ blockedPhase: "write-red-tests",
1565
+ blockedReason: promptOutput.blockedReason ?? "Red-test phase reported a blocked state.",
1566
+ code: "story_red_tests_blocked"
1567
+ },
1568
+ currentStory: input.currentStory
1569
+ });
1570
+ }
1571
+ await state8.set(STORY_STATE_KEYS.latestRedTestSummary, promptOutput);
1572
+ await state8.set(STORY_STATE_KEYS.activePhase, "implement-green");
1573
+ const attempt = await incrementStoryPhaseAttempt("implement-green");
1574
+ return implementGreenStep({
1575
+ currentStory: input.currentStory,
1576
+ explorationBrief: input.explorationBrief,
1577
+ redTestSummary: promptOutput,
1578
+ attempt,
1579
+ implementationContext: await loadStoryPhaseContext("implement-green")
1580
+ });
1581
+ });
1582
+
1583
+ // src/feature-implementation/deterministic-context-preflight/step.ts
1584
+ var DEFAULT_VALIDATION_COMMANDS = ["pnpm test", "pnpm typecheck", "pnpm lint"];
1585
+ var MAX_ITEMS = 12;
1586
+ var deterministicContextPreflightStep = step8({ id: "deterministic-context-preflight" }).do(
1587
+ async ({
1588
+ currentStory,
1589
+ implementationContext
1590
+ }) => {
1591
+ await state9.set(STORY_STATE_KEYS.activePhase, "deterministic-context-preflight");
1592
+ await incrementStoryPhaseAttempt("deterministic-context-preflight");
1593
+ const brief = await buildDeterministicContextPreflightBrief({
1594
+ currentStory,
1595
+ implementationContext,
1596
+ cwd: state9.cwd
1597
+ });
1598
+ await state9.set(STORY_STATE_KEYS.latestExplorationBrief, brief);
1599
+ await state9.set(STORY_STATE_KEYS.activePhase, "write-red-tests");
1600
+ const attempt = await incrementStoryPhaseAttempt("write-red-tests");
1601
+ return writeRedTestsStep({
1602
+ currentStory,
1603
+ explorationBrief: brief,
1604
+ attempt,
1605
+ implementationContext: await loadStoryPhaseContext("write-red-tests")
1606
+ });
1607
+ }
1608
+ );
1609
+ async function buildDeterministicContextPreflightBrief({
1610
+ currentStory,
1611
+ implementationContext,
1612
+ cwd
1613
+ }) {
1614
+ const sourceText = [currentStory.path, currentStory.content, implementationContext].filter(Boolean).join("\n");
1615
+ const relevantFiles = uniqueBounded([
1616
+ normalizeFileHint(currentStory.path, cwd),
1617
+ ...extractPathHints(sourceText),
1618
+ ...extractPathHints(implementationContext ?? "")
1619
+ ]);
1620
+ const packageHints = uniqueBounded(await packageHintsForFiles(relevantFiles, cwd));
1621
+ const symbols = uniqueBounded(extractSymbolHints(sourceText));
1622
+ const searchHints = uniqueBounded([
1623
+ ...symbols.slice(0, 6).map((symbol) => `rg ${shellQuote2(symbol)}`),
1624
+ relevantFiles.length === 0 && symbols.length === 0 ? "rg focused story keywords from the Goal and Acceptance Criteria" : void 0
1625
+ ]);
1626
+ const explicitValidationCommands = extractValidationCommands(sourceText);
1627
+ const focusedValidationHints = uniqueBounded([
1628
+ ...explicitValidationCommands,
1629
+ ...packageHints.flatMap((filter) => [
1630
+ `pnpm --filter ${filter} test`,
1631
+ `pnpm --filter ${filter} typecheck`,
1632
+ `pnpm --filter ${filter} lint`
1633
+ ]),
1634
+ ...packageHints.length === 0 ? DEFAULT_VALIDATION_COMMANDS : []
1635
+ ]);
1636
+ const graphifyHint = cwd ? await graphifyAvailable(cwd) : false;
1637
+ const relevantFileText = relevantFiles.length > 0 ? relevantFiles.join(", ") : "none detected";
1638
+ const packageText = packageHints.length > 0 ? packageHints.join(", ") : "none detected";
1639
+ const symbolText = symbols.length > 0 ? symbols.join(", ") : "none detected";
1640
+ const contextHints = uniqueBounded(extractContextHints(implementationContext ?? ""), 4);
1641
+ const contextText = contextHints.length > 0 ? ` Context hints: ${contextHints.join("; ")}.` : "";
1642
+ const graphText = graphifyHint ? "graphify appears available; use graphify query before broad grep for structural questions." : "graphify availability was not detected; use focused rg hints if more context is needed.";
1643
+ return {
1644
+ blocked: false,
1645
+ summary: `Deterministic context preflight completed without blocking. Likely files: ${relevantFileText}. Package/filter hints: ${packageText}. Symbols: ${symbolText}.${contextText} ${graphText}`,
1646
+ relevantFiles,
1647
+ testSeams: uniqueBounded([
1648
+ ...extractSectionBullets(sourceText, [
1649
+ "red phase",
1650
+ "acceptance criteria",
1651
+ "validation commands"
1652
+ ]),
1653
+ ...symbols.map((symbol) => `Behavior around ${symbol}`),
1654
+ ...searchHints
1655
+ ]),
1656
+ recommendedValidationCommands: focusedValidationHints,
1657
+ packageHints,
1658
+ symbols,
1659
+ searchHints: uniqueBounded([
1660
+ ...graphifyHint ? [
1661
+ `graphify query ${shellQuote2(`Where is ${symbols[0] ?? currentStory.path} implemented or tested?`)}`
1662
+ ] : [],
1663
+ ...searchHints
1664
+ ]),
1665
+ focusedValidationHints
1666
+ };
1667
+ }
1668
+ function extractPathHints(text) {
1669
+ const candidates = [];
1670
+ const pathPattern = /(?:^|[\s('"`])((?:\.{1,2}\/)?(?:packages|src|docs|scripts|test|tests|\.github|\.pi)\/[A-Za-z0-9_./-]+|[A-Za-z0-9_.-]+\.(?:ts|tsx|js|jsx|json|md|mjs|cjs|svelte|css|yml|yaml))/gmu;
1671
+ for (const match of text.matchAll(pathPattern)) {
1672
+ const candidate = sanitizePathCandidate(match[1]);
1673
+ if (candidate) {
1674
+ candidates.push(candidate);
1675
+ }
1676
+ }
1677
+ return candidates;
1678
+ }
1679
+ function sanitizePathCandidate(candidate) {
1680
+ const cleaned = candidate?.trim().replace(/[),.;:]+$/u, "").replace(/^`|`$/gu, "");
1681
+ if (!cleaned || cleaned.includes("://") || cleaned.includes("../") || /^[A-Za-z]:/u.test(cleaned)) {
1682
+ return void 0;
1683
+ }
1684
+ return cleaned.replace(/\\/gu, "/");
1685
+ }
1686
+ function normalizeFileHint(path, cwd) {
1687
+ const normalizedPath = path.replace(/\\/gu, "/");
1688
+ if (!cwd || !isAbsolute(path)) {
1689
+ return normalizedPath;
1690
+ }
1691
+ const relativePath = relative(cwd, path).replace(/\\/gu, "/");
1692
+ return relativePath.length > 0 && !relativePath.startsWith("../") ? relativePath : normalizedPath;
1693
+ }
1694
+ function extractSymbolHints(text) {
1695
+ const symbols = [];
1696
+ for (const match of text.matchAll(/`([^`\n]{3,80})`/gmu)) {
1697
+ const token = match[1]?.trim();
1698
+ if (token && !token.includes(" ") && !token.includes("/")) {
1699
+ symbols.push(token);
1700
+ }
1701
+ }
1702
+ for (const match of text.matchAll(/\b[A-Z][A-Za-z0-9]*(?:[A-Z][A-Za-z0-9]*)+\b/gmu)) {
1703
+ if (match[0].length <= 80) {
1704
+ symbols.push(match[0]);
1705
+ }
1706
+ }
1707
+ return symbols;
1708
+ }
1709
+ function extractContextHints(text) {
1710
+ return text.split(/\r?\n/u).map((line) => line.replace(/^\s*[-*]\s*/u, "").trim()).filter((line) => line.length > 0 && !/^[A-Za-z-]+:\s*/u.test(line));
1711
+ }
1712
+ function extractValidationCommands(text) {
1713
+ return uniqueBounded(
1714
+ text.split(/\r?\n/u).map((line) => line.replace(/^\s*[-*]\s*/u, "").trim()).filter((line) => /\b(pnpm|npm|yarn|vitest|tsc|biome|node)\b/u.test(line)).map((line) => line.replace(/^`|`$/gu, ""))
1715
+ );
1716
+ }
1717
+ function extractSectionBullets(text, sectionTitles) {
1718
+ const normalizedTitles = new Set(sectionTitles.map((title) => title.toLowerCase()));
1719
+ const lines = text.replace(/\r\n?/gu, "\n").split("\n");
1720
+ const bullets = [];
1721
+ let active = false;
1722
+ for (const line of lines) {
1723
+ const heading = /^#{2,6}\s+(.+?)\s*$/u.exec(line.trim());
1724
+ if (heading) {
1725
+ active = normalizedTitles.has((heading[1] ?? "").trim().toLowerCase());
1726
+ continue;
1727
+ }
1728
+ if (!active) {
1729
+ continue;
1730
+ }
1731
+ const bullet = /^\s*[-*]\s+(.+)$/u.exec(line);
1732
+ if (bullet?.[1]) {
1733
+ bullets.push(bullet[1].trim());
1734
+ }
1735
+ }
1736
+ return bullets;
1737
+ }
1738
+ async function packageHintsForFiles(files, cwd) {
1739
+ const packageDirs = uniqueBounded(
1740
+ files.flatMap((file) => {
1741
+ const normalizedFile = file.replace(/\\/gu, "/");
1742
+ const match = /^packages\/([^/]+)/u.exec(normalizedFile);
1743
+ return match?.[1] ? [`packages/${match[1]}`] : [];
1744
+ })
1745
+ );
1746
+ const hints = [];
1747
+ for (const packageDir of packageDirs) {
1748
+ const packageJsonPath = cwd ? join(cwd, packageDir, "package.json") : void 0;
1749
+ const packageName = packageJsonPath ? await readPackageName(packageJsonPath) : void 0;
1750
+ hints.push(packageName ?? packageDir);
1751
+ }
1752
+ return hints;
1753
+ }
1754
+ async function readPackageName(packageJsonPath) {
1755
+ try {
1756
+ const parsed = JSON.parse(await readFile(packageJsonPath, "utf8"));
1757
+ return typeof parsed.name === "string" ? parsed.name : void 0;
1758
+ } catch {
1759
+ return void 0;
1760
+ }
1761
+ }
1762
+ async function graphifyAvailable(cwd) {
1763
+ try {
1764
+ await access(join(cwd, "graphify-out", "graph.json"));
1765
+ return true;
1766
+ } catch {
1767
+ return false;
1768
+ }
1769
+ }
1770
+ function uniqueBounded(items, max = MAX_ITEMS) {
1771
+ const values = [];
1772
+ const seen = /* @__PURE__ */ new Set();
1773
+ for (const item of items) {
1774
+ if (item === void 0 || item === null) {
1775
+ continue;
1776
+ }
1777
+ const key = String(item);
1778
+ if (key.length === 0 || seen.has(key)) {
1779
+ continue;
1780
+ }
1781
+ seen.add(key);
1782
+ values.push(item);
1783
+ if (values.length >= max) {
1784
+ break;
1785
+ }
1786
+ }
1787
+ return values;
1788
+ }
1789
+ function shellQuote2(value) {
1790
+ return JSON.stringify(value);
1791
+ }
1792
+
1793
+ // src/feature-implementation/story-isolation-preflight/step.ts
1794
+ var storyIsolationPreflightStep = step9({ id: "story-isolation-preflight" }).do(
1795
+ async ({ currentStory }) => {
1796
+ const result = await runStoryIsolationPreflight(currentStory);
1797
+ if (!result.ok) {
1798
+ return result.failure;
1799
+ }
1800
+ const implementationContext = await loadStoryPhaseContext("deterministic-context-preflight");
1801
+ return deterministicContextPreflightStep({ currentStory, implementationContext });
1802
+ }
1803
+ );
1804
+ async function runStoryIsolationPreflight(currentStory) {
1805
+ const completedReplay = await loadCompletedReplayPreflightState(currentStory);
1806
+ if (completedReplay) {
1807
+ await state10.set(STORY_STATE_KEYS.activePhase, "story-isolation-preflight");
1808
+ await state10.set(STORY_STATE_KEYS.storyBaseline, completedReplay.baseline);
1809
+ await state10.set(STORY_STATE_KEYS.activeStoryStartCommit, { commit: completedReplay.baseline });
1810
+ await state10.set(STORY_STATE_KEYS.latestPreflightStatus, {
1811
+ ok: true,
1812
+ code: "story_preflight_replayed",
1813
+ message: "Completed story isolation preflight was replayed from durable state.",
1814
+ baseline: completedReplay.baseline
1815
+ });
1816
+ return { ok: true, baseline: completedReplay.baseline };
1817
+ }
1818
+ await state10.set(STORY_STATE_KEYS.activePhase, "story-isolation-preflight");
1819
+ await incrementStoryPhaseAttempt("story-isolation-preflight");
1820
+ const blockedReplay = await loadBlockedReplayPreflightState(currentStory);
1821
+ if (blockedReplay) {
1822
+ await state10.set(STORY_STATE_KEYS.storyBaseline, blockedReplay.baseline);
1823
+ await state10.set(STORY_STATE_KEYS.activeStoryStartCommit, { commit: blockedReplay.baseline });
1824
+ await state10.set(STORY_STATE_KEYS.latestPreflightStatus, {
1825
+ ok: true,
1826
+ code: "story_preflight_passed",
1827
+ message: "Story isolation preflight passed.",
1828
+ baseline: blockedReplay.baseline
1829
+ });
1830
+ return { ok: true, baseline: blockedReplay.baseline };
1831
+ }
1832
+ const cwd = state10.cwd;
1833
+ if (!cwd) {
1834
+ return preflightFailure(
1835
+ "story_preflight_missing_cwd",
1836
+ "Cannot start story implementation because the TrailStep workflow cwd is unavailable. Retry from a valid project checkout so TrailStep can record a git story baseline.",
1837
+ { storyPath: currentStory.path }
1838
+ );
1839
+ }
1840
+ const insideWorkTree = await runGit(["rev-parse", "--is-inside-work-tree"], cwd);
1841
+ if (!insideWorkTree.ok || insideWorkTree.stdout !== "true") {
1842
+ return preflightFailure(
1843
+ "story_preflight_not_git_worktree",
1844
+ "Cannot start story implementation because the workflow cwd is not a valid git worktree. Run take-it-away/grill-it-away from a git repository with a clean story boundary.",
1845
+ {
1846
+ storyPath: currentStory.path,
1847
+ cwd,
1848
+ gitError: insideWorkTree.ok ? void 0 : insideWorkTree.error
1849
+ }
1850
+ );
1851
+ }
1852
+ const head = await runGit(["rev-parse", "HEAD"], cwd);
1853
+ if (!head.ok || head.stdout.length === 0) {
1854
+ return preflightFailure(
1855
+ "story_preflight_missing_head",
1856
+ "Cannot start story implementation because `git rev-parse HEAD` did not produce a usable baseline commit. Create an initial commit or fix the git repository, then retry.",
1857
+ {
1858
+ storyPath: currentStory.path,
1859
+ cwd,
1860
+ gitError: head.ok ? "No commit was returned." : head.error
1861
+ }
1862
+ );
1863
+ }
1864
+ const existingBaseline = await state10.get(STORY_STATE_KEYS.storyBaseline);
1865
+ const baseline = existingBaseline ?? head.stdout;
1866
+ const reachable = await runGit(["cat-file", "-e", `${baseline}^{commit}`], cwd);
1867
+ if (!reachable.ok) {
1868
+ return preflightFailure(
1869
+ "story_preflight_unreachable_baseline",
1870
+ `Cannot start story implementation because the recorded story baseline \`${baseline}\` is missing or unreachable. Fix the checkout or retry after restoring a valid story boundary.`,
1871
+ { storyPath: currentStory.path, cwd, baseline, gitError: reachable.error }
1872
+ );
1873
+ }
1874
+ const status = await runGit(["status", "--short"], cwd);
1875
+ if (!status.ok) {
1876
+ return preflightFailure(
1877
+ "story_preflight_status_failed",
1878
+ "Cannot start story implementation because TrailStep could not inspect `git status --short`. Fix git status inspection and retry.",
1879
+ { storyPath: currentStory.path, cwd, baseline, gitError: status.error }
1880
+ );
1881
+ }
1882
+ if (status.stdout.trim().length > 0) {
1883
+ return preflightFailure(
1884
+ "story_preflight_dirty_worktree",
1885
+ "Cannot start story implementation because the working tree is dirty before the story begins. Commit, stash, or otherwise restore a clean story boundary, then retry.",
1886
+ { storyPath: currentStory.path, cwd, baseline, statusShort: status.stdout }
1887
+ );
1888
+ }
1889
+ await state10.set(STORY_STATE_KEYS.storyBaseline, baseline);
1890
+ await state10.set(STORY_STATE_KEYS.activeStoryStartCommit, { commit: baseline });
1891
+ await state10.set(STORY_STATE_KEYS.latestPreflightStatus, {
1892
+ ok: true,
1893
+ code: "story_preflight_passed",
1894
+ message: "Story isolation preflight passed.",
1895
+ baseline
1896
+ });
1897
+ return { ok: true, baseline };
1898
+ }
1899
+ async function loadCompletedReplayPreflightState(currentStory) {
1900
+ if (!state10.isReplayingCompletedStep) {
1901
+ return null;
1902
+ }
1903
+ const activeStory = await state10.getPersisted(STORY_STATE_KEYS.activeStory);
1904
+ const baseline = await loadPersistedRecordedBaseline();
1905
+ if (!activeStory || documentsMatch2(activeStory, currentStory)) {
1906
+ return baseline ? { baseline } : { baseline: "replayed-completed-step" };
1907
+ }
1908
+ return { baseline: "replayed-completed-step" };
1909
+ }
1910
+ async function loadBlockedReplayPreflightState(currentStory) {
1911
+ const routerState = await state10.get(
1912
+ STORY_STATE_KEYS.latestStoryRouterState
1913
+ );
1914
+ if (routerState?.route !== "blocked" || routerState.activeStory.path !== currentStory.path || routerState.activeStory.content !== currentStory.content) {
1915
+ return null;
1916
+ }
1917
+ const baseline = await loadRecordedBaseline();
1918
+ return baseline ? { baseline } : null;
1919
+ }
1920
+ async function loadRecordedBaseline() {
1921
+ return await state10.get(STORY_STATE_KEYS.storyBaseline) ?? (await state10.get(STORY_STATE_KEYS.activeStoryStartCommit))?.commit ?? null;
1922
+ }
1923
+ async function loadPersistedRecordedBaseline() {
1924
+ return await state10.getPersisted(STORY_STATE_KEYS.storyBaseline) ?? (await state10.getPersisted(
1925
+ STORY_STATE_KEYS.activeStoryStartCommit
1926
+ ))?.commit ?? null;
1927
+ }
1928
+ function documentsMatch2(left, right) {
1929
+ return left.path === right.path && left.content === right.content;
1930
+ }
1931
+ async function preflightFailure(code, message, details) {
1932
+ await state10.set(STORY_STATE_KEYS.latestPreflightStatus, {
1933
+ ok: false,
1934
+ code,
1935
+ message,
1936
+ baseline: typeof details.baseline === "string" ? details.baseline : void 0
1937
+ });
1938
+ return { ok: false, failure: fail2({ code, message, details }) };
1939
+ }
1940
+
1941
+ // src/feature-implementation/story-router/step.ts
1942
+ var StoryRouterFailureError = class extends Error {
1943
+ failure;
1944
+ constructor(failure) {
1945
+ super(failure.message);
1946
+ this.name = "StoryRouterFailureError";
1947
+ this.failure = failure;
1948
+ }
1949
+ };
1950
+ var storyRouterStep = step10({ id: "story-router" }).do(
1951
+ async ({ currentStory, reason }) => {
1952
+ const previousActivePhase = await state11.get(STORY_STATE_KEYS.activePhase);
1953
+ await state11.set(STORY_STATE_KEYS.activePhase, "story-router");
1954
+ await incrementStoryPhaseAttempt("story-router");
1955
+ if (reason === "failed-review") {
1956
+ const replayedRoute = await replayPersistedRetryRoute(
1957
+ reason,
1958
+ currentStory,
1959
+ previousActivePhase
1960
+ );
1961
+ if (replayedRoute) {
1962
+ return replayedRoute;
1963
+ }
1964
+ return routeFailedReview(currentStory);
1965
+ }
1966
+ if (reason === "failed-validation") {
1967
+ const replayedRoute = await replayPersistedRetryRoute(
1968
+ reason,
1969
+ currentStory,
1970
+ previousActivePhase
1971
+ );
1972
+ if (replayedRoute) {
1973
+ return replayedRoute;
1974
+ }
1975
+ return routeFailedValidation(currentStory);
1976
+ }
1977
+ if (typeof reason === "object" && reason.type === "blocked") {
1978
+ const blockedReplay = await replayBlockedStoryRoute();
1979
+ if (blockedReplay) {
1980
+ return blockedReplay;
1981
+ }
1982
+ return routeBlockedStory(currentStory, reason);
1983
+ }
1984
+ if (reason === "story-completed") {
1985
+ const repairFailure = await repairStaleStoryCompletion(currentStory);
1986
+ if (repairFailure) {
1987
+ return fail3(repairFailure);
1988
+ }
1989
+ }
1990
+ const activeStory = currentStory ?? await state11.get(STORY_STATE_KEYS.activeStory);
1991
+ if (!activeStory) {
1992
+ const [nextStory, ...remaining] = await state11.get(STORY_STATE_KEYS.storyQueue) ?? [];
1993
+ if (!nextStory) {
1994
+ return fail3({
1995
+ code: "story_router_no_active_story",
1996
+ message: "Cannot route story implementation because no active story or queued story is available."
1997
+ });
1998
+ }
1999
+ const [nextStoryContext = "", ...remainingStoryContexts] = await state11.get(STORY_STATE_KEYS.storyContextQueue) ?? [];
2000
+ await state11.set(STORY_STATE_KEYS.storyQueue, remaining);
2001
+ await state11.set(STORY_STATE_KEYS.storyContextQueue, remainingStoryContexts);
2002
+ await state11.set(STORY_STATE_KEYS.activeStory, nextStory);
2003
+ await state11.set(STORY_STATE_KEYS.activeStoryContext, nextStoryContext);
2004
+ return routeStoryAfterPreflight(nextStory);
2005
+ }
2006
+ await state11.set(STORY_STATE_KEYS.activeStory, activeStory);
2007
+ return routeStoryAfterPreflight(activeStory);
2008
+ }
2009
+ );
2010
+ async function routeFailedReview(currentStory) {
2011
+ const activeStory = await state11.get(STORY_STATE_KEYS.activeStory) ?? currentStory;
2012
+ if (!activeStory) {
2013
+ return fail3({
2014
+ code: "story_router_no_active_story",
2015
+ message: "Cannot route failed story review because no active story is available for retry."
2016
+ });
2017
+ }
2018
+ const review = await state11.get(STORY_STATE_KEYS.latestReviewResult);
2019
+ if (!review) {
2020
+ return fail3({
2021
+ code: "story_router_missing_review_result",
2022
+ message: "Cannot route failed story review because no latest review result is available.",
2023
+ details: { storyPath: activeStory.path }
2024
+ });
2025
+ }
2026
+ if (reviewPasses(review)) {
2027
+ return fail3({
2028
+ code: "story_router_review_already_passed",
2029
+ message: "Cannot route failed story review because the latest review already passes.",
2030
+ details: { review, storyPath: activeStory.path }
2031
+ });
2032
+ }
2033
+ const previousCounts = await loadRouterRetryCounts({
2034
+ reviewPhaseFallback: "review-story-implementation"
2035
+ });
2036
+ const reviewRetryCount = previousCounts.reviewRetryCount + 1;
2037
+ const nextCounts = { ...previousCounts, reviewRetryCount };
2038
+ if (reviewRetryCount >= MAX_STORY_REVIEW_ATTEMPTS) {
2039
+ const routerState = await persistRetryRouterState({
2040
+ activeStory,
2041
+ route: "exhausted",
2042
+ sourceReason: "failed-review",
2043
+ code: "story_review_exhausted",
2044
+ counts: nextCounts,
2045
+ retryLimit: MAX_STORY_REVIEW_ATTEMPTS,
2046
+ exhaustedReason: "review",
2047
+ review
2048
+ });
2049
+ return fail3({
2050
+ code: "story_review_exhausted",
2051
+ message: `Story failed review ${reviewRetryCount} times; retry limit is ${MAX_STORY_REVIEW_ATTEMPTS} (last score ${review.score}/5).`,
2052
+ details: {
2053
+ review,
2054
+ storyPath: activeStory.path,
2055
+ reviewRetryCount,
2056
+ retryLimit: MAX_STORY_REVIEW_ATTEMPTS,
2057
+ routerState
2058
+ }
2059
+ });
2060
+ }
2061
+ await state11.set(STORY_STATE_KEYS.activeStory, activeStory);
2062
+ await state11.set(STORY_STATE_KEYS.activePhase, "implement-green");
2063
+ await persistRetryRouterState({
2064
+ activeStory,
2065
+ route: "retrying",
2066
+ targetPhase: "implement-green",
2067
+ sourceReason: "failed-review",
2068
+ code: "story_review_retry",
2069
+ counts: nextCounts,
2070
+ retryLimit: MAX_STORY_REVIEW_ATTEMPTS,
2071
+ review
2072
+ });
2073
+ const attempt = await incrementStoryPhaseAttempt("implement-green");
2074
+ return implementGreenStep({
2075
+ currentStory: activeStory,
2076
+ explorationBrief: await state11.get(STORY_STATE_KEYS.latestExplorationBrief) ?? void 0,
2077
+ redTestSummary: await state11.get(STORY_STATE_KEYS.latestRedTestSummary) ?? void 0,
2078
+ attempt,
2079
+ previousReviewSummary: review.summary,
2080
+ requiredImprovements: review.requiredImprovements,
2081
+ implementationContext: await loadStoryPhaseContext("implement-green")
2082
+ });
2083
+ }
2084
+ async function routeFailedValidation(currentStory) {
2085
+ const activeStory = await state11.get(STORY_STATE_KEYS.activeStory) ?? currentStory;
2086
+ if (!activeStory) {
2087
+ return fail3({
2088
+ code: "story_router_no_active_story",
2089
+ message: "Cannot route failed story validation because no active story is available for retry."
2090
+ });
2091
+ }
2092
+ const validation = await state11.get(
2093
+ STORY_STATE_KEYS.latestValidationSummary
2094
+ );
2095
+ if (!validation) {
2096
+ return fail3({
2097
+ code: "story_router_missing_validation_result",
2098
+ message: "Cannot route failed story validation because no latest validation result is available.",
2099
+ details: { storyPath: activeStory.path }
2100
+ });
2101
+ }
2102
+ if (validation.blocked) {
2103
+ return routeBlockedStory(activeStory, {
2104
+ type: "blocked",
2105
+ sourceReason: "failed-validation",
2106
+ blockedPhase: "validate-story",
2107
+ blockedReason: validation.blockedReason ?? "Story validation reported a blocked state.",
2108
+ code: "story_validation_blocked",
2109
+ metadata: { validation }
2110
+ });
2111
+ }
2112
+ if (validation.validationPassed) {
2113
+ return fail3({
2114
+ code: "story_router_validation_already_passed",
2115
+ message: "Cannot route failed story validation because the latest validation already passes.",
2116
+ details: { storyPath: activeStory.path, validation }
2117
+ });
2118
+ }
2119
+ const previousCounts = await loadRouterRetryCounts({
2120
+ validationPhaseFallback: "validate-story"
2121
+ });
2122
+ const validationRetryCount = previousCounts.validationRetryCount + 1;
2123
+ const nextCounts = { ...previousCounts, validationRetryCount };
2124
+ if (validationRetryCount >= MAX_STORY_VALIDATION_ATTEMPTS) {
2125
+ const routerState = await persistRetryRouterState({
2126
+ activeStory,
2127
+ route: "exhausted",
2128
+ sourceReason: "failed-validation",
2129
+ code: "story_validation_exhausted",
2130
+ counts: nextCounts,
2131
+ retryLimit: MAX_STORY_VALIDATION_ATTEMPTS,
2132
+ exhaustedReason: "validation",
2133
+ validation
2134
+ });
2135
+ return fail3({
2136
+ code: "story_validation_exhausted",
2137
+ message: `Story failed validation ${validationRetryCount} times; retry limit is ${MAX_STORY_VALIDATION_ATTEMPTS}.`,
2138
+ details: {
2139
+ storyPath: activeStory.path,
2140
+ validation,
2141
+ validationRetryCount,
2142
+ retryLimit: MAX_STORY_VALIDATION_ATTEMPTS,
2143
+ routerState
2144
+ }
2145
+ });
2146
+ }
2147
+ const route = validationRetryCount >= STORY_DOCTOR_VALIDATION_FAILURE_THRESHOLD ? "doctoring" : "retrying";
2148
+ const targetPhase = route === "doctoring" ? "story-doctor" : "implement-green";
2149
+ await state11.set(STORY_STATE_KEYS.activeStory, activeStory);
2150
+ await state11.set(STORY_STATE_KEYS.activePhase, targetPhase);
2151
+ await persistRetryRouterState({
2152
+ activeStory,
2153
+ route,
2154
+ targetPhase,
2155
+ sourceReason: "failed-validation",
2156
+ code: route === "doctoring" ? "story_validation_doctor" : "story_validation_retry",
2157
+ counts: nextCounts,
2158
+ retryLimit: MAX_STORY_VALIDATION_ATTEMPTS,
2159
+ validation
2160
+ });
2161
+ if (route === "doctoring") {
2162
+ await incrementStoryPhaseAttempt("story-doctor");
2163
+ return storyDoctorStep({
2164
+ currentStory: activeStory,
2165
+ explorationBrief: await state11.get(STORY_STATE_KEYS.latestExplorationBrief) ?? void 0,
2166
+ redTestSummary: await state11.get(STORY_STATE_KEYS.latestRedTestSummary) ?? void 0,
2167
+ implementationSummary: await state11.get(STORY_STATE_KEYS.latestImplementationSummary) ?? void 0,
2168
+ failedValidationSummary: validation.summary,
2169
+ failedValidationCommands: validation.commands,
2170
+ validationRetryCount,
2171
+ retryLimit: MAX_STORY_VALIDATION_ATTEMPTS
2172
+ });
2173
+ }
2174
+ const attempt = await incrementStoryPhaseAttempt("implement-green");
2175
+ return implementGreenStep({
2176
+ currentStory: activeStory,
2177
+ explorationBrief: await state11.get(STORY_STATE_KEYS.latestExplorationBrief) ?? void 0,
2178
+ redTestSummary: await state11.get(STORY_STATE_KEYS.latestRedTestSummary) ?? void 0,
2179
+ attempt,
2180
+ failedValidationSummary: validation.summary,
2181
+ failedValidationCommands: validation.commands,
2182
+ implementationContext: await loadStoryPhaseContext("implement-green")
2183
+ });
2184
+ }
2185
+ async function routeBlockedStory(currentStory, blockedRoute) {
2186
+ const activeStory = await state11.get(STORY_STATE_KEYS.activeStory) ?? currentStory;
2187
+ if (!activeStory) {
2188
+ return fail3({
2189
+ code: "story_router_no_active_story",
2190
+ message: "Cannot route blocked story because no active story is available."
2191
+ });
2192
+ }
2193
+ const counts = await loadRouterRetryCounts({});
2194
+ const routerState = {
2195
+ route: "blocked",
2196
+ activeStory,
2197
+ reviewRetryCount: counts.reviewRetryCount,
2198
+ validationRetryCount: counts.validationRetryCount,
2199
+ blockedPhase: blockedRoute.blockedPhase,
2200
+ blockedReason: blockedRoute.blockedReason,
2201
+ source: {
2202
+ reason: blockedRoute.sourceReason,
2203
+ blocked: true,
2204
+ code: blockedRoute.code,
2205
+ metadata: blockedRoute.metadata
2206
+ }
2207
+ };
2208
+ await state11.set(STORY_STATE_KEYS.activeStory, activeStory);
2209
+ await state11.set(STORY_STATE_KEYS.latestStoryRouterState, routerState);
2210
+ await state11.set(STORY_STATE_KEYS.blockedReason, blockedRoute.blockedReason);
2211
+ return fail3(formatBlockedRouteFailure(routerState));
2212
+ }
2213
+ async function replayBlockedStoryRoute() {
2214
+ const routerState = await state11.get(
2215
+ STORY_STATE_KEYS.latestStoryRouterState
2216
+ );
2217
+ if (routerState?.route !== "blocked") {
2218
+ return null;
2219
+ }
2220
+ const activeStory = await state11.get(STORY_STATE_KEYS.activeStory);
2221
+ if (!activeStory) {
2222
+ return fail3({
2223
+ code: "story_router_no_active_story",
2224
+ message: "Cannot replay blocked story routing because durable router state exists but no active story is available.",
2225
+ details: { routerState }
2226
+ });
2227
+ }
2228
+ if (!storiesMatch(routerState.activeStory, activeStory)) {
2229
+ return fail3({
2230
+ code: "story_router_blocked_state_mismatch",
2231
+ message: "Cannot replay blocked story routing because durable router state does not match the active story.",
2232
+ details: { storyPath: activeStory.path, routerState }
2233
+ });
2234
+ }
2235
+ await state11.set(STORY_STATE_KEYS.activeStory, activeStory);
2236
+ await state11.set(STORY_STATE_KEYS.blockedReason, routerState.blockedReason ?? null);
2237
+ return fail3(formatBlockedRouteFailure(routerState));
2238
+ }
2239
+ async function replayPersistedRetryRoute(reason, currentStory, previousActivePhase) {
2240
+ const routerState = state11.isReplayingCompletedStep ? await state11.getPersisted(
2241
+ STORY_STATE_KEYS.latestStoryRouterState
2242
+ ) : await state11.get(STORY_STATE_KEYS.latestStoryRouterState);
2243
+ if (!routerState || !isPersistedRetryRoute(routerState.route)) {
2244
+ return null;
2245
+ }
2246
+ if (routerState.source?.reason !== reason) {
2247
+ return null;
2248
+ }
2249
+ const activeStory = await state11.get(STORY_STATE_KEYS.activeStory) ?? currentStory;
2250
+ if (!activeStory) {
2251
+ return inconsistentRetryState(
2252
+ "Cannot replay persisted story retry route because no active story is available.",
2253
+ { routerState }
2254
+ );
2255
+ }
2256
+ if (!routerState.activeStory) {
2257
+ return inconsistentRetryState(
2258
+ "Cannot replay persisted story retry route because the durable router state is missing its active story.",
2259
+ { storyPath: activeStory.path, routerState }
2260
+ );
2261
+ }
2262
+ if (!storiesMatch(routerState.activeStory, activeStory)) {
2263
+ return retryStateMismatch(routerState, activeStory);
2264
+ }
2265
+ const shapeFailure = validatePersistedRetryRouteShape(routerState, reason, activeStory);
2266
+ if (shapeFailure) {
2267
+ return shapeFailure;
2268
+ }
2269
+ if (routerState.route === "exhausted") {
2270
+ return fail3(formatExhaustedRetryRouteFailure(routerState, activeStory));
2271
+ }
2272
+ if (!state11.isReplayingCompletedStep && routerState.route !== "doctoring" && routerState.targetPhase !== previousActivePhase && previousActivePhase !== "story-router") {
2273
+ return null;
2274
+ }
2275
+ if (!await persistedRetryEvidenceMatches(reason, routerState)) {
2276
+ return null;
2277
+ }
2278
+ if (routerState.route === "retrying") {
2279
+ await state11.set(STORY_STATE_KEYS.activeStory, activeStory);
2280
+ await state11.set(STORY_STATE_KEYS.activePhase, "implement-green");
2281
+ const attempt = await incrementStoryPhaseAttempt("implement-green");
2282
+ return implementGreenStep({
2283
+ currentStory: activeStory,
2284
+ explorationBrief: await state11.get(STORY_STATE_KEYS.latestExplorationBrief) ?? void 0,
2285
+ redTestSummary: await state11.get(STORY_STATE_KEYS.latestRedTestSummary) ?? void 0,
2286
+ attempt,
2287
+ previousReviewSummary: reason === "failed-review" ? routerState.latestReview?.summary : void 0,
2288
+ requiredImprovements: reason === "failed-review" ? routerState.latestReview?.requiredImprovements : void 0,
2289
+ failedValidationSummary: reason === "failed-validation" ? routerState.latestValidation?.summary : void 0,
2290
+ failedValidationCommands: reason === "failed-validation" ? routerState.latestValidation?.commands : void 0,
2291
+ implementationContext: await loadStoryPhaseContext("implement-green")
2292
+ });
2293
+ }
2294
+ if (routerState.route === "doctoring") {
2295
+ await state11.set(STORY_STATE_KEYS.activeStory, activeStory);
2296
+ await state11.set(STORY_STATE_KEYS.activePhase, "story-doctor");
2297
+ await incrementStoryPhaseAttempt("story-doctor");
2298
+ return storyDoctorStep({
2299
+ currentStory: activeStory,
2300
+ explorationBrief: await state11.get(STORY_STATE_KEYS.latestExplorationBrief) ?? void 0,
2301
+ redTestSummary: await state11.get(STORY_STATE_KEYS.latestRedTestSummary) ?? void 0,
2302
+ implementationSummary: await state11.get(STORY_STATE_KEYS.latestImplementationSummary) ?? void 0,
2303
+ failedValidationSummary: routerState.latestValidation?.summary ?? "",
2304
+ failedValidationCommands: routerState.latestValidation?.commands ?? [],
2305
+ validationRetryCount: routerState.validationRetryCount ?? 0,
2306
+ retryLimit: routerState.retryLimit ?? MAX_STORY_VALIDATION_ATTEMPTS
2307
+ });
2308
+ }
2309
+ return inconsistentRetryState(
2310
+ "Cannot replay persisted story retry route with an unknown route.",
2311
+ {
2312
+ storyPath: activeStory.path,
2313
+ routerState
2314
+ }
2315
+ );
2316
+ }
2317
+ function isPersistedRetryRoute(route) {
2318
+ return route === "retrying" || route === "doctoring" || route === "exhausted";
2319
+ }
2320
+ function validatePersistedRetryRouteShape(routerState, reason, activeStory) {
2321
+ if (typeof routerState.source?.code !== "string" || routerState.source.code.length === 0) {
2322
+ return inconsistentRetryState(
2323
+ "Cannot replay persisted story retry route because the durable router state is missing its source code.",
2324
+ { storyPath: activeStory.path, routerState }
2325
+ );
2326
+ }
2327
+ if (!isNonNegativeInteger(routerState.reviewRetryCount)) {
2328
+ return inconsistentRetryState(
2329
+ "Cannot replay persisted story retry route because the review retry count is missing or invalid.",
2330
+ { storyPath: activeStory.path, routerState }
2331
+ );
2332
+ }
2333
+ if (!isNonNegativeInteger(routerState.validationRetryCount)) {
2334
+ return inconsistentRetryState(
2335
+ "Cannot replay persisted story retry route because the validation retry count is missing or invalid.",
2336
+ { storyPath: activeStory.path, routerState }
2337
+ );
2338
+ }
2339
+ if (!isPositiveInteger(routerState.retryLimit)) {
2340
+ return inconsistentRetryState(
2341
+ "Cannot replay persisted story retry route because the retry limit is missing or invalid.",
2342
+ { storyPath: activeStory.path, routerState }
2343
+ );
2344
+ }
2345
+ if (routerState.route === "retrying" && routerState.targetPhase !== "implement-green") {
2346
+ return inconsistentRetryState(
2347
+ "Cannot replay persisted story retry route because retrying routes must target implement-green.",
2348
+ { storyPath: activeStory.path, routerState }
2349
+ );
2350
+ }
2351
+ if (routerState.route === "doctoring" && routerState.targetPhase !== "story-doctor") {
2352
+ return inconsistentRetryState(
2353
+ "Cannot replay persisted story doctor route because doctoring routes must target story-doctor.",
2354
+ { storyPath: activeStory.path, routerState }
2355
+ );
2356
+ }
2357
+ if (reason === "failed-review" && !hasReviewMetadata(routerState.latestReview)) {
2358
+ return inconsistentRetryState(
2359
+ "Cannot replay persisted story review retry route because latest review evidence is missing or invalid.",
2360
+ { storyPath: activeStory.path, routerState }
2361
+ );
2362
+ }
2363
+ if (reason === "failed-validation" && !hasValidationMetadata(routerState.latestValidation)) {
2364
+ return inconsistentRetryState(
2365
+ "Cannot replay persisted story validation retry route because latest validation evidence is missing or invalid.",
2366
+ { storyPath: activeStory.path, routerState }
2367
+ );
2368
+ }
2369
+ return null;
2370
+ }
2371
+ async function persistedRetryEvidenceMatches(reason, routerState) {
2372
+ if (routerState.source?.reason !== reason) {
2373
+ return false;
2374
+ }
2375
+ if (state11.isReplayingCompletedStep) {
2376
+ return true;
2377
+ }
2378
+ return routerStateEvidenceMatchesLatest(routerState);
2379
+ }
2380
+ async function routerStateEvidenceMatchesLatest(routerState) {
2381
+ if (routerState.source?.reason === "failed-review") {
2382
+ if (!hasReviewMetadata(routerState.latestReview)) {
2383
+ return false;
2384
+ }
2385
+ const review = await state11.get(STORY_STATE_KEYS.latestReviewResult);
2386
+ return !!review && routerState.latestReview.score === review.score && routerState.latestReview.summary === review.summary && stringArraysEqual(routerState.latestReview.requiredImprovements, review.requiredImprovements);
2387
+ }
2388
+ if (routerState.source?.reason === "failed-validation") {
2389
+ if (!hasValidationMetadata(routerState.latestValidation)) {
2390
+ return false;
2391
+ }
2392
+ const validation = await state11.get(
2393
+ STORY_STATE_KEYS.latestValidationSummary
2394
+ );
2395
+ return !!validation && routerState.latestValidation.summary === validation.summary && validationCommandArraysEqual(routerState.latestValidation.commands, validation.commands);
2396
+ }
2397
+ return true;
2398
+ }
2399
+ function hasReviewMetadata(value) {
2400
+ return typeof value === "object" && value !== null && "score" in value && typeof value.score === "number" && "summary" in value && typeof value.summary === "string" && "requiredImprovements" in value && Array.isArray(value.requiredImprovements) && value.requiredImprovements.every((item) => typeof item === "string");
2401
+ }
2402
+ function hasValidationMetadata(value) {
2403
+ return typeof value === "object" && value !== null && "summary" in value && typeof value.summary === "string" && "commands" in value && Array.isArray(value.commands) && value.commands.every(
2404
+ (item) => typeof item === "object" && item !== null && "command" in item && typeof item.command === "string" && "result" in item && typeof item.result === "string"
2405
+ );
2406
+ }
2407
+ function storiesMatch(left, right) {
2408
+ return left.path === right.path && left.content === right.content;
2409
+ }
2410
+ function stringArraysEqual(left, right) {
2411
+ return left.length === right.length && left.every((item, index) => item === right[index]);
2412
+ }
2413
+ function validationCommandArraysEqual(left, right) {
2414
+ return left.length === right.length && left.every(
2415
+ (item, index) => item.command === right[index]?.command && item.result === right[index]?.result
2416
+ );
2417
+ }
2418
+ function isNonNegativeInteger(value) {
2419
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
2420
+ }
2421
+ function isPositiveInteger(value) {
2422
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
2423
+ }
2424
+ function retryStateMismatch(routerState, activeStory) {
2425
+ throw new StoryRouterFailureError({
2426
+ code: "story_router_retry_state_mismatch",
2427
+ message: "Cannot replay persisted story retry route because durable router state does not match the active story.",
2428
+ details: { storyPath: activeStory.path, routerState }
2429
+ });
2430
+ }
2431
+ function inconsistentRetryState(message, details) {
2432
+ throw new StoryRouterFailureError({
2433
+ code: "story_router_inconsistent_retry_state",
2434
+ message,
2435
+ details
2436
+ });
2437
+ }
2438
+ function formatExhaustedRetryRouteFailure(routerState, activeStory) {
2439
+ const exhaustedReason = routerState.exhaustedReason ?? reasonForSource(routerState.source?.reason);
2440
+ const retryCount = exhaustedReason === "review" ? routerState.reviewRetryCount : routerState.validationRetryCount;
2441
+ return {
2442
+ code: routerState.source?.code ?? `story_${exhaustedReason}_exhausted`,
2443
+ message: `Story ${exhaustedReason} retry route is already exhausted (${retryCount}/${routerState.retryLimit}).`,
2444
+ details: { storyPath: activeStory.path, routerState }
2445
+ };
2446
+ }
2447
+ function reasonForSource(reason) {
2448
+ return reason === "failed-review" ? "review" : "validation";
2449
+ }
2450
+ async function persistRetryRouterState(input) {
2451
+ const routerState = {
2452
+ route: input.route,
2453
+ activeStory: input.activeStory,
2454
+ targetPhase: input.targetPhase,
2455
+ reviewRetryCount: input.counts.reviewRetryCount,
2456
+ validationRetryCount: input.counts.validationRetryCount,
2457
+ retryLimit: input.retryLimit,
2458
+ exhaustedReason: input.exhaustedReason,
2459
+ latestReview: input.review ? {
2460
+ score: input.review.score,
2461
+ summary: input.review.summary,
2462
+ requiredImprovements: input.review.requiredImprovements
2463
+ } : void 0,
2464
+ latestValidation: input.validation ? {
2465
+ summary: input.validation.summary,
2466
+ commands: input.validation.commands
2467
+ } : void 0,
2468
+ source: {
2469
+ reason: input.sourceReason,
2470
+ code: input.code
2471
+ }
2472
+ };
2473
+ await state11.set(STORY_STATE_KEYS.latestStoryRouterState, routerState);
2474
+ return routerState;
2475
+ }
2476
+ async function loadRouterRetryCounts(input) {
2477
+ const activeStory = await state11.get(STORY_STATE_KEYS.activeStory);
2478
+ const routerState = await state11.get(
2479
+ STORY_STATE_KEYS.latestStoryRouterState
2480
+ );
2481
+ const attemptsByPhase = await state11.get(STORY_STATE_KEYS.attemptsByPhase) ?? {};
2482
+ const routerStateMatchesActiveStory = activeStory && routerState?.activeStory?.path === activeStory.path && routerState.activeStory.content === activeStory.content;
2483
+ if (routerStateMatchesActiveStory) {
2484
+ const fallbackCounts = {
2485
+ reviewRetryCount: phaseAttemptFallback(attemptsByPhase, input.reviewPhaseFallback),
2486
+ validationRetryCount: phaseAttemptFallback(attemptsByPhase, input.validationPhaseFallback)
2487
+ };
2488
+ const replayingSameReviewEvidence = input.reviewPhaseFallback && routerState.source?.reason === "failed-review";
2489
+ const replayingSameValidationEvidence = input.validationPhaseFallback && routerState.source?.reason === "failed-validation";
2490
+ if ((replayingSameReviewEvidence || replayingSameValidationEvidence) && !await routerStateEvidenceMatchesLatest(routerState)) {
2491
+ return fallbackCounts;
2492
+ }
2493
+ return {
2494
+ reviewRetryCount: normalizeRetryCount(routerState.reviewRetryCount),
2495
+ validationRetryCount: normalizeRetryCount(routerState.validationRetryCount)
2496
+ };
2497
+ }
2498
+ if (routerState?.activeStory) {
2499
+ return {
2500
+ reviewRetryCount: 0,
2501
+ validationRetryCount: 0
2502
+ };
2503
+ }
2504
+ return {
2505
+ reviewRetryCount: phaseAttemptFallback(attemptsByPhase, input.reviewPhaseFallback),
2506
+ validationRetryCount: phaseAttemptFallback(attemptsByPhase, input.validationPhaseFallback)
2507
+ };
2508
+ }
2509
+ function phaseAttemptFallback(attemptsByPhase, phase) {
2510
+ if (!phase) {
2511
+ return 0;
2512
+ }
2513
+ return Math.max(0, normalizeRetryCount(attemptsByPhase[phase]) - 1);
2514
+ }
2515
+ function normalizeRetryCount(value) {
2516
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
2517
+ }
2518
+ function formatBlockedRouteFailure(routerState) {
2519
+ return {
2520
+ code: routerState.source.code,
2521
+ message: routerState.blockedReason ?? "Story routing is blocked.",
2522
+ details: {
2523
+ storyPath: routerState.activeStory.path,
2524
+ routerState,
2525
+ ...formatBlockedRouteFailureDetails(
2526
+ routerState.blockedPhase ?? "validate-story",
2527
+ routerState.source.metadata
2528
+ )
2529
+ }
2530
+ };
2531
+ }
2532
+ function formatBlockedRouteFailureDetails(blockedPhase, metadata) {
2533
+ return {
2534
+ blockedPhase,
2535
+ ...metadata ? metadata : {}
2536
+ };
2537
+ }
2538
+ async function repairStaleStoryCompletion(currentStory) {
2539
+ if (!currentStory) {
2540
+ return void 0;
2541
+ }
2542
+ const persistedActiveStory = await state11.get(STORY_STATE_KEYS.activeStory);
2543
+ if (!persistedActiveStory || storiesMatch(persistedActiveStory, currentStory)) {
2544
+ return void 0;
2545
+ }
2546
+ const storyQueue = await state11.get(STORY_STATE_KEYS.storyQueue) ?? [];
2547
+ const queuedCurrentStoryIndex = storyQueue.findIndex(
2548
+ (story) => storiesMatch(story, currentStory)
2549
+ );
2550
+ if (queuedCurrentStoryIndex !== 0) {
2551
+ return {
2552
+ code: "story_router_stale_completion_mismatch",
2553
+ message: "Cannot repair stale story completion state because the routed next story is not the immediate queued successor.",
2554
+ details: {
2555
+ persistedActiveStoryPath: persistedActiveStory.path,
2556
+ currentStoryPath: currentStory.path,
2557
+ queuedCurrentStoryIndex,
2558
+ queuedStoryPaths: storyQueue.map((story) => story.path)
2559
+ }
2560
+ };
2561
+ }
2562
+ const completed = await state11.get(STORY_STATE_KEYS.completedStories) ?? [];
2563
+ const completedTitle = extractStoryTitle(persistedActiveStory.content, completed.length + 1);
2564
+ await resetStoryLocalStateForNextStory();
2565
+ await state11.set(
2566
+ STORY_STATE_KEYS.completedStories,
2567
+ completed.includes(completedTitle) ? completed : [...completed, completedTitle]
2568
+ );
2569
+ await state11.set(STORY_STATE_KEYS.storyQueue, storyQueue.slice(1));
2570
+ const storyContextQueue = await state11.get(STORY_STATE_KEYS.storyContextQueue) ?? [];
2571
+ await state11.set(STORY_STATE_KEYS.storyContextQueue, storyContextQueue.slice(1));
2572
+ await state11.set(STORY_STATE_KEYS.activeStory, currentStory);
2573
+ await state11.set(STORY_STATE_KEYS.activeStoryContext, storyContextQueue[0] ?? "");
2574
+ return void 0;
2575
+ }
2576
+ async function routeStoryAfterPreflight(activeStory) {
2577
+ return storyIsolationPreflightStep({ currentStory: activeStory });
2578
+ }
2579
+
2580
+ export {
2581
+ takeItAwayOutput,
2582
+ STORY_BOUNDARY,
2583
+ STORY_CONTEXT_START,
2584
+ STORY_CONTEXT_END,
2585
+ MAX_IMPLEMENTATION_DOC_REVIEW_ATTEMPTS,
2586
+ reviewOutput,
2587
+ reviewPasses,
2588
+ STORY_STATE_KEYS,
2589
+ resetStoryLocalState,
2590
+ takeItAwayInput,
2591
+ normalizeTakeItAwayWorkflowOptions,
2592
+ storyRouterStep
2593
+ };
2594
+ //# sourceMappingURL=chunk-RF3PMHPX.js.map