wile 0.8.0 → 1.0.1

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.
Files changed (31) hide show
  1. package/dist/agent/Dockerfile +1 -1
  2. package/dist/agent/README.md +20 -20
  3. package/dist/agent/entrypoint.sh +33 -21
  4. package/dist/agent/scripts/claude-stream.ts +159 -0
  5. package/dist/agent/scripts/codex-stream.ts +167 -0
  6. package/dist/agent/scripts/gemini-stream.ts +97 -0
  7. package/dist/agent/scripts/mock-claude.sh +48 -21
  8. package/dist/agent/scripts/mock-codex.sh +48 -21
  9. package/dist/agent/scripts/mock-gemini.sh +48 -21
  10. package/dist/agent/scripts/opencode-stream.ts +93 -0
  11. package/dist/agent/scripts/prompt-compact.md +36 -34
  12. package/dist/agent/scripts/prompt-preflight.md +0 -7
  13. package/dist/agent/scripts/prompt.md +26 -24
  14. package/dist/agent/scripts/test-additional-instructions.sh +4 -2
  15. package/dist/agent/scripts/test-env-project-docker.sh +5 -4
  16. package/dist/agent/scripts/test-iteration-limit.sh +4 -2
  17. package/dist/agent/scripts/test-prd-validation-docker.sh +151 -0
  18. package/dist/agent/scripts/test-preflight-claude-docker.sh +5 -4
  19. package/dist/agent/scripts/test-preflight-docker.sh +16 -13
  20. package/dist/agent/scripts/validate-compact.ts +134 -0
  21. package/dist/agent/scripts/validate-prd.ts +280 -0
  22. package/dist/agent/scripts/wile-compact.sh +6 -6
  23. package/dist/agent/scripts/wile-preflight.sh +4 -4
  24. package/dist/agent/scripts/wile.sh +4 -4
  25. package/dist/cli.js +236 -58
  26. package/package.json +10 -1
  27. package/dist/agent/scripts/claude-stream.js +0 -88
  28. package/dist/agent/scripts/codex-stream.js +0 -125
  29. package/dist/agent/scripts/gemini-stream.js +0 -63
  30. package/dist/agent/scripts/opencode-stream.js +0 -52
  31. package/dist/agent/scripts/validate-compact.js +0 -135
@@ -0,0 +1,151 @@
1
+ #!/bin/sh
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR=$(cd "$(dirname "$0")/../../.." && pwd)
5
+ AGENT_DIR="$ROOT_DIR/packages/agent"
6
+
7
+ docker build -t wile-agent:local "$AGENT_DIR" >/dev/null
8
+
9
+ run_case() {
10
+ CASE_NAME="$1"
11
+ EXPECT_SUCCESS="$2"
12
+ EXPECT_PATTERN="$3"
13
+
14
+ TMP_DIR=$(mktemp -d "/tmp/wile-prd-${CASE_NAME}-XXXXXX")
15
+ cleanup() {
16
+ rm -rf "$TMP_DIR"
17
+ }
18
+ trap cleanup EXIT INT TERM
19
+
20
+ mkdir -p "$TMP_DIR/.wile"
21
+ printf "# Wile Progress Log\n\n## Codebase Patterns\n\n---\n" > "$TMP_DIR/.wile/progress.txt"
22
+
23
+ case "$CASE_NAME" in
24
+ missing_prd)
25
+ ;;
26
+ root_only)
27
+ cat > "$TMP_DIR/prd.json" <<'JSON'
28
+ {
29
+ "stories": [
30
+ {
31
+ "id": 1,
32
+ "title": "Root only",
33
+ "description": "should fail because .wile/prd.json is required",
34
+ "acceptanceCriteria": ["n/a"],
35
+ "dependsOn": [],
36
+ "status": "pending"
37
+ }
38
+ ]
39
+ }
40
+ JSON
41
+ ;;
42
+ missing_dependency)
43
+ cat > "$TMP_DIR/.wile/prd.json" <<'JSON'
44
+ {
45
+ "stories": [
46
+ {
47
+ "id": 1,
48
+ "title": "Invalid dep",
49
+ "description": "missing dependency",
50
+ "acceptanceCriteria": ["n/a"],
51
+ "dependsOn": [99],
52
+ "status": "pending"
53
+ }
54
+ ]
55
+ }
56
+ JSON
57
+ ;;
58
+ cycle)
59
+ cat > "$TMP_DIR/.wile/prd.json" <<'JSON'
60
+ {
61
+ "stories": [
62
+ {
63
+ "id": 1,
64
+ "title": "Cycle A",
65
+ "description": "depends on B",
66
+ "acceptanceCriteria": ["n/a"],
67
+ "dependsOn": [2],
68
+ "status": "pending"
69
+ },
70
+ {
71
+ "id": 2,
72
+ "title": "Cycle B",
73
+ "description": "depends on A",
74
+ "acceptanceCriteria": ["n/a"],
75
+ "dependsOn": [1],
76
+ "status": "pending"
77
+ }
78
+ ]
79
+ }
80
+ JSON
81
+ ;;
82
+ later_dependency_valid)
83
+ cat > "$TMP_DIR/.wile/prd.json" <<'JSON'
84
+ {
85
+ "stories": [
86
+ {
87
+ "id": 1,
88
+ "title": "Can run with later dependency",
89
+ "description": "depends on story 2 which is already done",
90
+ "acceptanceCriteria": ["n/a"],
91
+ "dependsOn": [2],
92
+ "status": "pending"
93
+ },
94
+ {
95
+ "id": 2,
96
+ "title": "Done prerequisite",
97
+ "description": "already complete",
98
+ "acceptanceCriteria": ["n/a"],
99
+ "dependsOn": [],
100
+ "status": "done"
101
+ }
102
+ ]
103
+ }
104
+ JSON
105
+ ;;
106
+ *)
107
+ echo "error: unknown case $CASE_NAME" >&2
108
+ exit 1
109
+ ;;
110
+ esac
111
+
112
+ OUTPUT_FILE="$TMP_DIR/output.txt"
113
+
114
+ set +e
115
+ docker run --rm \
116
+ -e CODING_AGENT=CC \
117
+ -e CC_ANTHROPIC_API_KEY=dummy-key \
118
+ -e WILE_REPO_SOURCE=local \
119
+ -e WILE_LOCAL_REPO_PATH=/home/wile/workspace/repo \
120
+ -e MAX_ITERATIONS=2 \
121
+ -e WILE_MOCK_CLAUDE=true \
122
+ -v "$TMP_DIR:/home/wile/workspace/repo" \
123
+ wile-agent:local 2>&1 | tee "$OUTPUT_FILE"
124
+ EXIT_CODE=$?
125
+ set -e
126
+
127
+ if [ "$EXPECT_SUCCESS" = "yes" ]; then
128
+ if [ "$EXIT_CODE" -ne 0 ]; then
129
+ echo "error: expected success for case $CASE_NAME" >&2
130
+ exit 1
131
+ fi
132
+ else
133
+ if [ "$EXIT_CODE" -eq 0 ]; then
134
+ echo "error: expected failure for case $CASE_NAME" >&2
135
+ exit 1
136
+ fi
137
+ fi
138
+
139
+ grep -q "$EXPECT_PATTERN" "$OUTPUT_FILE"
140
+
141
+ rm -rf "$TMP_DIR"
142
+ trap - EXIT INT TERM
143
+ }
144
+
145
+ run_case missing_prd no ".wile/prd.json not found"
146
+ run_case root_only no ".wile/prd.json not found"
147
+ run_case missing_dependency no "depends on missing story id"
148
+ run_case cycle no "Dependency cycle detected"
149
+ run_case later_dependency_valid yes "Starting Wile Loop"
150
+
151
+ echo "test-prd-validation-docker: ok"
@@ -20,13 +20,14 @@ trap cleanup EXIT INT TERM
20
20
  mkdir -p "$TMP_DIR/.wile"
21
21
  cat > "$TMP_DIR/.wile/prd.json" <<'JSON'
22
22
  {
23
- "userStories": [
23
+ "stories": [
24
24
  {
25
- "id": "US-TEST-CLAUDE-001",
25
+ "id": 1,
26
26
  "title": "Preflight fail test (claude)",
27
+ "description": "Preflight failure harness",
27
28
  "acceptanceCriteria": ["n/a"],
28
- "priority": 1,
29
- "passes": false
29
+ "dependsOn": [],
30
+ "status": "pending"
30
31
  }
31
32
  ]
32
33
  }
@@ -15,15 +15,16 @@ run_failure_case() {
15
15
  trap cleanup EXIT INT TERM
16
16
 
17
17
  mkdir -p "$TMP_DIR/.wile"
18
- cat > "$TMP_DIR/.wile/prd.json" <<'JSON'
18
+ cat > "$TMP_DIR/.wile/prd.json" <<'JSON'
19
19
  {
20
- "userStories": [
20
+ "stories": [
21
21
  {
22
- "id": "US-TEST-001",
22
+ "id": 1,
23
23
  "title": "Preflight fail test",
24
+ "description": "Preflight failure harness",
24
25
  "acceptanceCriteria": ["n/a"],
25
- "priority": 1,
26
- "passes": false
26
+ "dependsOn": [],
27
+ "status": "pending"
27
28
  }
28
29
  ]
29
30
  }
@@ -87,13 +88,14 @@ run_trailing_case() {
87
88
  mkdir -p "$TMP_DIR/.wile"
88
89
  cat > "$TMP_DIR/.wile/prd.json" <<'JSON'
89
90
  {
90
- "userStories": [
91
+ "stories": [
91
92
  {
92
- "id": "US-TEST-003",
93
+ "id": 1,
93
94
  "title": "Preflight trailing marker test",
95
+ "description": "Preflight trailing marker harness",
94
96
  "acceptanceCriteria": ["n/a"],
95
- "priority": 1,
96
- "passes": false
97
+ "dependsOn": [],
98
+ "status": "pending"
97
99
  }
98
100
  ]
99
101
  }
@@ -145,13 +147,14 @@ run_success_case() {
145
147
  mkdir -p "$TMP_DIR/.wile"
146
148
  cat > "$TMP_DIR/.wile/prd.json" <<'JSON'
147
149
  {
148
- "userStories": [
150
+ "stories": [
149
151
  {
150
- "id": "US-TEST-002",
152
+ "id": 1,
151
153
  "title": "Preflight success test",
154
+ "description": "Preflight success harness",
152
155
  "acceptanceCriteria": ["n/a"],
153
- "priority": 1,
154
- "passes": false
156
+ "dependsOn": [],
157
+ "status": "pending"
155
158
  }
156
159
  ]
157
160
  }
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env bun
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import path from "node:path";
5
+
6
+ type JsonObject = Record<string, unknown>;
7
+
8
+ const prdPath = ".wile/prd.json";
9
+ const progressPath = ".wile/progress.txt";
10
+ const prdOriginalPath = ".wile/prd.json.original";
11
+ const scriptPath = process.argv[1] ?? "validate-compact.ts";
12
+ const validatePrdPath = path.join(path.dirname(scriptPath), "validate-prd.ts");
13
+
14
+ const fail = (message: string): never => {
15
+ console.error(message);
16
+ process.exit(1);
17
+ };
18
+
19
+ const isObject = (value: unknown): value is JsonObject =>
20
+ typeof value === "object" && value !== null && !Array.isArray(value);
21
+
22
+ const parseStories = (value: unknown, errorMessage: string): JsonObject[] => {
23
+ if (!Array.isArray(value)) {
24
+ fail(errorMessage);
25
+ }
26
+ const values = value as unknown[];
27
+
28
+ const stories: JsonObject[] = [];
29
+ for (const entry of values) {
30
+ if (!isObject(entry)) {
31
+ fail(errorMessage);
32
+ }
33
+ stories.push(entry as JsonObject);
34
+ }
35
+
36
+ return stories;
37
+ };
38
+
39
+ const getStoryId = (story: JsonObject, errorPrefix: string): number => {
40
+ const id = story.id;
41
+ if (typeof id !== "number" || !Number.isInteger(id)) {
42
+ fail(`${errorPrefix} story has invalid id.`);
43
+ }
44
+ return id as number;
45
+ };
46
+
47
+ const getStoryStatus = (story: JsonObject, errorPrefix: string): string => {
48
+ const status = story.status;
49
+ if (typeof status !== "string") {
50
+ fail(`${errorPrefix} story ${getStoryId(story, errorPrefix)} has invalid status.`);
51
+ }
52
+ return status as string;
53
+ };
54
+
55
+ if (!existsSync(prdPath)) {
56
+ fail("Missing .wile/prd.json after compact.");
57
+ }
58
+
59
+ if (!existsSync(progressPath)) {
60
+ fail("Missing .wile/progress.txt after compact.");
61
+ }
62
+
63
+ const validateResult = spawnSync("bun", [validatePrdPath, "--path", prdPath], {
64
+ encoding: "utf8"
65
+ });
66
+ if (validateResult.status !== 0) {
67
+ const message = (validateResult.stderr || validateResult.stdout || "").trim();
68
+ fail(message || "Compacted .wile/prd.json failed schema validation.");
69
+ }
70
+
71
+ let payload: unknown;
72
+ try {
73
+ payload = JSON.parse(readFileSync(prdPath, "utf8"));
74
+ } catch {
75
+ fail("Compacted .wile/prd.json is not valid JSON.");
76
+ }
77
+
78
+ if (!isObject(payload)) {
79
+ fail('Compacted .wile/prd.json must contain a top-level "stories" array.');
80
+ }
81
+
82
+ const currentStories = parseStories(
83
+ (payload as JsonObject).stories,
84
+ 'Compacted .wile/prd.json must contain a top-level "stories" array.'
85
+ );
86
+
87
+ if (existsSync(prdOriginalPath)) {
88
+ let originalPayload: unknown;
89
+ try {
90
+ originalPayload = JSON.parse(readFileSync(prdOriginalPath, "utf8"));
91
+ } catch {
92
+ fail("Original .wile/prd.json.original is not valid JSON.");
93
+ }
94
+
95
+ if (!isObject(originalPayload)) {
96
+ fail('Original .wile/prd.json.original must contain a top-level "stories" array.');
97
+ }
98
+
99
+ const originalStories = parseStories(
100
+ (originalPayload as JsonObject).stories,
101
+ 'Original .wile/prd.json.original must contain a top-level "stories" array.'
102
+ );
103
+
104
+ const originalPending = originalStories.filter(
105
+ (story) => getStoryStatus(story, "Original") === "pending"
106
+ );
107
+ const currentPending = currentStories.filter(
108
+ (story) => getStoryStatus(story, "Compacted") === "pending"
109
+ );
110
+
111
+ if (originalPending.length !== currentPending.length) {
112
+ fail("Compaction must preserve all pending stories.");
113
+ }
114
+
115
+ for (const story of originalPending) {
116
+ const storyId = getStoryId(story, "Original");
117
+ const currentStory = currentPending.find((candidate) => getStoryId(candidate, "Compacted") === storyId);
118
+ if (!currentStory) {
119
+ fail(`Compaction removed pending story ${storyId}.`);
120
+ }
121
+ if (JSON.stringify(currentStory) !== JSON.stringify(story)) {
122
+ fail(`Compaction modified pending story ${storyId}. Pending stories must remain unchanged.`);
123
+ }
124
+ }
125
+ }
126
+
127
+ const progressText = readFileSync(progressPath, "utf8");
128
+ if (!progressText.startsWith("# Wile Progress Log")) {
129
+ fail("Compacted progress log must start with # Wile Progress Log.");
130
+ }
131
+
132
+ if (!progressText.includes("## Codebase Patterns")) {
133
+ fail("Compacted progress log must include ## Codebase Patterns.");
134
+ }
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { existsSync, readFileSync } from "node:fs";
4
+
5
+ type JsonObject = Record<string, unknown>;
6
+ type StoryStatus = "pending" | "done";
7
+
8
+ interface Story {
9
+ id: number;
10
+ title: string;
11
+ description: string;
12
+ acceptanceCriteria: string[];
13
+ dependsOn: number[];
14
+ compactedFrom?: number[];
15
+ status: StoryStatus;
16
+ }
17
+
18
+ const fail = (message: string): never => {
19
+ console.error(message);
20
+ process.exit(1);
21
+ };
22
+
23
+ const isObject = (value: unknown): value is JsonObject =>
24
+ typeof value === "object" && value !== null && !Array.isArray(value);
25
+
26
+ const toObject = (value: unknown, message: string): JsonObject => {
27
+ if (!isObject(value)) {
28
+ fail(message);
29
+ }
30
+ return value as JsonObject;
31
+ };
32
+
33
+ const toInteger = (value: unknown, message: string): number => {
34
+ if (typeof value !== "number" || !Number.isInteger(value)) {
35
+ fail(message);
36
+ }
37
+ return value as number;
38
+ };
39
+
40
+ const toNonEmptyString = (value: unknown, message: string): string => {
41
+ if (typeof value !== "string" || value.trim().length === 0) {
42
+ fail(message);
43
+ }
44
+ return value as string;
45
+ };
46
+
47
+ const toStringArray = (value: unknown, message: string): string[] => {
48
+ if (!Array.isArray(value)) {
49
+ fail(message);
50
+ }
51
+ const values = value as unknown[];
52
+
53
+ const items: string[] = [];
54
+ for (const item of values) {
55
+ if (typeof item !== "string" || item.trim().length === 0) {
56
+ fail(message);
57
+ }
58
+ items.push(item as string);
59
+ }
60
+ return items;
61
+ };
62
+
63
+ const toIntegerArray = (value: unknown, message: string): number[] => {
64
+ if (!Array.isArray(value)) {
65
+ fail(message);
66
+ }
67
+ const values = value as unknown[];
68
+
69
+ const items: number[] = [];
70
+ for (const item of values) {
71
+ if (typeof item !== "number" || !Number.isInteger(item)) {
72
+ fail(message);
73
+ }
74
+ items.push(item as number);
75
+ }
76
+ return items;
77
+ };
78
+
79
+ const parseStory = (raw: unknown, idx: number): Story => {
80
+ const label = `stories[${idx}]`;
81
+ const storyObj = toObject(raw, `${label} must be an object.`);
82
+
83
+ const statusRaw = storyObj.status;
84
+ if (statusRaw !== "pending" && statusRaw !== "done") {
85
+ fail(`${label}.status must be "pending" or "done".`);
86
+ }
87
+
88
+ let compactedFrom: number[] | undefined;
89
+ if (storyObj.compactedFrom !== undefined) {
90
+ compactedFrom = toIntegerArray(
91
+ storyObj.compactedFrom,
92
+ `${label}.compactedFrom must be an array of integer story IDs.`
93
+ );
94
+ if (new Set(compactedFrom).size !== compactedFrom.length) {
95
+ fail(`${label}.compactedFrom must not contain duplicate IDs.`);
96
+ }
97
+ if (statusRaw !== "done") {
98
+ fail(`${label}.compactedFrom is only allowed when status is "done".`);
99
+ }
100
+ }
101
+
102
+ return {
103
+ id: toInteger(storyObj.id, `${label}.id must be an integer number.`),
104
+ title: toNonEmptyString(storyObj.title, `${label}.title must be a non-empty string.`),
105
+ description: toNonEmptyString(
106
+ storyObj.description,
107
+ `${label}.description must be a non-empty string.`
108
+ ),
109
+ acceptanceCriteria: toStringArray(
110
+ storyObj.acceptanceCriteria,
111
+ `${label}.acceptanceCriteria must be an array of non-empty strings.`
112
+ ),
113
+ dependsOn: toIntegerArray(
114
+ storyObj.dependsOn,
115
+ `${label}.dependsOn must be an array of integer story IDs.`
116
+ ),
117
+ compactedFrom,
118
+ status: statusRaw as StoryStatus
119
+ };
120
+ };
121
+
122
+ const findCycle = (stories: Story[]): number[] | null => {
123
+ const edges = new Map<number, number[]>();
124
+ for (const story of stories) {
125
+ edges.set(story.id, story.dependsOn);
126
+ }
127
+
128
+ const visiting = new Set<number>();
129
+ const visited = new Set<number>();
130
+ const path: number[] = [];
131
+
132
+ const dfs = (id: number): number[] | null => {
133
+ if (visiting.has(id)) {
134
+ const start = path.indexOf(id);
135
+ return start === -1 ? [id] : [...path.slice(start), id];
136
+ }
137
+ if (visited.has(id)) {
138
+ return null;
139
+ }
140
+
141
+ visiting.add(id);
142
+ path.push(id);
143
+
144
+ const deps = edges.get(id) ?? [];
145
+ for (const depId of deps) {
146
+ const cycle = dfs(depId);
147
+ if (cycle) {
148
+ return cycle;
149
+ }
150
+ }
151
+
152
+ path.pop();
153
+ visiting.delete(id);
154
+ visited.add(id);
155
+ return null;
156
+ };
157
+
158
+ for (const story of stories) {
159
+ const cycle = dfs(story.id);
160
+ if (cycle) {
161
+ return cycle;
162
+ }
163
+ }
164
+
165
+ return null;
166
+ };
167
+
168
+ const args = process.argv.slice(2);
169
+ let prdPath = ".wile/prd.json";
170
+ let summaryOnly = false;
171
+
172
+ for (let i = 0; i < args.length; i += 1) {
173
+ const arg = args[i];
174
+
175
+ if (arg === "--path") {
176
+ const next = args[i + 1];
177
+ if (!next) {
178
+ fail("Missing value for --path.");
179
+ }
180
+ prdPath = next;
181
+ i += 1;
182
+ continue;
183
+ }
184
+
185
+ if (arg === "--summary") {
186
+ summaryOnly = true;
187
+ continue;
188
+ }
189
+
190
+ fail(`Unknown argument: ${arg}`);
191
+ }
192
+
193
+ if (!existsSync(prdPath)) {
194
+ fail(`Missing ${prdPath}.`);
195
+ }
196
+
197
+ let payload: unknown;
198
+ try {
199
+ payload = JSON.parse(readFileSync(prdPath, "utf8"));
200
+ } catch {
201
+ fail(`${prdPath} is not valid JSON.`);
202
+ }
203
+
204
+ const prdObj = toObject(payload, `${prdPath} must be a JSON object.`);
205
+ const rawStories = prdObj.stories;
206
+ if (!Array.isArray(rawStories)) {
207
+ fail(`${prdPath} must contain a top-level "stories" array.`);
208
+ }
209
+ const storyItems = rawStories as unknown[];
210
+
211
+ const stories: Story[] = storyItems.map((story: unknown, idx: number) => parseStory(story, idx));
212
+ const storyById = new Map<number, Story>();
213
+ for (const story of stories) {
214
+ if (storyById.has(story.id)) {
215
+ fail(`Duplicate story id detected: ${story.id}.`);
216
+ }
217
+ storyById.set(story.id, story);
218
+ }
219
+
220
+ const compactedById = new Map<number, number>();
221
+ for (const story of stories) {
222
+ for (const compactedId of story.compactedFrom ?? []) {
223
+ const existingOwner = compactedById.get(compactedId);
224
+ if (existingOwner !== undefined) {
225
+ fail(
226
+ `Compacted story id ${compactedId} is listed multiple times (stories ${existingOwner} and ${story.id}).`
227
+ );
228
+ }
229
+ compactedById.set(compactedId, story.id);
230
+ }
231
+ }
232
+
233
+ for (const [compactedId, ownerStoryId] of compactedById) {
234
+ if (storyById.has(compactedId)) {
235
+ fail(`Story id ${compactedId} is reserved by compactedFrom in story ${ownerStoryId}.`);
236
+ }
237
+ }
238
+
239
+ for (const story of stories) {
240
+ for (const depId of story.dependsOn) {
241
+ const compactedOwner = compactedById.get(depId);
242
+ if (compactedOwner !== undefined) {
243
+ fail(
244
+ `Story ${story.id} depends on compacted story id ${depId} (compacted in story ${compactedOwner}).`
245
+ );
246
+ }
247
+
248
+ if (!storyById.has(depId)) {
249
+ fail(`Story ${story.id} depends on missing story id ${depId}.`);
250
+ }
251
+ }
252
+ }
253
+
254
+ const cycle = findCycle(stories);
255
+ if (cycle) {
256
+ fail(`Dependency cycle detected: ${cycle.join(" -> ")}.`);
257
+ }
258
+
259
+ const pendingStories = stories.filter((story) => story.status === "pending");
260
+ const runnableStory =
261
+ stories.find(
262
+ (story) =>
263
+ story.status === "pending" &&
264
+ story.dependsOn.every((depId) => storyById.get(depId)?.status === "done")
265
+ ) ?? null;
266
+
267
+ if (pendingStories.length > 0 && !runnableStory) {
268
+ const blocked = pendingStories.map((story) => story.id).join(", ");
269
+ fail(`No runnable pending stories in ${prdPath}. Pending stories are blocked: ${blocked}.`);
270
+ }
271
+
272
+ if (summaryOnly) {
273
+ console.log(
274
+ JSON.stringify({
275
+ pendingCount: pendingStories.length,
276
+ runnableStoryId: runnableStory ? runnableStory.id : null,
277
+ allDone: pendingStories.length === 0
278
+ })
279
+ );
280
+ }
@@ -62,7 +62,7 @@ run_claude() {
62
62
  local prompt_path="$1"
63
63
  cat "$prompt_path" \
64
64
  | claude --model "$CLAUDE_MODEL" --print --output-format stream-json --verbose --dangerously-skip-permissions \
65
- | node "$SCRIPT_DIR/claude-stream.js"
65
+ | bun "$SCRIPT_DIR/claude-stream.ts"
66
66
  }
67
67
 
68
68
  run_opencode() {
@@ -73,7 +73,7 @@ run_opencode() {
73
73
  fi
74
74
  cat "$prompt_path" \
75
75
  | opencode run --format json --model "$model_arg" \
76
- | node "$SCRIPT_DIR/opencode-stream.js"
76
+ | bun "$SCRIPT_DIR/opencode-stream.ts"
77
77
  }
78
78
 
79
79
  run_gemini() {
@@ -85,7 +85,7 @@ run_gemini() {
85
85
  model_args=(--model "$GEMINI_MODEL")
86
86
  fi
87
87
  gemini --output-format stream-json --yolo "${model_args[@]}" "$prompt_text" \
88
- | node "$SCRIPT_DIR/gemini-stream.js"
88
+ | bun "$SCRIPT_DIR/gemini-stream.ts"
89
89
  }
90
90
 
91
91
  run_codex() {
@@ -96,7 +96,7 @@ run_codex() {
96
96
  fi
97
97
  cat "$prompt_path" \
98
98
  | codex exec --json --dangerously-bypass-approvals-and-sandbox "${model_args[@]}" - \
99
- | node "$SCRIPT_DIR/codex-stream.js"
99
+ | bun "$SCRIPT_DIR/codex-stream.ts"
100
100
  }
101
101
 
102
102
  run_agent() {
@@ -112,7 +112,7 @@ run_agent() {
112
112
  fi
113
113
  }
114
114
 
115
- # Snapshot original PRD for validation of passes:false preservation.
115
+ # Snapshot original PRD for validation of pending-story preservation.
116
116
  if [ -f ".wile/prd.json" ]; then
117
117
  cp ".wile/prd.json" ".wile/prd.json.original"
118
118
  fi
@@ -121,7 +121,7 @@ echo "Running compact agent..."
121
121
  OUTPUT=$(run_agent "$PROMPT_FILE" | tee "$TEE_TARGET") || true
122
122
 
123
123
  # Validate the resulting files instead of the response format.
124
- node "$SCRIPT_DIR/validate-compact.js"
124
+ bun "$SCRIPT_DIR/validate-compact.ts"
125
125
 
126
126
  if [ -f ".wile/prd.json.original" ]; then
127
127
  rm -f ".wile/prd.json.original"