wile 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/dist/agent/.env.example +9 -11
- package/dist/agent/README.md +5 -6
- package/dist/agent/entrypoint.sh +1 -22
- package/dist/agent/scripts/mock-claude.sh +85 -10
- package/dist/agent/scripts/mock-codex.sh +85 -10
- package/dist/agent/scripts/mock-gemini.sh +85 -10
- package/dist/agent/scripts/prompt-compact.md +3 -2
- package/dist/agent/scripts/validate-prd.ts +111 -21
- package/dist/agent/scripts/wile-compact.sh +5 -16
- package/dist/agent/scripts/wile-preflight.sh +5 -16
- package/dist/agent/scripts/wile.sh +5 -16
- package/dist/cli.js +533 -116
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,6 +16,19 @@ From your project repo:
|
|
|
16
16
|
bunx wile config
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
For non-interactive setup docs (useful for agents creating `.wile/secrets/.env` directly):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bunx wile config --non-interactive
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Apply non-interactive config (agent mode):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
export WILE_PROMPTS_INJECT='{"codingAgent":"OC","repoSource":"local","ocModel":"opencode/kimi-k2.5-free","branchName":"main","envProjectPath":".wile/.env.project","maxIterations":25}'
|
|
29
|
+
bunx wile config --non-interactive "$WILE_PROMPTS_INJECT"
|
|
30
|
+
```
|
|
31
|
+
|
|
19
32
|
This creates:
|
|
20
33
|
|
|
21
34
|
- `.wile/secrets/.env` for required credentials
|
|
@@ -26,7 +39,7 @@ This creates:
|
|
|
26
39
|
Set `WILE_REPO_SOURCE=local` in `.wile/secrets/.env` to run against the current directory without GitHub.
|
|
27
40
|
When `WILE_REPO_SOURCE=local`, GitHub credentials are optional.
|
|
28
41
|
Set `WILE_MAX_ITERATIONS` in `.wile/secrets/.env` to change the default loop limit (default: 25).
|
|
29
|
-
Set `CODING_AGENT=CX` to use Codex CLI, `CODING_AGENT=OC` to use OpenCode (
|
|
42
|
+
Set `CODING_AGENT=CX` to use Codex CLI, `CODING_AGENT=OC` to use OpenCode (free native models), `CODING_AGENT=GC` to use Gemini CLI, otherwise `CODING_AGENT=CC` uses Claude Code.
|
|
30
43
|
|
|
31
44
|
## Run Wile
|
|
32
45
|
|
package/dist/agent/.env.example
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# Choose which coding agent to run:
|
|
9
9
|
# - CC = Claude Code (default)
|
|
10
10
|
# - GC = Gemini CLI (Google account OAuth)
|
|
11
|
-
# - OC = OpenCode (
|
|
11
|
+
# - OC = OpenCode (free native models)
|
|
12
12
|
# - CX = Codex CLI (ChatGPT subscription)
|
|
13
13
|
CODING_AGENT=CC
|
|
14
14
|
|
|
@@ -30,10 +30,8 @@ CC_CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-xxxxx
|
|
|
30
30
|
# REQUIRED - OpenCode Authentication (OC only)
|
|
31
31
|
# =============================================================================
|
|
32
32
|
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# OpenRouter model id (Wile maps `glm-4.7` to `openrouter/z-ai/glm-4.7`)
|
|
36
|
-
# OC_MODEL=glm-4.7
|
|
33
|
+
# Native model id (default: opencode/kimi-k2.5-free)
|
|
34
|
+
# OC_MODEL=opencode/kimi-k2.5-free
|
|
37
35
|
|
|
38
36
|
# =============================================================================
|
|
39
37
|
# REQUIRED - Gemini CLI Authentication (GC only, choose ONE)
|
|
@@ -91,14 +89,14 @@ BRANCH_NAME=feature/my-feature
|
|
|
91
89
|
# Maximum loop iterations before stopping (default: 25)
|
|
92
90
|
MAX_ITERATIONS=25
|
|
93
91
|
|
|
94
|
-
# Claude model alias or full model name (default:
|
|
95
|
-
CC_CLAUDE_MODEL=
|
|
92
|
+
# Claude model alias or full model name (default: opus)
|
|
93
|
+
CC_CLAUDE_MODEL=opus
|
|
96
94
|
|
|
97
|
-
# Gemini model name (default:
|
|
98
|
-
# GEMINI_MODEL=
|
|
95
|
+
# Gemini model name (default: gemini-3-pro)
|
|
96
|
+
# GEMINI_MODEL=gemini-3-pro
|
|
99
97
|
|
|
100
|
-
# Codex model name (
|
|
101
|
-
# CODEX_MODEL=gpt-5.
|
|
98
|
+
# Codex model name (default: gpt-5.3-codex)
|
|
99
|
+
# CODEX_MODEL=gpt-5.3-codex
|
|
102
100
|
|
|
103
101
|
# =============================================================================
|
|
104
102
|
# PROJECT ENVIRONMENT VARIABLES
|
package/dist/agent/README.md
CHANGED
|
@@ -110,23 +110,22 @@ For UI work, tell the agent how to verify:
|
|
|
110
110
|
|
|
111
111
|
| Variable | Required | Description |
|
|
112
112
|
|----------|----------|-------------|
|
|
113
|
-
| `CODING_AGENT` | No | `CC` (Claude Code, default), `GC` (Gemini CLI), `OC` (OpenCode
|
|
113
|
+
| `CODING_AGENT` | No | `CC` (Claude Code, default), `GC` (Gemini CLI), `OC` (OpenCode native/free models), or `CX` (Codex CLI) |
|
|
114
114
|
| `CC_CLAUDE_CODE_OAUTH_TOKEN` | Yes* | OAuth token from `claude setup-token` (uses Pro/Max subscription) |
|
|
115
115
|
| `CC_ANTHROPIC_API_KEY` | Yes* | API key (uses API credits - alternative to OAuth) |
|
|
116
116
|
| `GEMINI_OAUTH_CREDS_B64` | Yes (GC)* | Base64 OAuth creds from `~/.gemini/oauth_creds.json` (uses Google account) |
|
|
117
117
|
| `GEMINI_API_KEY` | Yes (GC)* | Gemini API key (uses API credits - alternative to OAuth) |
|
|
118
|
-
| `
|
|
119
|
-
| `OC_MODEL` | Yes (OC) | OpenRouter model id (set `glm-4.7` to target `openrouter/z-ai/glm-4.7`) |
|
|
118
|
+
| `OC_MODEL` | Yes (OC) | OpenCode native model id (default: `opencode/kimi-k2.5-free`) |
|
|
120
119
|
| `CODEX_AUTH_JSON_B64` | Yes (CX)* | Base64 of `~/.codex/auth.json` from `codex login` (uses ChatGPT subscription) |
|
|
121
120
|
| `CODEX_API_KEY` | Yes (CX)* | OpenAI API key (uses API credits - alternative to auth.json) |
|
|
122
|
-
| `GEMINI_MODEL` | No (GC) | Gemini model name (default: `
|
|
121
|
+
| `GEMINI_MODEL` | No (GC) | Gemini model name (default: `gemini-3-pro-preview`) |
|
|
123
122
|
| `WILE_REPO_SOURCE` | No | `github` (default) or `local` |
|
|
124
123
|
| `GITHUB_TOKEN` | Yes (github) | GitHub PAT with repo access |
|
|
125
124
|
| `GITHUB_REPO_URL` | Yes (github) | HTTPS URL to repository |
|
|
126
125
|
| `BRANCH_NAME` | Yes (github) | Branch to work on |
|
|
127
126
|
| `MAX_ITERATIONS` | No | Max loops (default: 25) |
|
|
128
|
-
| `CC_CLAUDE_MODEL` | No | Claude model alias/name (default:
|
|
129
|
-
| `CODEX_MODEL` | No (CX) | Codex model name (
|
|
127
|
+
| `CC_CLAUDE_MODEL` | No | Claude model alias/name (default: `opus`) |
|
|
128
|
+
| `CODEX_MODEL` | No (CX) | Codex model name (default: `gpt-5.3-codex`) |
|
|
130
129
|
|
|
131
130
|
*Either `CC_CLAUDE_CODE_OAUTH_TOKEN` or `CC_ANTHROPIC_API_KEY` is required when `CODING_AGENT=CC`.
|
|
132
131
|
*Either `GEMINI_OAUTH_CREDS_B64` or `GEMINI_API_KEY` is required when `CODING_AGENT=GC`.
|
package/dist/agent/entrypoint.sh
CHANGED
|
@@ -105,11 +105,6 @@ fi
|
|
|
105
105
|
|
|
106
106
|
# Authentication for selected coding agent
|
|
107
107
|
if [ "$CODING_AGENT" = "OC" ]; then
|
|
108
|
-
OC_PROVIDER="${OC_PROVIDER:-native}"
|
|
109
|
-
if [ "$OC_PROVIDER" = "openrouter" ] && [ -z "$OC_OPENROUTER_API_KEY" ]; then
|
|
110
|
-
echo "ERROR: OC_OPENROUTER_API_KEY is required for OpenCode with OpenRouter provider"
|
|
111
|
-
exit 1
|
|
112
|
-
fi
|
|
113
108
|
if [ -z "$OC_MODEL" ]; then
|
|
114
109
|
echo "ERROR: OC_MODEL is required for OpenCode"
|
|
115
110
|
exit 1
|
|
@@ -177,23 +172,7 @@ if [ "${WILE_MOCK_CODEX:-}" = "true" ] && [ "$CODING_AGENT" = "CX" ]; then
|
|
|
177
172
|
fi
|
|
178
173
|
|
|
179
174
|
if [ "$CODING_AGENT" = "OC" ]; then
|
|
180
|
-
|
|
181
|
-
echo " Auth: OpenRouter (OpenCode)"
|
|
182
|
-
OPENCODE_DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/opencode"
|
|
183
|
-
mkdir -p "$OPENCODE_DATA_DIR"
|
|
184
|
-
cat > "$OPENCODE_DATA_DIR/auth.json" << OPENCODEAUTH
|
|
185
|
-
{
|
|
186
|
-
"openrouter": {
|
|
187
|
-
"type": "api",
|
|
188
|
-
"key": "$OC_OPENROUTER_API_KEY"
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
OPENCODEAUTH
|
|
192
|
-
chmod 600 "$OPENCODE_DATA_DIR/auth.json"
|
|
193
|
-
export OPENROUTER_API_KEY="$OC_OPENROUTER_API_KEY"
|
|
194
|
-
else
|
|
195
|
-
echo " Auth: Native (free models)"
|
|
196
|
-
fi
|
|
175
|
+
echo " Auth: Native (free models)"
|
|
197
176
|
elif [ "$CODING_AGENT" = "GC" ]; then
|
|
198
177
|
if [ -n "$GEMINI_OAUTH_CREDS_B64" ] || [ -n "$GEMINI_OAUTH_CREDS_PATH" ]; then
|
|
199
178
|
echo " Auth: OAuth (Google account)"
|
|
@@ -11,6 +11,79 @@ const prd = JSON.parse(fs.readFileSync(prdPath, "utf8"));
|
|
|
11
11
|
const stories = Array.isArray(prd.stories) ? prd.stories : [];
|
|
12
12
|
const pendingStories = stories.filter((story) => story.status === "pending");
|
|
13
13
|
const doneStories = stories.filter((story) => story.status === "done");
|
|
14
|
+
const parseCompactedFromRanges = (value) => {
|
|
15
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ranges = [];
|
|
20
|
+
const tokens = value.split(",").map((token) => token.trim()).filter(Boolean);
|
|
21
|
+
for (const token of tokens) {
|
|
22
|
+
const match = token.match(/^(-?\d+)(?:\.\.(-?\d+))?$/);
|
|
23
|
+
if (!match) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const start = Number(match[1]);
|
|
28
|
+
const end = match[2] === undefined ? start : Number(match[2]);
|
|
29
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start > end) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
ranges.push({ start, end });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
ranges.sort((a, b) => (a.start === b.start ? a.end - b.end : a.start - b.start));
|
|
37
|
+
const merged = [];
|
|
38
|
+
for (const range of ranges) {
|
|
39
|
+
const last = merged[merged.length - 1];
|
|
40
|
+
if (!last || range.start > last.end + 1) {
|
|
41
|
+
merged.push({ ...range });
|
|
42
|
+
} else if (range.end > last.end) {
|
|
43
|
+
last.end = range.end;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return merged;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const compactedIdsFromStory = (story) => {
|
|
51
|
+
const ids = [];
|
|
52
|
+
for (const range of parseCompactedFromRanges(story.compactedFrom)) {
|
|
53
|
+
for (let id = range.start; id <= range.end; id += 1) {
|
|
54
|
+
ids.push(id);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return ids;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const compactedFromStringFromIds = (ids) => {
|
|
61
|
+
const sorted = [...new Set(ids)].sort((a, b) => a - b);
|
|
62
|
+
if (sorted.length === 0) {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const ranges = [];
|
|
67
|
+
let start = sorted[0];
|
|
68
|
+
let end = sorted[0];
|
|
69
|
+
for (let i = 1; i < sorted.length; i += 1) {
|
|
70
|
+
const id = sorted[i];
|
|
71
|
+
if (id === end + 1) {
|
|
72
|
+
end = id;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
ranges.push({ start, end });
|
|
77
|
+
start = id;
|
|
78
|
+
end = id;
|
|
79
|
+
}
|
|
80
|
+
ranges.push({ start, end });
|
|
81
|
+
|
|
82
|
+
return ranges
|
|
83
|
+
.map((range) => (range.start === range.end ? `${range.start}` : `${range.start}..${range.end}`))
|
|
84
|
+
.join(",");
|
|
85
|
+
};
|
|
86
|
+
|
|
14
87
|
const requiredDoneIds = new Set();
|
|
15
88
|
for (const story of pendingStories) {
|
|
16
89
|
const deps = Array.isArray(story.dependsOn) ? story.dependsOn : [];
|
|
@@ -20,25 +93,27 @@ for (const story of pendingStories) {
|
|
|
20
93
|
}
|
|
21
94
|
const retainedDoneStories = doneStories.filter((story) => requiredDoneIds.has(story.id));
|
|
22
95
|
const compactableDoneStories = doneStories.filter((story) => !requiredDoneIds.has(story.id));
|
|
23
|
-
|
|
96
|
+
|
|
97
|
+
let maxReservedId = Math.max(0, ...stories.map((story) => (Number.isInteger(story.id) ? story.id : 0)));
|
|
24
98
|
for (const story of stories) {
|
|
25
|
-
const
|
|
26
|
-
for (const
|
|
27
|
-
|
|
99
|
+
const ranges = parseCompactedFromRanges(story.compactedFrom);
|
|
100
|
+
for (const range of ranges) {
|
|
101
|
+
if (range.end > maxReservedId) {
|
|
102
|
+
maxReservedId = range.end;
|
|
103
|
+
}
|
|
28
104
|
}
|
|
29
105
|
}
|
|
30
|
-
const summaryId =
|
|
106
|
+
const summaryId = maxReservedId + 1;
|
|
31
107
|
|
|
32
108
|
const nextStories = [...pendingStories, ...retainedDoneStories];
|
|
33
109
|
if (compactableDoneStories.length > 0) {
|
|
34
|
-
const
|
|
110
|
+
const compactedIds = [
|
|
35
111
|
...new Set(
|
|
36
112
|
compactableDoneStories.flatMap((story) => {
|
|
37
|
-
|
|
38
|
-
return [story.id, ...priorCompacted];
|
|
113
|
+
return [story.id, ...compactedIdsFromStory(story)];
|
|
39
114
|
})
|
|
40
115
|
)
|
|
41
|
-
]
|
|
116
|
+
];
|
|
42
117
|
|
|
43
118
|
nextStories.push({
|
|
44
119
|
id: summaryId,
|
|
@@ -49,7 +124,7 @@ if (compactableDoneStories.length > 0) {
|
|
|
49
124
|
"Pending stories were preserved unchanged."
|
|
50
125
|
],
|
|
51
126
|
dependsOn: [],
|
|
52
|
-
compactedFrom,
|
|
127
|
+
compactedFrom: compactedFromStringFromIds(compactedIds),
|
|
53
128
|
status: "done"
|
|
54
129
|
});
|
|
55
130
|
}
|
|
@@ -11,6 +11,79 @@ const prd = JSON.parse(fs.readFileSync(prdPath, "utf8"));
|
|
|
11
11
|
const stories = Array.isArray(prd.stories) ? prd.stories : [];
|
|
12
12
|
const pendingStories = stories.filter((story) => story.status === "pending");
|
|
13
13
|
const doneStories = stories.filter((story) => story.status === "done");
|
|
14
|
+
const parseCompactedFromRanges = (value) => {
|
|
15
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ranges = [];
|
|
20
|
+
const tokens = value.split(",").map((token) => token.trim()).filter(Boolean);
|
|
21
|
+
for (const token of tokens) {
|
|
22
|
+
const match = token.match(/^(-?\d+)(?:\.\.(-?\d+))?$/);
|
|
23
|
+
if (!match) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const start = Number(match[1]);
|
|
28
|
+
const end = match[2] === undefined ? start : Number(match[2]);
|
|
29
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start > end) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
ranges.push({ start, end });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
ranges.sort((a, b) => (a.start === b.start ? a.end - b.end : a.start - b.start));
|
|
37
|
+
const merged = [];
|
|
38
|
+
for (const range of ranges) {
|
|
39
|
+
const last = merged[merged.length - 1];
|
|
40
|
+
if (!last || range.start > last.end + 1) {
|
|
41
|
+
merged.push({ ...range });
|
|
42
|
+
} else if (range.end > last.end) {
|
|
43
|
+
last.end = range.end;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return merged;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const compactedIdsFromStory = (story) => {
|
|
51
|
+
const ids = [];
|
|
52
|
+
for (const range of parseCompactedFromRanges(story.compactedFrom)) {
|
|
53
|
+
for (let id = range.start; id <= range.end; id += 1) {
|
|
54
|
+
ids.push(id);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return ids;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const compactedFromStringFromIds = (ids) => {
|
|
61
|
+
const sorted = [...new Set(ids)].sort((a, b) => a - b);
|
|
62
|
+
if (sorted.length === 0) {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const ranges = [];
|
|
67
|
+
let start = sorted[0];
|
|
68
|
+
let end = sorted[0];
|
|
69
|
+
for (let i = 1; i < sorted.length; i += 1) {
|
|
70
|
+
const id = sorted[i];
|
|
71
|
+
if (id === end + 1) {
|
|
72
|
+
end = id;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
ranges.push({ start, end });
|
|
77
|
+
start = id;
|
|
78
|
+
end = id;
|
|
79
|
+
}
|
|
80
|
+
ranges.push({ start, end });
|
|
81
|
+
|
|
82
|
+
return ranges
|
|
83
|
+
.map((range) => (range.start === range.end ? `${range.start}` : `${range.start}..${range.end}`))
|
|
84
|
+
.join(",");
|
|
85
|
+
};
|
|
86
|
+
|
|
14
87
|
const requiredDoneIds = new Set();
|
|
15
88
|
for (const story of pendingStories) {
|
|
16
89
|
const deps = Array.isArray(story.dependsOn) ? story.dependsOn : [];
|
|
@@ -20,25 +93,27 @@ for (const story of pendingStories) {
|
|
|
20
93
|
}
|
|
21
94
|
const retainedDoneStories = doneStories.filter((story) => requiredDoneIds.has(story.id));
|
|
22
95
|
const compactableDoneStories = doneStories.filter((story) => !requiredDoneIds.has(story.id));
|
|
23
|
-
|
|
96
|
+
|
|
97
|
+
let maxReservedId = Math.max(0, ...stories.map((story) => (Number.isInteger(story.id) ? story.id : 0)));
|
|
24
98
|
for (const story of stories) {
|
|
25
|
-
const
|
|
26
|
-
for (const
|
|
27
|
-
|
|
99
|
+
const ranges = parseCompactedFromRanges(story.compactedFrom);
|
|
100
|
+
for (const range of ranges) {
|
|
101
|
+
if (range.end > maxReservedId) {
|
|
102
|
+
maxReservedId = range.end;
|
|
103
|
+
}
|
|
28
104
|
}
|
|
29
105
|
}
|
|
30
|
-
const summaryId =
|
|
106
|
+
const summaryId = maxReservedId + 1;
|
|
31
107
|
|
|
32
108
|
const nextStories = [...pendingStories, ...retainedDoneStories];
|
|
33
109
|
if (compactableDoneStories.length > 0) {
|
|
34
|
-
const
|
|
110
|
+
const compactedIds = [
|
|
35
111
|
...new Set(
|
|
36
112
|
compactableDoneStories.flatMap((story) => {
|
|
37
|
-
|
|
38
|
-
return [story.id, ...priorCompacted];
|
|
113
|
+
return [story.id, ...compactedIdsFromStory(story)];
|
|
39
114
|
})
|
|
40
115
|
)
|
|
41
|
-
]
|
|
116
|
+
];
|
|
42
117
|
|
|
43
118
|
nextStories.push({
|
|
44
119
|
id: summaryId,
|
|
@@ -49,7 +124,7 @@ if (compactableDoneStories.length > 0) {
|
|
|
49
124
|
"Pending stories were preserved unchanged."
|
|
50
125
|
],
|
|
51
126
|
dependsOn: [],
|
|
52
|
-
compactedFrom,
|
|
127
|
+
compactedFrom: compactedFromStringFromIds(compactedIds),
|
|
53
128
|
status: "done"
|
|
54
129
|
});
|
|
55
130
|
}
|
|
@@ -11,6 +11,79 @@ const prd = JSON.parse(fs.readFileSync(prdPath, "utf8"));
|
|
|
11
11
|
const stories = Array.isArray(prd.stories) ? prd.stories : [];
|
|
12
12
|
const pendingStories = stories.filter((story) => story.status === "pending");
|
|
13
13
|
const doneStories = stories.filter((story) => story.status === "done");
|
|
14
|
+
const parseCompactedFromRanges = (value) => {
|
|
15
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ranges = [];
|
|
20
|
+
const tokens = value.split(",").map((token) => token.trim()).filter(Boolean);
|
|
21
|
+
for (const token of tokens) {
|
|
22
|
+
const match = token.match(/^(-?\d+)(?:\.\.(-?\d+))?$/);
|
|
23
|
+
if (!match) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const start = Number(match[1]);
|
|
28
|
+
const end = match[2] === undefined ? start : Number(match[2]);
|
|
29
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start > end) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
ranges.push({ start, end });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
ranges.sort((a, b) => (a.start === b.start ? a.end - b.end : a.start - b.start));
|
|
37
|
+
const merged = [];
|
|
38
|
+
for (const range of ranges) {
|
|
39
|
+
const last = merged[merged.length - 1];
|
|
40
|
+
if (!last || range.start > last.end + 1) {
|
|
41
|
+
merged.push({ ...range });
|
|
42
|
+
} else if (range.end > last.end) {
|
|
43
|
+
last.end = range.end;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return merged;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const compactedIdsFromStory = (story) => {
|
|
51
|
+
const ids = [];
|
|
52
|
+
for (const range of parseCompactedFromRanges(story.compactedFrom)) {
|
|
53
|
+
for (let id = range.start; id <= range.end; id += 1) {
|
|
54
|
+
ids.push(id);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return ids;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const compactedFromStringFromIds = (ids) => {
|
|
61
|
+
const sorted = [...new Set(ids)].sort((a, b) => a - b);
|
|
62
|
+
if (sorted.length === 0) {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const ranges = [];
|
|
67
|
+
let start = sorted[0];
|
|
68
|
+
let end = sorted[0];
|
|
69
|
+
for (let i = 1; i < sorted.length; i += 1) {
|
|
70
|
+
const id = sorted[i];
|
|
71
|
+
if (id === end + 1) {
|
|
72
|
+
end = id;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
ranges.push({ start, end });
|
|
77
|
+
start = id;
|
|
78
|
+
end = id;
|
|
79
|
+
}
|
|
80
|
+
ranges.push({ start, end });
|
|
81
|
+
|
|
82
|
+
return ranges
|
|
83
|
+
.map((range) => (range.start === range.end ? `${range.start}` : `${range.start}..${range.end}`))
|
|
84
|
+
.join(",");
|
|
85
|
+
};
|
|
86
|
+
|
|
14
87
|
const requiredDoneIds = new Set();
|
|
15
88
|
for (const story of pendingStories) {
|
|
16
89
|
const deps = Array.isArray(story.dependsOn) ? story.dependsOn : [];
|
|
@@ -20,25 +93,27 @@ for (const story of pendingStories) {
|
|
|
20
93
|
}
|
|
21
94
|
const retainedDoneStories = doneStories.filter((story) => requiredDoneIds.has(story.id));
|
|
22
95
|
const compactableDoneStories = doneStories.filter((story) => !requiredDoneIds.has(story.id));
|
|
23
|
-
|
|
96
|
+
|
|
97
|
+
let maxReservedId = Math.max(0, ...stories.map((story) => (Number.isInteger(story.id) ? story.id : 0)));
|
|
24
98
|
for (const story of stories) {
|
|
25
|
-
const
|
|
26
|
-
for (const
|
|
27
|
-
|
|
99
|
+
const ranges = parseCompactedFromRanges(story.compactedFrom);
|
|
100
|
+
for (const range of ranges) {
|
|
101
|
+
if (range.end > maxReservedId) {
|
|
102
|
+
maxReservedId = range.end;
|
|
103
|
+
}
|
|
28
104
|
}
|
|
29
105
|
}
|
|
30
|
-
const summaryId =
|
|
106
|
+
const summaryId = maxReservedId + 1;
|
|
31
107
|
|
|
32
108
|
const nextStories = [...pendingStories, ...retainedDoneStories];
|
|
33
109
|
if (compactableDoneStories.length > 0) {
|
|
34
|
-
const
|
|
110
|
+
const compactedIds = [
|
|
35
111
|
...new Set(
|
|
36
112
|
compactableDoneStories.flatMap((story) => {
|
|
37
|
-
|
|
38
|
-
return [story.id, ...priorCompacted];
|
|
113
|
+
return [story.id, ...compactedIdsFromStory(story)];
|
|
39
114
|
})
|
|
40
115
|
)
|
|
41
|
-
]
|
|
116
|
+
];
|
|
42
117
|
|
|
43
118
|
nextStories.push({
|
|
44
119
|
id: summaryId,
|
|
@@ -49,7 +124,7 @@ if (compactableDoneStories.length > 0) {
|
|
|
49
124
|
"Pending stories were preserved unchanged."
|
|
50
125
|
],
|
|
51
126
|
dependsOn: [],
|
|
52
|
-
compactedFrom,
|
|
127
|
+
compactedFrom: compactedFromStringFromIds(compactedIds),
|
|
53
128
|
status: "done"
|
|
54
129
|
});
|
|
55
130
|
}
|
|
@@ -5,6 +5,7 @@ Goal: compact both `.wile/prd.json` and `.wile/progress.txt` while preserving co
|
|
|
5
5
|
Rules:
|
|
6
6
|
- Final `.wile/prd.json` must stay valid under the current schema (`stories`, numeric `id`, `dependsOn`, `status`).
|
|
7
7
|
- Never reuse a compacted story ID. Any ID listed in any `compactedFrom` is permanently reserved.
|
|
8
|
+
- `compactedFrom` must use canonical range-string syntax like `1..3,5` (sorted, non-overlapping, merged).
|
|
8
9
|
- Keep every pending story exactly as-is.
|
|
9
10
|
- Do not create missing dependencies.
|
|
10
11
|
|
|
@@ -18,12 +19,12 @@ Steps:
|
|
|
18
19
|
- Keep all `pendingStories` exactly as-is.
|
|
19
20
|
- Keep all done stories whose id is in `requiredDoneIds` exactly as-is.
|
|
20
21
|
- For remaining done stories, replace them with one summary done story:
|
|
21
|
-
- `id`: next available integer id greater than every story `id` and every
|
|
22
|
+
- `id`: next available integer id greater than every story `id` and every id covered by any `compactedFrom` ranges
|
|
22
23
|
- `title`: `[COMPACT] Completed stories summary`
|
|
23
24
|
- `description`: concise summary of shipped work
|
|
24
25
|
- `acceptanceCriteria`: a short list (1-3 bullets) describing what was completed
|
|
25
26
|
- `dependsOn`: []
|
|
26
|
-
- `compactedFrom`:
|
|
27
|
+
- `compactedFrom`: canonical range-string of all compacted story IDs (example: `1..3,5`)
|
|
27
28
|
- Include each replaced done story `id`
|
|
28
29
|
- If a replaced story already has `compactedFrom`, include those IDs too (preserve tombstones transitively)
|
|
29
30
|
- `status`: `"done"`
|