@afokapu/atdd-bun 0.10.5 → 0.10.6

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
@@ -181,11 +181,11 @@ delivery:
181
181
  # board: { url: http://127.0.0.1:2586 } # opt-in: agents talk through a local board; absent, there is none
182
182
  independence: different-model # a reviewer's model wrote none of the work it reviews; or fresh-process
183
183
  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] }
184
+ plan: { writer: [codex, claude-opus, kimi], reviewer: [glm, claude-opus, codex, kimi] }
185
+ red: { writer: [glm, claude-sonnet, deepseek-flash, claude-opus, codex, kimi] }
186
+ green: { writer: [glm, claude-sonnet, deepseek-flash, claude-opus, codex, kimi] }
187
+ refactor: { writer: [glm, claude-sonnet, deepseek-flash, claude-opus, codex, kimi] }
188
+ final: { reviewer: [codex, glm, claude-opus, kimi] }
189
189
  fallback: { after_failures: 3, within_minutes: 10, when_exhausted: block } # or wait
190
190
  commands: {} # per model: { author: "...", review: "..." } overriding delivery.operating-model's defaults
191
191
  ```
@@ -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.
package/integrity.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "0.10.5",
2
+ "version": "0.10.6",
3
3
  "files": {
4
4
  "HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
5
5
  "PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
6
- "README.md": "b3d92666940ed0801e52bd8c4fd0f2942b841a52f4eceb543776d867117e2f75",
6
+ "README.md": "b70a7002a8f887ed0108fc22b3bce15af438df1f094e83e8abc334547cd9e95d",
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",
@@ -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.6",
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];