@afokapu/atdd-bun 0.10.5 → 0.10.7

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
@@ -171,6 +171,10 @@ From 0.10.5 only coordinators and drivers use the board: a writer or reviewer ta
171
171
  launch prompt and returns its answer as output, which the driver posts. `atdd-bun chat wait` takes several
172
172
  topics and `--skip heartbeat`; reviewers answer in JSON.
173
173
 
174
+ From 0.10.7 only a browser spec (a `*.e2e.*` file, or a Playwright file bound by `// Train:`/`// Journey:` or a
175
+ `test:train:`/`test:journey:` URN) is a journey spec. A `bun:test` file may carry the `Acceptance:` line
176
+ traceability needs, including a test proving `acc:train:…` and the journey test `atdd-bun.topology.e2e-location` requires.
177
+
174
178
  Every key is optional; these are the defaults:
175
179
 
176
180
  ```yaml
@@ -181,11 +185,11 @@ delivery:
181
185
  # board: { url: http://127.0.0.1:2586 } # opt-in: agents talk through a local board; absent, there is none
182
186
  independence: different-model # a reviewer's model wrote none of the work it reviews; or fresh-process
183
187
  stages: # models in preference order: the first, then recorded fallbacks
184
- plan: { writer: [codex, claude-opus], reviewer: [glm, claude-opus, codex] }
185
- red: { writer: [glm, claude-sonnet, claude-opus, codex] }
186
- green: { writer: [glm, claude-sonnet, claude-opus, codex] }
187
- refactor: { writer: [glm, claude-sonnet, claude-opus, codex] }
188
- final: { reviewer: [codex, glm, claude-opus] }
188
+ plan: { writer: [codex, claude-opus, kimi], reviewer: [glm, claude-opus, codex, kimi] }
189
+ red: { writer: [glm, claude-sonnet, deepseek-flash, claude-opus, codex, kimi] }
190
+ green: { writer: [glm, claude-sonnet, deepseek-flash, claude-opus, codex, kimi] }
191
+ refactor: { writer: [glm, claude-sonnet, deepseek-flash, claude-opus, codex, kimi] }
192
+ final: { reviewer: [codex, glm, claude-opus, kimi] }
189
193
  fallback: { after_failures: 3, within_minutes: 10, when_exhausted: block } # or wait
190
194
  commands: {} # per model: { author: "...", review: "..." } overriding delivery.operating-model's defaults
191
195
  ```
@@ -27,7 +27,7 @@ terms:
27
27
  atdd-bun chat post writes the header: from, to, participants and conversation always; kind (task, result, review-request, verdict, rebuttal, status, blocked, heartbeat), stage, reply-to, repo, branch, worktree, goal, base and sha when they apply. The body carries the whole instruction or answer, however long: the thread is the history. A heartbeat carries nothing else; anything that needs an answer goes as blocked, never inside a heartbeat.
28
28
  - term_id: commands
29
29
  text: >-
30
- Post with the body on standard input; read what is on a topic; wait for the next message addressed to you on one or several topics (comma-separated), skipping kinds you do not act on (--skip heartbeat), bounded by --timeout so a tool call ends (exit 2: nothing yet, wait again with --since the last id); and, for a human, atdd-bun chat alone opens the board: every topic on the left, the selected conversation on the right, live.
30
+ Post with the body on standard input; read what is on a topic; wait for the next message addressed to you on one or several topics (comma-separated), skipping kinds you do not act on (--skip heartbeat), bounded by --timeout so a tool call ends (exit 2: nothing yet). wait remembers where each identity stopped on each set of topics (~/.atdd-board/cursors), so re-arming a watcher is always the same command; --since still overrides it; and, for a human, atdd-bun chat alone opens the board: every topic on the left, the selected conversation on the right, live.
31
31
  values:
32
32
  post: atdd-bun chat post <topic> --to <agent> --kind <kind> [--reply-to <id>] < message.md
33
33
  read: atdd-bun chat read <topic> [--mine]
@@ -21,11 +21,11 @@ content:
21
21
  root: docs/delivery/tranches
22
22
  independence: different-model
23
23
  stages:
24
- plan: { writer: [codex, claude-opus], reviewer: [glm, claude-opus, codex] }
25
- red: { writer: [glm, claude-sonnet, claude-opus, codex] }
26
- green: { writer: [glm, claude-sonnet, claude-opus, codex] }
27
- refactor: { writer: [glm, claude-sonnet, claude-opus, codex] }
28
- final: { reviewer: [codex, glm, claude-opus] }
24
+ plan: { writer: [codex, claude-opus, kimi], reviewer: [glm, claude-opus, codex, kimi] }
25
+ red: { writer: [glm, claude-sonnet, deepseek-flash, claude-opus, codex, kimi] }
26
+ green: { writer: [glm, claude-sonnet, deepseek-flash, claude-opus, codex, kimi] }
27
+ refactor: { writer: [glm, claude-sonnet, deepseek-flash, claude-opus, codex, kimi] }
28
+ final: { reviewer: [codex, glm, claude-opus, kimi] }
29
29
 
30
30
  independence defaults to different-model. A stage may name a writer, a reviewer or both. The 0.9
31
31
  names plan_review, test_review, code_review and final_review are still read, as plan, red, refactor
@@ -29,7 +29,7 @@ terms:
29
29
  One line each, for the coordinator to wait on: PROGRAM_EVENT <tranche> <STAGE <stage> <sha> | WORKER_START <role> <model> <sha> | WORKER_END <role> <model> <verdict> | FALLBACK <role> <from>→<to> <reason> | PR_OPENED <url> | MERGED <sha> | BLOCKED <reason> | HEARTBEAT>.
30
30
  - term_id: commands
31
31
  text: >-
32
- The default headless command per model, overridden per model under delivery.commands.<model>.author (the writer's command) and .review. {prompt} and {worktree} are substituted. Claude runs take project settings only, so a user's own allowances cannot widen a reviewer. glm runs through Pi, which has no permission system: as a reviewer it gets the read tool alone (the driver puts the diff in its prompt and records its output), and as a writer it runs only inside a container or sandbox limited to the worktree.
32
+ The default headless command per model, overridden per model under delivery.commands.<model>.author (the writer's command) and .review. {prompt} and {worktree} are substituted. Claude runs take project settings only, so a user's own allowances cannot widen a reviewer. glm, kimi (Moonshot kimi-k3, a replacement for codex and claude-opus) and deepseek-flash (a smaller coding model beside claude-sonnet) run through Pi, which has no permission system; kimi and deepseek-flash take their keys from MOONSHOT_API_KEY and DEEPSEEK_API_KEY, or point delivery.commands at a wrapper that provides them. Through Pi, as a reviewer it gets the read tool alone (the driver puts the diff in its prompt and records its output), and as a writer it runs only inside a container or sandbox limited to the worktree.
33
33
  values:
34
34
  codex:
35
35
  author: codex exec --cd {worktree} --sandbox workspace-write "{prompt}"
@@ -43,6 +43,12 @@ terms:
43
43
  glm:
44
44
  author: cd {worktree} && pi -p --no-session --no-extensions --no-skills --no-context-files --tools read,bash,edit,write "{prompt}"
45
45
  review: cd {worktree} && pi -p --no-session --no-extensions --no-skills --no-context-files --tools read "{prompt}"
46
+ kimi:
47
+ author: cd {worktree} && pi --provider moonshotai --model kimi-k3 -p --no-session --no-extensions --no-skills --no-context-files --tools read,bash,edit,write "{prompt}"
48
+ review: cd {worktree} && pi --provider moonshotai --model kimi-k3 -p --no-session --no-extensions --no-skills --no-context-files --tools read "{prompt}"
49
+ deepseek-flash:
50
+ author: cd {worktree} && pi --provider deepseek --model deepseek-flash -p --no-session --no-extensions --no-skills --no-context-files --tools read,bash,edit,write "{prompt}"
51
+ review: cd {worktree} && pi --provider deepseek --model deepseek-flash -p --no-session --no-extensions --no-skills --no-context-files --tools read "{prompt}"
46
52
  - term_id: example_record
47
53
  text: >-
48
54
  A tranche in progress: the plan written by codex and approved by glm; red, green and refactor written by glm; the final review requesting changes. Only the finding's outcome is ever added to an earlier entry.
@@ -3,7 +3,7 @@ rule_id: tester.htmx.journey-no-acceptance-marker
3
3
  kind: rule
4
4
  status: active
5
5
  name: A journey spec carries no Acceptance or WMBT header
6
- statement: A journey spec MUST NOT carry `// Acceptance:` or `// WMBT:` header lines. Bun/htmx realization of `tester.vite.journey-no-acceptance-marker`,
6
+ statement: A journey spec, meaning a browser spec (a `*.e2e.*` file, or a Playwright file bound to a train or journey), MUST NOT carry `// Acceptance:` or `// WMBT:` header lines; a bun test file is never a journey spec. Bun/htmx realization of `tester.vite.journey-no-acceptance-marker`,
7
7
  strict.
8
8
  terms:
9
9
  - term_id: browser_spec
@@ -65,7 +65,10 @@ export function collectSpecs(roots, excludes) {
65
65
  return [...out.values()];
66
66
  }
67
67
 
68
- export const isJourneySpec = (spec) => spec.e2eNamed || spec.journeyMarked;
68
+ // A journey spec is a browser spec: a *.e2e.* file, or a Playwright file whose header binds a train or journey. A bun:test
69
+ // file is never one, so a Bun test proving acc:train:… (or the journey test atdd-bun.topology.e2e-location requires) may
70
+ // carry the Acceptance: line traceability needs.
71
+ export const isJourneySpec = (spec) => spec.e2eNamed || (spec.playwright && spec.journeyMarked);
69
72
 
70
73
  /** The plan subjects a spec can bind to, read with the package's own loader. */
71
74
  export async function planOf(root) {
package/integrity.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "0.10.5",
2
+ "version": "0.10.7",
3
3
  "files": {
4
4
  "HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
5
5
  "PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
6
- "README.md": "b3d92666940ed0801e52bd8c4fd0f2942b841a52f4eceb543776d867117e2f75",
6
+ "README.md": "54f6959a5fe0a83ff7ba07856f5385eff0bff0607bfe7b2287897b2d8378825e",
7
7
  "bunfig.toml": "b9fc65eca9014c5179380259d70e76385a6a80788fa9a2df5fb3eaa5554fd2fe",
8
8
  "conventions/atdd-bun.planner/atdd-bun.planner.acceptance-identity.convention.yaml": "81c5c773d5ee15e8d99a2c237aa9846b110b2533df45ebf407d0e3ec6ed3dd03",
9
9
  "conventions/atdd-bun.planner/atdd-bun.planner.identity-required.convention.yaml": "e96d7c1455d0c221072d82e7f2b55da1c4f9ceb17718eb6ed96affdeef37675b",
@@ -92,13 +92,13 @@
92
92
  "conventions/coder.htmx/coder.htmx.verb-endpoint-same-origin.convention.yaml": "2128d26153e2d1f19c65a1e1665d4f82c78395daedb336d6ef1256e82b74aedb",
93
93
  "conventions/coder.htmx/coder.htmx.verb-mutation-signals-progress.convention.yaml": "ae85c5ea01a09a3c41b939988088a8e5da91848d2710a1f0a1e0c86308638ac9",
94
94
  "conventions/delivery/delivery.approved-sha-resolves.convention.yaml": "12eadbc75b081e9469b9a0a7dbf3fdd30c8b34f66e647efa8bf2528119e61547",
95
- "conventions/delivery/delivery.board.convention.yaml": "c61df91a5fa95ceb77be42d0571a0f904f76ed2324cd61ae17e0d87caf42cec9",
96
- "conventions/delivery/delivery.config-schema.convention.yaml": "455001b7ac12824e27b1f1dd13ae8c09bfa140957e118ebd1229779ea0808d4f",
95
+ "conventions/delivery/delivery.board.convention.yaml": "51ff4c16dddf20504f34accc5ddcd150108fab70e4e6f5bdb36cec71d0c905a8",
96
+ "conventions/delivery/delivery.config-schema.convention.yaml": "4c7b449074a8bd9f89b70c702ab3e0df229105ddb4458772f894f0bf37813e9f",
97
97
  "conventions/delivery/delivery.evidence-schema.convention.yaml": "9c6a22495dfb3e2fcbca700392866c3b5660084b1f5594230eb320b499d5e5cb",
98
98
  "conventions/delivery/delivery.findings-resolved.convention.yaml": "f6574e93a9a2d4bdd6d6f5645720b1a647fc0048d95f7675fe783d2ddb62c05b",
99
99
  "conventions/delivery/delivery.merge-gate.convention.yaml": "48745ec23625b68be9b8fe2c6dcdcba88b2b38924f6593abf6987391ff0ee243",
100
100
  "conventions/delivery/delivery.model-allowed.convention.yaml": "1071a727f61bacd21d70921885826974234ef0a58c0a814454b9375a0432b0fe",
101
- "conventions/delivery/delivery.operating-model.convention.yaml": "e66f75787518954cd830fff544d40ac7ddecdf8cf58aa1d73a76a30beeff3fe9",
101
+ "conventions/delivery/delivery.operating-model.convention.yaml": "7982157d957938ade878a114d3f5c2bdaa8f02e52ac21614634d9af4be9692f1",
102
102
  "conventions/delivery/delivery.review.convention.yaml": "b033579c2d90fcb098d325dcc07c7b084e048cca43fbe8e28159ec08a412018a",
103
103
  "conventions/delivery/delivery.reviewer-independent.convention.yaml": "8c436d66ef5b1f6d6bc5dd55a235caca2490020e88665eecadb2ca41cd4149c5",
104
104
  "conventions/delivery/delivery.stages-complete.convention.yaml": "0e5dd8057ad51eff7906e3424a9686a81be1814447afb9a30acab43d39e49b4c",
@@ -150,7 +150,7 @@
150
150
  "conventions/tester.htmx/tester.htmx.fragment-asserts-returned-markup.convention.yaml": "b8de8e893b582d8ec4b7c24acfa16da0eccd14e6e58e92baf9e9c10c9749ea0c",
151
151
  "conventions/tester.htmx/tester.htmx.journey-binding-header.convention.yaml": "7a9f8655cccdd12ab7e2d49af128c9f9572ad441823ca6e32cbae26b26b2d714",
152
152
  "conventions/tester.htmx/tester.htmx.journey-layer-assembly.convention.yaml": "86948c4b1e928eef07329afeec73660ea51878e3baa58350f61a8f81c20e9fc3",
153
- "conventions/tester.htmx/tester.htmx.journey-no-acceptance-marker.convention.yaml": "548588cb2722fb906034af7cc98bcce91000afb1cfd20875a96f143f71e43a18",
153
+ "conventions/tester.htmx/tester.htmx.journey-no-acceptance-marker.convention.yaml": "f0d96af21e057e8ae02bb0d3c3f6e10c136e5b3eb19d94a7d0f034bc0b0a87e0",
154
154
  "conventions/tester.htmx/tester.htmx.journey-urn-format.convention.yaml": "6771d984ddb6fe564a6803e971e934242d0a53bd8711826e66d4d0c3283f8aed",
155
155
  "conventions/tester.htmx/tester.htmx.presentation-smoke-coverage.convention.yaml": "b18565f55f9f92f7b292bae8231c2343e72f910dc9e228b307db318eb62488cc",
156
156
  "conventions/tester.htmx/tester.htmx.responsive-harness.convention.yaml": "cec312935ab592c2c7a5c6a94623a02df771a9e2ca2035b721ef1006c4800fdf",
@@ -675,7 +675,7 @@
675
675
  "detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/orphan/README.md": "7d480b1fff6546615e55d866784bc2267d06d0221dfde8d0ba0353fa1ea0db9a",
676
676
  "detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/ui/evidence.yaml": "777b4f7921e302dfce79cd764bba8b44091ac9c7ee04a00f5dbb663342ec68a0",
677
677
  "detectors/htmx_e2e_detector/atdd.implementation.yaml": "454bb123039d57c992d27d9274e20fa3452ac41a237eb55471be62a4bcfebf06",
678
- "detectors/htmx_e2e_detector/checks/_e2e.mjs": "4e3605500ac232603083790c3984de5c828028c60eef366b26aba18d38fce30b",
678
+ "detectors/htmx_e2e_detector/checks/_e2e.mjs": "efec6c8e3dc8b95bf3acd826397ecdedac64c06d367791a4017a272bc0660896",
679
679
  "detectors/htmx_e2e_detector/checks/_map.json": "94dec08ff4764cd6a3dba139f12a7c984292e95a9354b0cdaa0ce9d4180e2c89",
680
680
  "detectors/htmx_e2e_detector/checks/a11y_harness.mjs": "d66e2a6da3c3820db72fdaddcd53efb9c0e39d03bc559a59b800923d5c944700",
681
681
  "detectors/htmx_e2e_detector/checks/covered_train_is_routed.mjs": "c7a8db0e932ba96489477fb5a8589cd9eba868db6fbaaa8012802a4da78fa0f8",
@@ -1192,10 +1192,10 @@
1192
1192
  "relationships.yaml": "97da2db334e3c8cd4f634ee8d12fe426aabc0f5c37508d02450ce8beca100f04",
1193
1193
  "src/agent.ts": "65e4579adb5a54977f557f8c783aba0e2a2a1d5cf3cea9f017b67ffe598b2f65",
1194
1194
  "src/board-ui.ts": "16a843705552f8f18e0e328e90e4a2a9148d771ca69f2708fe4a91302947dfad",
1195
- "src/board.ts": "dede8d3059165a1a0fd051420d83ef91ff9a5fffa57d1ff6c44f31defa393bfa",
1195
+ "src/board.ts": "115c6cff12fb2b20e6471013161aef020a65331ecb97fa323e9b08f55053149f",
1196
1196
  "src/ci.ts": "67e54d2cbfc44a9e42837d5e750af526d6255c539d3c452a4d5fca3226c0d9ac",
1197
1197
  "src/cli.ts": "e45f2666334503a91eb65b1b5ee4dd8acab6b99f62d333e2ee2b65963db6358b",
1198
- "src/delivery.ts": "3fd36b6f7dbe9b5682241cff16362618ac0a85c0ac93a238141ebabdbe402e52",
1198
+ "src/delivery.ts": "9a4500ae949faeef7bcfda114b7b09d4ac2b2cbb1218e68016829e0b6892847b",
1199
1199
  "src/docs-capability.ts": "115cf27049a5cf19c133bac5ec237072be6f969a59a2ef6767e054108a21dc5c",
1200
1200
  "src/enforce.ts": "41a6f8c058a2a034996c817f224191403cc66ccc4ed4e04bbefc430014c783d8",
1201
1201
  "src/hooks.ts": "9779eb48f9d0cb6234c33f6616f98dde6583d2ee03ac3c19f3ace2d156af26d0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-bun",
3
- "version": "0.10.5",
3
+ "version": "0.10.7",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/afokapu/atdd-bun.git"
package/src/board.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
3
4
  import { readFile } from "node:fs/promises";
4
5
  import { join } from "node:path";
5
6
  import { runUi } from "./board-ui";
@@ -107,17 +108,26 @@ export async function read(url: string, topics: string[], since = "all"): Promis
107
108
  * It polls rather than streams: a live stream can drop a message published while it connects. ntfy reads `since` (a
108
109
  * message id) as that message's time on every topic, so one cursor serves several topics. Returns null after
109
110
  * `timeoutSeconds`, so an agent's tool call stays bounded. */
110
- export async function waitFor(url: string, topics: string[], agent: string, since = "all", timeoutSeconds = 0, skip: string[] = [], intervalMs = 500): Promise<Message | null> {
111
+ export async function waitFor(url: string, topics: string[], agent: string, since = "all", timeoutSeconds = 0, skip: string[] = [], intervalMs = 500, seen: (id: string) => void = () => {}): Promise<Message | null> {
111
112
  const deadline = timeoutSeconds > 0 ? Date.now() + timeoutSeconds * 1000 : Infinity;
112
113
  for (;;) {
113
114
  let messages: Message[] = [];
114
115
  try { messages = await read(url, topics, since); } catch { /* the board is briefly unavailable: keep waiting */ }
115
- for (const message of messages) { since = message.id; if (addressedTo(message, agent) && !skip.includes(message.header.kind ?? "")) return message; }
116
+ for (const message of messages) { since = message.id; seen(since); if (addressedTo(message, agent) && !skip.includes(message.header.kind ?? "")) return message; }
116
117
  if (Date.now() >= deadline) return null;
117
118
  await Bun.sleep(intervalMs);
118
119
  }
119
120
  }
120
121
 
122
+ /** Where `wait` keeps its place, per identity and topic set, so restarting a watcher is always the same command.
123
+ * Best effort: where the file cannot be read or written, wait behaves as if it had no saved place. */
124
+ function cursorFile(agent: string, topics: string[], env: Record<string, string | undefined>): string {
125
+ const key = `${agent}__${[...topics].sort().join("+")}`.replace(/[^A-Za-z0-9@+_.-]/g, "_");
126
+ return join(env.ATDD_BOARD_STATE || join(homedir(), ".atdd-board", "cursors"), key);
127
+ }
128
+ const loadCursor = (file: string) => { try { return readFileSync(file, "utf8").trim() || undefined; } catch { return undefined; } };
129
+ const saveCursor = (file: string, id: string) => { try { mkdirSync(join(file, ".."), { recursive: true }); writeFileSync(file, `${id}\n`); } catch { /* best effort */ } };
130
+
121
131
  export function format(message: Message, withTopic = false): string {
122
132
  const time = new Date(message.time * 1000).toTimeString().slice(0, 8), h = message.header;
123
133
  const route = `${h.from ?? "?"} -> ${[h.to ?? []].flat().join(", ") || "?"}`, tags = [h.kind, h.stage].filter(Boolean).join(" · ");
@@ -141,8 +151,10 @@ export async function chat(args: string[], env: Record<string, string | undefine
141
151
  const me = agent();
142
152
  topics.forEach(topic => checkTopic(topic, env));
143
153
  if (command === "wait") {
144
- const message = await waitFor(url, topics, me, flag("since") || "all", Number(flag("timeout") ?? 0), list(flag("skip")));
145
- if (!message) { console.error(`no message for ${me} on ${topics.join(", ")} yet; wait again with --since to continue`); return 2; }
154
+ // Without --since, continue from where this identity last stopped on these topics; either way, remember the place.
155
+ const file = cursorFile(me, topics, env);
156
+ const message = await waitFor(url, topics, me, flag("since") || loadCursor(file) || "all", Number(flag("timeout") ?? 0), list(flag("skip")), 500, id => saveCursor(file, id));
157
+ if (!message) { console.error(`no message for ${me} on ${topics.join(", ")} yet; run the same wait again to continue`); return 2; }
146
158
  console.log(format(message, topics.length > 1));
147
159
  return 0;
148
160
  }
package/src/delivery.ts CHANGED
@@ -41,11 +41,11 @@ export type DeliveryPolicy = {
41
41
  /** The default operating model: two reviews, the plan and the whole change; the stages between them are written and
42
42
  * held by their deterministic gates. Lists are preference orders; a later model is used only as a recorded fallback. */
43
43
  const DEFAULT_STAGES: Partial<Record<Stage, { writer?: string[]; reviewer?: string[] }>> = {
44
- plan: { writer: ["codex", "claude-opus"], reviewer: ["glm", "claude-opus", "codex"] },
45
- red: { writer: ["glm", "claude-sonnet", "claude-opus", "codex"] },
46
- green: { writer: ["glm", "claude-sonnet", "claude-opus", "codex"] },
47
- refactor: { writer: ["glm", "claude-sonnet", "claude-opus", "codex"] },
48
- final: { reviewer: ["codex", "glm", "claude-opus"] },
44
+ plan: { writer: ["codex", "claude-opus", "kimi"], reviewer: ["glm", "claude-opus", "codex", "kimi"] },
45
+ red: { writer: ["glm", "claude-sonnet", "deepseek-flash", "claude-opus", "codex", "kimi"] },
46
+ green: { writer: ["glm", "claude-sonnet", "deepseek-flash", "claude-opus", "codex", "kimi"] },
47
+ refactor: { writer: ["glm", "claude-sonnet", "deepseek-flash", "claude-opus", "codex", "kimi"] },
48
+ final: { reviewer: ["codex", "glm", "claude-opus", "kimi"] },
49
49
  };
50
50
  /** The stage a record or config name refers to: a current name, or a legacy review stage's. */
51
51
  export const stageOf = (name: string): Stage | undefined => (STAGES as readonly string[]).includes(name) ? name as Stage : LEGACY_STAGES[name];