@afokapu/atdd-bun 0.10.4 → 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
@@ -167,6 +167,10 @@ fail. Records in progress are judged by the current policy.
167
167
  From 0.10.4 a stage with nothing written (a plan-only or no-op tranche) records no work; a review
168
168
  whose range recorded none answers for the latest recorded work before it.
169
169
 
170
+ From 0.10.5 only coordinators and drivers use the board: a writer or reviewer takes its task from its
171
+ launch prompt and returns its answer as output, which the driver posts. `atdd-bun chat wait` takes several
172
+ topics and `--skip heartbeat`; reviewers answer in JSON.
173
+
170
174
  Every key is optional; these are the defaults:
171
175
 
172
176
  ```yaml
@@ -177,11 +181,11 @@ delivery:
177
181
  # board: { url: http://127.0.0.1:2586 } # opt-in: agents talk through a local board; absent, there is none
178
182
  independence: different-model # a reviewer's model wrote none of the work it reviews; or fresh-process
179
183
  stages: # models in preference order: the first, then recorded fallbacks
180
- plan: { writer: [codex, claude-opus], reviewer: [glm, claude-opus, codex] }
181
- red: { writer: [glm, claude-sonnet, claude-opus, codex] }
182
- green: { writer: [glm, claude-sonnet, claude-opus, codex] }
183
- refactor: { writer: [glm, claude-sonnet, claude-opus, codex] }
184
- 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] }
185
189
  fallback: { after_failures: 3, within_minutes: 10, when_exhausted: block } # or wait
186
190
  commands: {} # per model: { author: "...", review: "..." } overriding delivery.operating-model's defaults
187
191
  ```
@@ -4,11 +4,11 @@ kind: policy
4
4
  status: active
5
5
  name: Agents talk through a local board, one topic per conversation
6
6
  statement: >-
7
- Where atdd-bun.yaml sets delivery.board, the coordinator, drivers, writers and reviewers talk through a local message board with atdd-bun chat, never by typing into another agent's pane: one topic for the program, one per tranche, one per review conversation, each agent launched with its identity and the only topics it may use, every message naming its sender and recipients. The board makes the work visible; the evidence record stays the only thing the delivery rules judge.
7
+ Where atdd-bun.yaml sets delivery.board, the coordinator and drivers talk through a local message board with atdd-bun chat, never by typing into another agent's pane: one topic for the program, one per tranche, one per review conversation, each agent launched with its identity and the only topics it may use, every message naming its sender and recipients. The board makes the work visible; the evidence record stays the only thing the delivery rules judge.
8
8
  terms:
9
9
  - term_id: topics
10
10
  text: >-
11
- Three levels, each its own topic, named by atdd-bun chat topic so every agent spells them alike. atdd-<program>: coordinator, drivers and humans; status only (started, blocked, ready, merged, a decision a human must take). atdd-<program>-<tranche>: the tranche's driver and writers; tasks and results. atdd-<program>-<tranche>-<stage>-<round>: the driver and one reviewer; the review request and the verdict. A rebuttal opens the next round's topic.
11
+ Three levels, each its own topic, named by atdd-bun chat topic so every agent spells them alike. atdd-<program>: coordinator, drivers and humans; status only (started, blocked, ready, merged, a decision a human must take). atdd-<program>-<tranche>: the tranche's driver; the tasks it gives its writers and their results. atdd-<program>-<tranche>-<stage>-<round>: the driver; one review's request and verdict. A rebuttal opens the next round's topic.
12
12
  values:
13
13
  program: atdd-bun chat topic <program>
14
14
  tranche: atdd-bun chat topic <program> <tranche>
@@ -18,21 +18,20 @@ terms:
18
18
  <role>@<tranche> for the whole tranche (driver@auth, writer-glm@auth, reviewer-codex@auth; a later round adds it: reviewer-codex-2@auth); coordinator for the coordinator.
19
19
  - term_id: launch
20
20
  text: >-
21
- Whoever starts an agent gives it, in its environment, ATDD_AGENT (its identity) and ATDD_TOPICS (the topics it may use, comma-separated), and ATDD_BOARD_URL when the board is not the configured one. The coordinator gives a driver its tranche topic and the program topic; the driver adds each review topic as it opens it, gives a writer the tranche topic, and gives a reviewer its review topic alone. atdd-bun chat refuses any other topic, to read or to write.
21
+ Whoever starts a coordinator or driver gives it, in its environment, ATDD_AGENT (its identity) and ATDD_TOPICS (the topics it may use, comma-separated), and ATDD_BOARD_URL when the board is not the configured one. The coordinator gives a driver its tranche topic and the program topic; the driver adds each review topic as it opens it. atdd-bun chat refuses any other topic, to read or to write. Writers and reviewers get no board access: each is a headless run whose task is its launch prompt and whose answer is its output, which the driver posts (a verdict as "verdict of <reviewer>"), so their tools stay minimal and no sandbox has to reach the board.
22
22
  values:
23
+ coordinator: ATDD_AGENT=coordinator@<program> ATDD_TOPICS=atdd-<program>
23
24
  driver: ATDD_AGENT=driver@<tranche> ATDD_TOPICS=atdd-<program>,atdd-<program>-<tranche>
24
- writer: ATDD_AGENT=writer-<model>@<tranche> ATDD_TOPICS=atdd-<program>-<tranche>
25
- reviewer: ATDD_AGENT=reviewer-<model>@<tranche> ATDD_TOPICS=atdd-<program>-<tranche>-<stage>-<round>
26
25
  - term_id: message
27
26
  text: >-
28
- atdd-bun chat post writes the header: from, to, participants and conversation always; kind (task, result, review-request, verdict, rebuttal, status, blocked), 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.
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.
29
28
  - term_id: commands
30
29
  text: >-
31
- Post with the body on standard input; read what is on a topic; wait for the next message addressed to you, 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.
32
31
  values:
33
32
  post: atdd-bun chat post <topic> --to <agent> --kind <kind> [--reply-to <id>] < message.md
34
33
  read: atdd-bun chat read <topic> [--mine]
35
- wait: atdd-bun chat wait <topic> [--since <id>] --timeout 540
34
+ wait: atdd-bun chat wait <topic>[,<topic>...] [--since <id>] [--skip heartbeat] --timeout 540
36
35
  board: atdd-bun chat
37
36
  - term_id: server
38
37
  text: >-
@@ -43,11 +42,11 @@ content:
43
42
  summary: >-
44
43
  Typing into an agent's pane loses messages to startup screens, busy prompts and cut pastes. A board with one topic per conversation delivers every message, keeps the history, and lets a human follow any thread.
45
44
  normative_text: |
46
- A reviewer never reads the tranche's conversation: its brief is the review request on its own topic, and it posts its verdict there. The driver records that verdict in evidence.yaml; the board is never evidence.
45
+ A reviewer never reads the tranche's conversation: its brief is the review request in its launch prompt, and its verdict is its output. The driver records that verdict in evidence.yaml and posts it on the review topic; the board is never evidence.
47
46
  Agents use atdd-bun chat only. It refuses an address that is not local and a topic the agent was not given. Never call ntfy directly: in the ntfy client -u means credentials, and a topic without a server goes to the public ntfy.sh.
48
- A running session that must be woken is reached through its own channel (codex queue --thread <id>; --resume <session> for Claude and Pi), never by typing into its pane; its launch prompt tells it to wait on its topic between steps.
47
+ A running session that must be woken is reached through its own channel (codex queue --thread <id>; --resume <session> for Claude and Pi), never by typing into its pane; a driver's launch prompt tells it to wait on its topics between steps.
49
48
  fix_hint: |
50
- Derive the topic with atdd-bun chat topic, launch the agent with ATDD_AGENT and ATDD_TOPICS, and post with --to. To follow the work, run atdd-bun chat in a terminal pane. ntfy cannot list its topics, so the first message on a topic also posts the topic's name to the directory topic atdd-topics, which the view reads.
49
+ Derive the topic with atdd-bun chat topic, launch coordinators and drivers with ATDD_AGENT and ATDD_TOPICS, and post with --to. To follow the work, run atdd-bun chat in a terminal pane. ntfy cannot list its topics, so the first message on a topic also posts the topic's name to the directory topic atdd-topics, which the view reads.
51
50
  exceptions:
52
51
  - >-
53
52
  The board is opt-in: without a delivery.board block there is none, and atdd-bun chat refuses to post or read. With the block and no url it is at http://127.0.0.1:2586; ATDD_BOARD_URL moves it on one machine but never switches it on. Switching it on or off changes how agents talk, not what a record must prove, so it is not a loosening.
@@ -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
@@ -14,7 +14,7 @@ terms:
14
14
  Owns one tranche end to end. For each stage of delivery.stages, in order, it starts the stage's writer (the first model in its writer list, or the next after a recorded fallback) in the tranche worktree, commits red on its own before green starts, passes the stage's gate, and appends a work entry; for each stage with a reviewer it starts a review. It never reviews.
15
15
  - term_id: review_run
16
16
  text: >-
17
- Every review is a fresh process in its own detached worktree at the exact SHA (git worktree add --detach), given the delivery.review convention, the stage, the base SHA, the SHA and the RED commit. Afterwards git status --porcelain must be empty and HEAD still the SHA, or the review is a REVIEWER_FAILURE and does not count. The raw output is kept in the tranche folder and named in the review's report.
17
+ Every review is a fresh process in its own detached worktree at the exact SHA (git worktree add --detach), launched with a prompt that gives it the delivery.review convention, the stage, the base SHA, the SHA and the RED commit. Afterwards git status --porcelain must be empty and HEAD still the SHA, or the review is a REVIEWER_FAILURE and does not count. The raw output is kept in the tranche folder and named in the review's report.
18
18
  - term_id: fallback
19
19
  text: >-
20
20
  After fallback.after_failures failures within fallback.within_minutes (outage, rate limit, no auditable report, timeout), the driver uses the next model in the list and records the fallback with its role, kind, failures and window. A request-changes verdict is never a failure. With the list exhausted, when_exhausted block emits BLOCKED provider-unavailable; wait keeps retrying the last model.
@@ -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.
@@ -74,7 +80,7 @@ content:
74
80
  summary: >-
75
81
  The policy says who may write and review each stage; this convention says how the coordinator and drivers carry it out. The record they leave is what the delivery rules judge.
76
82
  normative_text: |
77
- With delivery.board set, tasks, results, review requests and verdicts travel on the board (delivery.board), each agent launched with its identity and topics. Without it, a task is the prompt an agent is launched with and its result is the run's output, which the driver keeps. Agents run in panes of the multiplexer named in delivery.multiplexer (default herdr) only for humans to watch; learn its commands from its own help before the first dispatch. Never deliver work to an agent by typing into its pane.
83
+ With delivery.board set, the coordinator and drivers talk on the board (delivery.board), and each driver posts its workers' tasks, results, review requests and verdicts there; workers never use the board. Without it, a task is the prompt an agent is launched with and its result is the run's output, which the driver keeps. Agents run in panes of the multiplexer named in delivery.multiplexer (default herdr) only for humans to watch; learn its commands from its own help before the first dispatch. Never deliver work to an agent by typing into its pane.
78
84
  Before a hosted model receives private repository content, confirm the user or organization authorized it.
79
85
  Never edit the delivery skill, these conventions, or loosen the delivery policy to get a tranche through. If the policy must change, stop and ask the human.
80
86
  fix_hint: |
@@ -27,7 +27,7 @@ content:
27
27
  normative_text: |
28
28
  The reviewer runs in a detached worktree at the SHA and reads the change with git diff base..SHA. The gates are CI's, not the reviewer's: it does not re-run them, and judges what they cannot. It may read, search and use git show, git diff and git log. It never edits, commits or writes, including output-file options such as git diff --output=; the driver checks the worktree afterwards, and a review that wrote does not count (delivery.reviewer-independent). It judges and proposes; the author applies.
29
29
  A finding carrying the author's rebuttal is withdrawn, left out, when the rebuttal holds, or upheld, repeated with the same id; there is no second round (delivery.findings-resolved).
30
- The reviewer returns exactly one YAML document, the review entry of delivery-evidence.schema.json without fallback or report, which the driver adds: stage, sha, verdict (approve only with no critical or high finding), checked, and findings, each with id, severity (critical, high, medium, low), evidence, invariant, affects and proposed_fix, a precise description or short snippet, never a rewrite.
30
+ The reviewer returns exactly one JSON object (valid YAML, so it goes into evidence.yaml unchanged, and a # inside a string is never read as a comment), the review entry of delivery-evidence.schema.json without fallback or report, which the driver adds: stage, sha, verdict (approve only with no critical or high finding), checked, and findings, each with id, severity (critical, high, medium, low), evidence, invariant, affects and proposed_fix, a precise description or short snippet, never a rewrite.
31
31
  fix_hint: |
32
32
  Give the reviewer this convention, the stage, the base SHA, the SHA and the RED commit. A review that skipped part of its checklist shows it in checked; run the stage again with a fresh reviewer.
33
33
  exceptions:
package/integrity.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "0.10.4",
2
+ "version": "0.10.6",
3
3
  "files": {
4
4
  "HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
5
5
  "PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
6
- "README.md": "afff8ee1ca16773915650f8ed3c1e58bbd839259a7963d16441a0259423c8756",
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,14 +92,14 @@
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": "7dbdc307ad74a8fa020eff9b964f7efec5e7feee2eb2294089aef5d113b10ffa",
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": "980858b735974093c98e850d8f6dd83cb21eea86a8b6e8249de678f0fc336f1b",
102
- "conventions/delivery/delivery.review.convention.yaml": "d71b889a92ca695bc7bc989636ef16272860a4d3389a2f1e2cf3776257e97af8",
101
+ "conventions/delivery/delivery.operating-model.convention.yaml": "7982157d957938ade878a114d3f5c2bdaa8f02e52ac21614634d9af4be9692f1",
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",
105
105
  "conventions/planner.docs/planner.docs.adr-registry-derived.convention.yaml": "0ea4232beba90280c664f93d097cafa44c67c2919c1468d4823fdc9e686aa5d2",
@@ -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": "f04c425f74edc003609baec7b5b2d75c1bc98b0e177d702de865dd42d9dd95e5",
1195
+ "src/board.ts": "115c6cff12fb2b20e6471013161aef020a65331ecb97fa323e9b08f55053149f",
1196
1196
  "src/ci.ts": "67e54d2cbfc44a9e42837d5e750af526d6255c539d3c452a4d5fca3226c0d9ac",
1197
- "src/cli.ts": "b1618d7a7b3b0d42238cb1160aea4ee7962fef8135a7c75b89b0f3fa30dc1f45",
1198
- "src/delivery.ts": "3fd36b6f7dbe9b5682241cff16362618ac0a85c0ac93a238141ebabdbe402e52",
1197
+ "src/cli.ts": "e45f2666334503a91eb65b1b5ee4dd8acab6b99f62d333e2ee2b65963db6358b",
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.4",
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";
@@ -103,19 +104,30 @@ export async function read(url: string, topics: string[], since = "all"): Promis
103
104
  .map(m => ({ id: m.id, time: m.time, topic: m.topic, raw: m.message, ...parseEnvelope(m.message) }));
104
105
  }
105
106
 
106
- /** Waits for the next message addressed to `agent` after `since`. It polls rather than streams: a live stream can drop
107
- * a message published while it connects. Returns null after `timeoutSeconds`, so an agent's tool call stays bounded. */
108
- export async function waitFor(url: string, topic: string, agent: string, since = "all", timeoutSeconds = 0, intervalMs = 500): Promise<Message | null> {
107
+ /** Waits for the next message addressed to `agent` on any of `topics` after `since`, passing over the kinds in `skip`.
108
+ * It polls rather than streams: a live stream can drop a message published while it connects. ntfy reads `since` (a
109
+ * message id) as that message's time on every topic, so one cursor serves several topics. Returns null after
110
+ * `timeoutSeconds`, so an agent's tool call stays bounded. */
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> {
109
112
  const deadline = timeoutSeconds > 0 ? Date.now() + timeoutSeconds * 1000 : Infinity;
110
113
  for (;;) {
111
114
  let messages: Message[] = [];
112
- try { messages = await read(url, [topic], since); } catch { /* the board is briefly unavailable: keep waiting */ }
113
- for (const message of messages) { since = message.id; if (addressedTo(message, agent)) return message; }
115
+ try { messages = await read(url, topics, since); } catch { /* the board is briefly unavailable: keep waiting */ }
116
+ for (const message of messages) { since = message.id; seen(since); if (addressedTo(message, agent) && !skip.includes(message.header.kind ?? "")) return message; }
114
117
  if (Date.now() >= deadline) return null;
115
118
  await Bun.sleep(intervalMs);
116
119
  }
117
120
  }
118
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
+
119
131
  export function format(message: Message, withTopic = false): string {
120
132
  const time = new Date(message.time * 1000).toTimeString().slice(0, 8), h = message.header;
121
133
  const route = `${h.from ?? "?"} -> ${[h.to ?? []].flat().join(", ") || "?"}`, tags = [h.kind, h.stage].filter(Boolean).join(" · ");
@@ -136,9 +148,18 @@ export async function chat(args: string[], env: Record<string, string | undefine
136
148
  if (command === undefined) return await runUi(url, env);
137
149
  const topics = list(positional[0]);
138
150
  if (!topics.length) throw new Error(`usage: atdd-bun chat ${command} <topic> …`);
151
+ const me = agent();
152
+ topics.forEach(topic => checkTopic(topic, env));
153
+ if (command === "wait") {
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; }
158
+ console.log(format(message, topics.length > 1));
159
+ return 0;
160
+ }
139
161
  if (topics.length !== 1) throw new Error(`${command} takes one topic`);
140
- const topic = topics[0], me = agent();
141
- checkTopic(topic, env);
162
+ const topic = topics[0];
142
163
  if (command === "post") {
143
164
  const to = list(flag("to"));
144
165
  if (!to.length) throw new Error("--to is required: every message names its recipients");
@@ -153,12 +174,6 @@ export async function chat(args: string[], env: Record<string, string | undefine
153
174
  for (const message of await read(url, [topic], flag("since") || "all")) if (!rest.includes("--mine") || addressedTo(message, me)) console.log(format(message));
154
175
  return 0;
155
176
  }
156
- if (command === "wait") {
157
- const message = await waitFor(url, topic, me, flag("since") || "all", Number(flag("timeout") ?? 0));
158
- if (!message) { console.error(`no message for ${me} on ${topic} yet; wait again with --since to continue`); return 2; }
159
- console.log(format(message));
160
- return 0;
161
- }
162
177
  throw new Error(`unknown chat command ${command}; run atdd-bun chat alone for the board, or topic, post, read, wait`);
163
178
  } catch (error) {
164
179
  console.error(error instanceof Error ? error.message : String(error));
package/src/cli.ts CHANGED
@@ -25,7 +25,7 @@ const usage = {
25
25
  "atdd-bun chat topic <program> [<tranche> [<stage> <round>]]",
26
26
  "atdd-bun chat post <topic> --to <agent,...> [--kind K] [--stage S] [--reply-to ID] [--repo R] [--branch B] [--worktree W] [--goal G] (body on stdin)",
27
27
  "atdd-bun chat read <topic> [--mine] [--since ID]",
28
- "atdd-bun chat wait <topic> [--since ID] [--timeout SECONDS]",
28
+ "atdd-bun chat wait <topic[,topic...]> [--since ID] [--skip KIND,...] [--timeout SECONDS]",
29
29
  "atdd-bun chat (the board: every topic on the left, the conversation on the right)",
30
30
  ],
31
31
  profiles: profileNames,
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];