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 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 (OpenRouter), `CODING_AGENT=GC` to use Gemini CLI, otherwise `CODING_AGENT=CC` uses Claude Code.
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
 
@@ -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 (OpenRouter)
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
- # OpenRouter API key (used with OpenCode)
34
- # OC_OPENROUTER_API_KEY=sk-or-xxxxx
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: sonnet)
95
- CC_CLAUDE_MODEL=sonnet
92
+ # Claude model alias or full model name (default: opus)
93
+ CC_CLAUDE_MODEL=opus
96
94
 
97
- # Gemini model name (default: auto-gemini-3)
98
- # GEMINI_MODEL=auto-gemini-3
95
+ # Gemini model name (default: gemini-3-pro)
96
+ # GEMINI_MODEL=gemini-3-pro
99
97
 
100
- # Codex model name (defaults to Codex CLI default)
101
- # CODEX_MODEL=gpt-5.1-codex
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
@@ -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 via OpenRouter), or `CX` (Codex CLI) |
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
- | `OC_OPENROUTER_API_KEY` | Yes (OC) | OpenRouter API key for OpenCode |
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: `auto-gemini-3`) |
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: sonnet) |
129
- | `CODEX_MODEL` | No (CX) | Codex model name (defaults to Codex CLI default) |
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`.
@@ -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
- if [ "$OC_PROVIDER" = "openrouter" ]; then
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
- const reservedIds = new Set(stories.map((story) => story.id));
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 priorCompacted = Array.isArray(story.compactedFrom) ? story.compactedFrom : [];
26
- for (const compactedId of priorCompacted) {
27
- reservedIds.add(compactedId);
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 = Math.max(0, ...reservedIds) + 1;
106
+ const summaryId = maxReservedId + 1;
31
107
 
32
108
  const nextStories = [...pendingStories, ...retainedDoneStories];
33
109
  if (compactableDoneStories.length > 0) {
34
- const compactedFrom = [
110
+ const compactedIds = [
35
111
  ...new Set(
36
112
  compactableDoneStories.flatMap((story) => {
37
- const priorCompacted = Array.isArray(story.compactedFrom) ? story.compactedFrom : [];
38
- return [story.id, ...priorCompacted];
113
+ return [story.id, ...compactedIdsFromStory(story)];
39
114
  })
40
115
  )
41
- ].sort((a, b) => a - b);
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
- const reservedIds = new Set(stories.map((story) => story.id));
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 priorCompacted = Array.isArray(story.compactedFrom) ? story.compactedFrom : [];
26
- for (const compactedId of priorCompacted) {
27
- reservedIds.add(compactedId);
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 = Math.max(0, ...reservedIds) + 1;
106
+ const summaryId = maxReservedId + 1;
31
107
 
32
108
  const nextStories = [...pendingStories, ...retainedDoneStories];
33
109
  if (compactableDoneStories.length > 0) {
34
- const compactedFrom = [
110
+ const compactedIds = [
35
111
  ...new Set(
36
112
  compactableDoneStories.flatMap((story) => {
37
- const priorCompacted = Array.isArray(story.compactedFrom) ? story.compactedFrom : [];
38
- return [story.id, ...priorCompacted];
113
+ return [story.id, ...compactedIdsFromStory(story)];
39
114
  })
40
115
  )
41
- ].sort((a, b) => a - b);
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
- const reservedIds = new Set(stories.map((story) => story.id));
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 priorCompacted = Array.isArray(story.compactedFrom) ? story.compactedFrom : [];
26
- for (const compactedId of priorCompacted) {
27
- reservedIds.add(compactedId);
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 = Math.max(0, ...reservedIds) + 1;
106
+ const summaryId = maxReservedId + 1;
31
107
 
32
108
  const nextStories = [...pendingStories, ...retainedDoneStories];
33
109
  if (compactableDoneStories.length > 0) {
34
- const compactedFrom = [
110
+ const compactedIds = [
35
111
  ...new Set(
36
112
  compactableDoneStories.flatMap((story) => {
37
- const priorCompacted = Array.isArray(story.compactedFrom) ? story.compactedFrom : [];
38
- return [story.id, ...priorCompacted];
113
+ return [story.id, ...compactedIdsFromStory(story)];
39
114
  })
40
115
  )
41
- ].sort((a, b) => a - b);
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 value in every `compactedFrom` array
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`: sorted unique list of all compacted story IDs
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"`