@afokapu/atdd-bun 0.9.2 → 0.10.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/integrity.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "0.9.2",
2
+ "version": "0.10.0",
3
3
  "files": {
4
4
  "HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
5
5
  "PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
6
- "README.md": "efc3f9f4c8cf13a62b6f17d2a41a17d871d2b14c551617a8b49a832edcb77d04",
6
+ "README.md": "e6bd0deb0c31bfcdb514a8b7626b2782a3ab12d2cccb9558bc907f7f8173b979",
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",
@@ -91,14 +91,17 @@
91
91
  "conventions/coder.htmx/coder.htmx.verb-endpoint-is-routed.convention.yaml": "d6aa58d35df4b7ebbcf675ca79f0177d64e56f095802b9a3eaae3e4befb5c270",
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
- "conventions/delivery/delivery.approved-sha-resolves.convention.yaml": "8bad8a0fdb28c4dc5f22b8949c8113eec4cc7b18b18af8c12ead5e3e320384ac",
95
- "conventions/delivery/delivery.config-schema.convention.yaml": "6c37e1eef9d955f8dadd1308c9641fa5842529d207e7576284a3bc21a076da71",
96
- "conventions/delivery/delivery.evidence-schema.convention.yaml": "d835dc07f029ffa01ad2dd5b129c45863efb2883b7fe0139eef8ac0bce68cc08",
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",
97
+ "conventions/delivery/delivery.evidence-schema.convention.yaml": "9c6a22495dfb3e2fcbca700392866c3b5660084b1f5594230eb320b499d5e5cb",
97
98
  "conventions/delivery/delivery.findings-resolved.convention.yaml": "f6574e93a9a2d4bdd6d6f5645720b1a647fc0048d95f7675fe783d2ddb62c05b",
98
- "conventions/delivery/delivery.merge-gate.convention.yaml": "5961a599a750bd0137f10a25b69b4ba8210c5d3022a02c258c100c4716553172",
99
- "conventions/delivery/delivery.model-allowed.convention.yaml": "0476fb4a00dfdfbc16ac3c5843d7796d03e9d7c3ebd2c898eddb47160ac2fcdb",
100
- "conventions/delivery/delivery.reviewer-independent.convention.yaml": "f3388fe27a0b50549c913ab56a3afda7b70e54f8d0c32146ff59bd9da76afcfc",
101
- "conventions/delivery/delivery.stages-complete.convention.yaml": "1fd8c0a57e8ee2f91104088c25ee2000fb83e15b58a0cc5c432b37fa0457d056",
99
+ "conventions/delivery/delivery.merge-gate.convention.yaml": "48745ec23625b68be9b8fe2c6dcdcba88b2b38924f6593abf6987391ff0ee243",
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": "1cd8c3e34be51355da470f7c5604cc559c8b90f9ed97a006dffdf59379adcd18",
103
+ "conventions/delivery/delivery.reviewer-independent.convention.yaml": "264df63e768a2a17dca782dddbba6ba47a21fa3dd9cd39ed150f9a00dd04bdb0",
104
+ "conventions/delivery/delivery.stages-complete.convention.yaml": "2658cb9f4a4cd8939231e4ebb0ed4db2d0982c94690d25e4577de9dad4c2f742",
102
105
  "conventions/planner.docs/planner.docs.adr-registry-derived.convention.yaml": "0ea4232beba90280c664f93d097cafa44c67c2919c1468d4823fdc9e686aa5d2",
103
106
  "conventions/planner.docs/planner.docs.area-index-required.convention.yaml": "fd931e9f225bfbb5e78436d0d8a2a89cc0f5114493579962c72dd971301becce",
104
107
  "conventions/planner.docs/planner.docs.artifact-path-shape.convention.yaml": "a9f6178d29abe9385342a46a2c57470a3631aaf5e26f01e3660b7088b042418e",
@@ -667,7 +670,7 @@
667
670
  "detectors/delivery_evidence/fixtures/clean/atdd-bun.yaml": "f701861e3530bb5a25a70d73e248549fb96642f48db1ff4d3e8e4546aaa825ee",
668
671
  "detectors/delivery_evidence/fixtures/clean/docs/delivery/tranches/api/evidence.yaml": "2cdbc4f628cf2d56627e6692b1f4e1a9b72316a3b7d09b47fffcd54c696a0fcb",
669
672
  "detectors/delivery_evidence/fixtures/dirty/atdd-bun.yaml": "85c572d65f27b11b7ff8f115883913b2d2f1978097c416d3341b9c9d0de373d4",
670
- "detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/api/evidence.yaml": "2b848f160a401a217f56d53d87ce8e0a0efc0ed6d3781f6cc746e384c93eb8cc",
673
+ "detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/api/evidence.yaml": "05a6c7926603842dbccd820cf19326bcf7377012931b63501748ba30a196945b",
671
674
  "detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/data/evidence.yaml": "165dd67ff64f14c30268de5b18bf0c9c6634fb9ca48439c660c1962cebe3e0cb",
672
675
  "detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/orphan/README.md": "7d480b1fff6546615e55d866784bc2267d06d0221dfde8d0ba0353fa1ea0db9a",
673
676
  "detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/ui/evidence.yaml": "777b4f7921e302dfce79cd764bba8b44091ac9c7ee04a00f5dbb663342ec68a0",
@@ -1172,8 +1175,8 @@
1172
1175
  "planner-schemas/binding-lock.schema.json": "f2455606702f45fafa194549126d4a66e6c286ba1c4ba9e6bdcf932286a6d34d",
1173
1176
  "planner-schemas/component.schema.json": "d20d3502c155627a120cb10d5e9bdec4ba2c73af9c3e9a9d10e3c351f167c72a",
1174
1177
  "planner-schemas/contract-registry.schema.json": "b9dbaf6c87d4f4d676e3b4251344b9eada523b1c5e739c0423cabe267012c12e",
1175
- "planner-schemas/delivery-config.schema.json": "15b70a67c0725b441e6b9d428e9ccd76c06f4ee6d36406050893b70ecc130841",
1176
- "planner-schemas/delivery-evidence.schema.json": "9b084eb73752d5637ba1d429f3709f8795cc2860c21c0b9bd58751b9887b001c",
1178
+ "planner-schemas/delivery-config.schema.json": "16a9b66a7d36edf8a49c68d9786f2516cd2ff5392be6dd60b766a09ffc95a9c6",
1179
+ "planner-schemas/delivery-evidence.schema.json": "291c2c09f0baa3ca037ab073efd998bdc62fb7af62cc0e33ee26949d26eed518",
1177
1180
  "planner-schemas/feature.schema.json": "eb4aa40d89067ae5c86a079bb3d8e4f45c8727f9e15db2b86c08dacf8e4ae56a",
1178
1181
  "planner-schemas/journey.schema.json": "1ff705bcf89a2823cf60c9e1597e254f97844e7a74dc58299d53192ad2c9f8c3",
1179
1182
  "planner-schemas/registry-index.schema.json": "ffd5643da662ced4052707eeb270eb02908f2e48365103ff0638ec7ba146f430",
@@ -1186,14 +1189,16 @@
1186
1189
  "planner-schemas/train.schema.json": "d6fbac9067c1bffeb84eb79a71228441055af6e6e7ba141dd4e0bcb079345ec0",
1187
1190
  "planner-schemas/wagon.schema.json": "57b98f1ade998d745538eb3d72fb3014a50042006870f90bce47028eea336eb4",
1188
1191
  "planner-schemas/wmbt.schema.json": "7a3a5693bfcf01340667c2be4a3c48e74f34b1af7e8dc13372c0de06b7f4fe12",
1189
- "relationships.yaml": "a996da48da310c2fb2503e75470cdec77c850387b7e441b011acff56550b695b",
1190
- "src/agent.ts": "b2015873940ca83bb8fdc683dbb85c2d53fe6f8c3e2b6b0b71bda895f0c1eb2f",
1192
+ "relationships.yaml": "97da2db334e3c8cd4f634ee8d12fe426aabc0f5c37508d02450ce8beca100f04",
1193
+ "src/agent.ts": "65e4579adb5a54977f557f8c783aba0e2a2a1d5cf3cea9f017b67ffe598b2f65",
1194
+ "src/board-ui.ts": "16a843705552f8f18e0e328e90e4a2a9148d771ca69f2708fe4a91302947dfad",
1195
+ "src/board.ts": "f04c425f74edc003609baec7b5b2d75c1bc98b0e177d702de865dd42d9dd95e5",
1191
1196
  "src/ci.ts": "67e54d2cbfc44a9e42837d5e750af526d6255c539d3c452a4d5fca3226c0d9ac",
1192
- "src/cli.ts": "bb2047cee60f48bfe3eaee4c1b3dcd3538a0e1edc170cf58e87686117bea07a7",
1193
- "src/delivery.ts": "8c2f964a4acc4471b6541f980c9c6f270b33cd64c95f5fe852a401060d507975",
1197
+ "src/cli.ts": "b1618d7a7b3b0d42238cb1160aea4ee7962fef8135a7c75b89b0f3fa30dc1f45",
1198
+ "src/delivery.ts": "f78cb15e1eabeca1492bd8f57b2029370d9c2dbc3d3fb7d25ed3a18ea9de87cb",
1194
1199
  "src/docs-capability.ts": "115cf27049a5cf19c133bac5ec237072be6f969a59a2ef6767e054108a21dc5c",
1195
1200
  "src/enforce.ts": "41a6f8c058a2a034996c817f224191403cc66ccc4ed4e04bbefc430014c783d8",
1196
- "src/hooks.ts": "bafda87a1cb1352133ec7e69712cd8b7acd7767dd6c87ad072f1b5596baaee0e",
1201
+ "src/hooks.ts": "6766342c14b6f5a8f937433e15f04230436e3025a8316606e630481c96d9f35a",
1197
1202
  "src/index.ts": "8ee4d7716794f6990ae8580e471b98256a0d0223dad063cd3b4542e598b6e744",
1198
1203
  "src/integrity.ts": "ac516b1d3a73f872d0ed2b20a27ef0ac14ecc015293bd8e51954f322f8a063bb",
1199
1204
  "src/journey-docs.ts": "994d229376244c84212cff951032823068a76565dd2fd427b9d44e341bd14853",
@@ -1208,8 +1213,7 @@
1208
1213
  "templates/agents/AGENTS.block.md": "7e9687b1eff245b66da4127b336a9e20af2f5fa273e08895188c0a0dffebd77d",
1209
1214
  "templates/agents/atdd-bun.integrity.test.ts": "dec6f6e5f65a08d9512fc703b367c163f8c9da1f6aa7fd466835c1ec30348a41",
1210
1215
  "templates/agents/atdd/SKILL.md": "b621abe22851a75b30fc0e6c339a2c0a3ed6bbf78b8c17000a4ce301e05df96d",
1211
- "templates/agents/delivery/SKILL.md": "9d9b80394929220f6ec1521137df02976e3240601bbc92ee6513e078442856a6",
1212
- "templates/agents/delivery/review.md": "289ee5d55ba0f321e5703a6fd8d5417d55bf0d9c6dc3103ea0f79af11c159d9c",
1216
+ "templates/agents/delivery/SKILL.md": "0c4fa25303ada52db9c6b67d51a4011c15039e92c4f2947994ec9cba4d0c5efa",
1213
1217
  "templates/github/atdd-bun-release.yml": "d9547e9e6ef3ae010d53d55314f6a54567f88bd900f7e50d994cd308601d2684",
1214
1218
  "templates/github/atdd-bun.yml": "bb42414f72f4b9a2fb1eb69530695e193f105a984542f4ca124c898a5f1489be"
1215
1219
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-bun",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/afokapu/atdd-bun.git"
@@ -13,19 +13,32 @@
13
13
  },
14
14
  "require_record": {
15
15
  "type": "boolean",
16
- "description": "At the gate, a change outside the delivery root needs a tranche record in the same change. Default: true; turning it off is a loosening."
16
+ "description": "At the gate, a change outside the delivery root needs a tranche record in the same change, unless atdd-bun.yaml is the only such file. Default: true; turning it off is a loosening."
17
17
  },
18
18
  "multiplexer": {
19
19
  "type": "string",
20
20
  "pattern": "^[a-z][a-z0-9-]*$",
21
21
  "description": "The terminal multiplexer the coordinator drives agents through, named by its command. Default: herdr. Any other name is accepted; the agent learns its commands from the multiplexer's own help."
22
22
  },
23
+ "board": {
24
+ "type": "object",
25
+ "additionalProperties": false,
26
+ "description": "Opt-in: when present, agents talk through this local message board (delivery.board); when absent there is no board. $ATDD_BOARD_URL moves it on one machine but never switches it on.",
27
+ "properties": {
28
+ "url": { "type": "string", "pattern": "^https?://(127\\.0\\.0\\.1|localhost|\\[::1\\])(:[0-9]{1,5})?/?$", "description": "A local address only. Default: http://127.0.0.1:2586." }
29
+ }
30
+ },
23
31
  "independence": { "$ref": "#/definitions/independence" },
24
32
  "stages": {
25
33
  "type": "object",
26
34
  "additionalProperties": false,
27
35
  "minProperties": 1,
28
36
  "properties": {
37
+ "plan": { "$ref": "#/definitions/stage" },
38
+ "red": { "$ref": "#/definitions/stage" },
39
+ "green": { "$ref": "#/definitions/stage" },
40
+ "refactor": { "$ref": "#/definitions/stage" },
41
+ "final": { "$ref": "#/definitions/stage" },
29
42
  "plan_review": { "$ref": "#/definitions/stage" },
30
43
  "test_review": { "$ref": "#/definitions/stage" },
31
44
  "code_review": { "$ref": "#/definitions/stage" },
@@ -57,7 +70,7 @@
57
70
  "definitions": {
58
71
  "independence": {
59
72
  "enum": ["fresh-process", "different-model"],
60
- "description": "fresh-process: the reviewer is a new process that authored nothing in the tranche. different-model: additionally, the reviewer's model differs from the stage author's."
73
+ "description": "fresh-process: the reviewer is a new process that wrote nothing in the tranche. different-model (the default): additionally, its model wrote none of the work the review covers."
61
74
  },
62
75
  "models": {
63
76
  "type": "array",
@@ -69,10 +82,12 @@
69
82
  "stage": {
70
83
  "type": "object",
71
84
  "additionalProperties": false,
72
- "required": ["reviewers"],
85
+ "anyOf": [{ "required": ["writer"] }, { "required": ["reviewer"] }, { "required": ["reviewers"] }],
73
86
  "properties": {
74
- "authors": { "$ref": "#/definitions/models" },
75
- "reviewers": { "$ref": "#/definitions/models" },
87
+ "writer": { "$ref": "#/definitions/models", "description": "Who may write this stage, in preference order. Absent: nobody writes it, as for final." },
88
+ "reviewer": { "$ref": "#/definitions/models", "description": "Who reviews this stage, in preference order. Absent: the stage is held by its gate alone." },
89
+ "authors": { "$ref": "#/definitions/models", "description": "0.9 spelling of writer, on a 0.9 stage name." },
90
+ "reviewers": { "$ref": "#/definitions/models", "description": "0.9 spelling of reviewer, on a 0.9 stage name." },
76
91
  "independence": { "$ref": "#/definitions/independence" }
77
92
  }
78
93
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "title": "Delivery Evidence Schema",
4
- "description": "Validates one <delivery root>/<tranche>/evidence.yaml: the record a tranche leaves of its reviews. Local shape only: stage completeness, allowed models and fallbacks, reviewer independence, finding outcomes and SHA resolution are cross-field validator concerns (src/delivery.ts).",
4
+ "description": "Validates one <delivery root>/<tranche>/evidence.yaml: the record a tranche leaves of who wrote each stage and of its reviews. Local shape only: stage completeness, allowed models and fallbacks, reviewer independence, finding outcomes and SHA resolution are cross-field validator concerns (src/delivery.ts).",
5
5
  "$comment": "Shipped by the delivery profile (delivery_evidence). `reviews` is chronological: a repair loop appends a new entry for the same stage rather than editing the earlier one.",
6
6
  "type": "object",
7
7
  "required": ["tranche", "status", "reviews"],
@@ -12,6 +12,7 @@
12
12
  "base_sha": { "$ref": "#/definitions/sha", "$comment": "Informational: the commit the tranche started from. Not checked; the gate derives its range from Git." },
13
13
  "approved_sha": { "$ref": "#/definitions/sha" },
14
14
  "pr": { "type": "string", "minLength": 1, "$comment": "Informational: the pull request URL. Not checked." },
15
+ "work": { "type": "array", "items": { "$ref": "#/definitions/work" }, "description": "Who wrote each stage, in order: one entry per stage written, more after a repair." },
15
16
  "reviews": { "type": "array", "items": { "$ref": "#/definitions/review" } }
16
17
  },
17
18
  "if": { "required": ["status"], "properties": { "status": { "const": "ready" } } },
@@ -32,7 +33,7 @@
32
33
  "required": ["from", "kind", "failures", "window", "reason"],
33
34
  "additionalProperties": false,
34
35
  "properties": {
35
- "role": { "enum": ["author", "reviewer"], "description": "Which actor fell back. Default: reviewer." },
36
+ "role": { "enum": ["writer", "author", "reviewer"], "description": "Which actor fell back; author is the 0.9 spelling of writer. Default: writer on a work entry, reviewer on a review." },
36
37
  "from": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
37
38
  "kind": { "enum": ["outage", "rate_limit", "no_report", "timeout"], "description": "Why the model counts as unavailable. A verdict is never one of these." },
38
39
  "failures": { "type": "integer", "minimum": 1, "description": "Failures observed before falling back; at least delivery.fallback.after_failures." },
@@ -62,14 +63,25 @@
62
63
  "decision": { "type": "string", "minLength": 5, "description": "The human's ruling when outcome is human." }
63
64
  }
64
65
  },
66
+ "work": {
67
+ "type": "object",
68
+ "required": ["stage", "sha", "writer"],
69
+ "additionalProperties": false,
70
+ "properties": {
71
+ "stage": { "enum": ["plan", "red", "green", "refactor", "final", "plan_review", "test_review", "code_review", "final_review"] },
72
+ "sha": { "$ref": "#/definitions/sha", "description": "The commit the stage's work ended at." },
73
+ "writer": { "$ref": "#/definitions/actor" },
74
+ "fallback": { "type": "array", "items": { "$ref": "#/definitions/fallback" } }
75
+ }
76
+ },
65
77
  "review": {
66
78
  "type": "object",
67
79
  "required": ["stage", "sha", "reviewer", "verdict", "checked"],
68
80
  "additionalProperties": false,
69
81
  "properties": {
70
- "stage": { "enum": ["plan_review", "test_review", "code_review", "final_review"] },
82
+ "stage": { "enum": ["plan", "red", "green", "refactor", "final", "plan_review", "test_review", "code_review", "final_review"], "description": "The stage reviewed. The 0.9 names read as plan, red, refactor and final." },
71
83
  "sha": { "$ref": "#/definitions/sha" },
72
- "author": { "$ref": "#/definitions/actor" },
84
+ "author": { "$ref": "#/definitions/actor", "$comment": "0.9: the writer of the reviewed work. Records now name writers under work." },
73
85
  "reviewer": { "$ref": "#/definitions/actor" },
74
86
  "fallback": { "type": "array", "items": { "$ref": "#/definitions/fallback" } },
75
87
  "verdict": { "enum": ["approve", "request_changes"] },
@@ -95,11 +95,14 @@ nodes:
95
95
  - coder.htmx.verb-endpoint-same-origin
96
96
  - coder.htmx.verb-mutation-signals-progress
97
97
  - delivery.approved-sha-resolves
98
+ - delivery.board
98
99
  - delivery.config-schema
99
100
  - delivery.evidence-schema
100
101
  - delivery.findings-resolved
101
102
  - delivery.merge-gate
102
103
  - delivery.model-allowed
104
+ - delivery.operating-model
105
+ - delivery.review
103
106
  - delivery.reviewer-independent
104
107
  - delivery.stages-complete
105
108
  - planner.acceptance.abstract-fields-required
@@ -5254,6 +5257,56 @@ edges:
5254
5257
  target_ref: delivery.findings-resolved
5255
5258
  type: requires
5256
5259
  origin: "@afokapu/atdd-bun"
5260
+ - confidence: 1
5261
+ constraint: mandatory
5262
+ control: internal
5263
+ foundation: start_to_start
5264
+ reason: a reviewer works read-only in a fresh process, as the independence rule judges from the record
5265
+ source_ref: delivery.review
5266
+ strength: important
5267
+ target_ref: delivery.reviewer-independent
5268
+ type: requires
5269
+ origin: "@afokapu/atdd-bun"
5270
+ - confidence: 1
5271
+ constraint: mandatory
5272
+ control: internal
5273
+ foundation: start_to_start
5274
+ reason: a reviewer settles a rebuttal in one round, as the findings rule judges from the record
5275
+ source_ref: delivery.review
5276
+ strength: important
5277
+ target_ref: delivery.findings-resolved
5278
+ type: requires
5279
+ origin: "@afokapu/atdd-bun"
5280
+ - confidence: 1
5281
+ constraint: mandatory
5282
+ control: internal
5283
+ foundation: start_to_start
5284
+ reason: the driver runs every review as the review convention describes
5285
+ source_ref: delivery.operating-model
5286
+ strength: important
5287
+ target_ref: delivery.review
5288
+ type: requires
5289
+ origin: "@afokapu/atdd-bun"
5290
+ - confidence: 1
5291
+ constraint: mandatory
5292
+ control: internal
5293
+ foundation: finish_to_start
5294
+ reason: the operating model leaves the record the evidence schema judges
5295
+ source_ref: delivery.operating-model
5296
+ strength: important
5297
+ target_ref: delivery.evidence-schema
5298
+ type: requires
5299
+ origin: "@afokapu/atdd-bun"
5300
+ - confidence: 1
5301
+ constraint: mandatory
5302
+ control: internal
5303
+ foundation: start_to_start
5304
+ reason: the coordinator and drivers dispatch work and receive results through the board
5305
+ source_ref: delivery.operating-model
5306
+ strength: important
5307
+ target_ref: delivery.board
5308
+ type: requires
5309
+ origin: "@afokapu/atdd-bun"
5257
5310
  - confidence: 1
5258
5311
  constraint: mandatory
5259
5312
  control: internal
package/src/agent.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { existsSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { deliveryAdopted } from "./delivery";
@@ -8,8 +8,10 @@ const version = (await Bun.file(join(root, "package.json")).json() as { version:
8
8
  const render = async (path: string) => (await readFile(join(root, "templates/agents", path), "utf8")).replace("{{VERSION}}", version);
9
9
  // .agents/skills is the vendor-neutral Agent Skills path (Codex, Copilot, Cursor, Gemini CLI, …); .claude/skills is Claude Code's.
10
10
  const skillPaths = [".agents/skills/atdd/SKILL.md", ".claude/skills/atdd/SKILL.md"];
11
- /** The delivery skill and its review contract, installed only where the delivery profile is adopted: generated path → template. */
12
- export const deliverySkillFiles = [".agents/skills", ".claude/skills"].flatMap(base => ["SKILL.md", "review.md"].map(name => [`${base}/delivery/${name}`, `delivery/${name}`] as const));
11
+ /** The delivery skill, installed only where the delivery profile is adopted: generated path → template. */
12
+ export const deliverySkillFiles = [".agents/skills", ".claude/skills"].map(base => [`${base}/delivery/SKILL.md`, "delivery/SKILL.md"] as const);
13
+ /** Generated files an earlier version installed and this one no longer ships: the review contract, now the skill's Reviewer section. */
14
+ const retiredFiles = [".agents/skills/delivery/review.md", ".claude/skills/delivery/review.md"];
13
15
  /** Whether the repository's atdd-bun.yaml adopts the delivery profile. */
14
16
  export async function deliveryInstalled(repo: string): Promise<boolean> {
15
17
  const file = join(repo, "atdd-bun.yaml");
@@ -35,6 +37,7 @@ export async function agentInit(repo = process.cwd(), replace = false) {
35
37
  if (existsSync(output) && !replace) { kept.push(output); continue; }
36
38
  await mkdir(dirname(output), { recursive: true }); await writeFile(output, await render(template)); written.push(output);
37
39
  }
40
+ for (const path of retiredFiles) if (existsSync(join(repo, path))) { await rm(join(repo, path)); written.push(`${join(repo, path)} (removed)`); }
38
41
  const managed = await render("AGENTS.block.md");
39
42
  for (const path of instructionPaths) {
40
43
  const file = join(repo, path), current = existsSync(file) ? await readFile(file, "utf8") : "";
@@ -0,0 +1,91 @@
1
+ import { allowedTopics, DIRECTORY, format, read, type Message } from "./board";
2
+
3
+ /** `atdd-bun chat` alone: a terminal view of the board, every topic in a panel on the left and the selected topic's
4
+ * conversation on the right, refreshed every second. ntfy cannot list its topics, so the board keeps a directory
5
+ * topic (atdd-topics) that atdd-bun chat post fills with each topic's name on its first message. */
6
+
7
+ /** Every topic the directory names, in order. */
8
+ export async function knownTopics(url: string): Promise<string[]> {
9
+ const entries = await read(url, [DIRECTORY]).catch(() => [] as Message[]);
10
+ return [...new Set(entries.map(entry => entry.raw.trim()).filter(topic => /^[A-Za-z0-9_-]{1,64}$/.test(topic)))].sort((a, b) => a.localeCompare(b, "en", { numeric: true }));
11
+ }
12
+
13
+ export type UiState = { topics: string[]; messages: Map<string, Message[]>; selected: number; scroll: number; url: string };
14
+
15
+ /** A topic's parent is the longest other topic it extends (atdd-p-t3-final-1 under atdd-p-t3 under atdd-p). */
16
+ function tree(topics: string[]): Array<{ topic: string; depth: number; label: string }> {
17
+ return topics.map(topic => {
18
+ const ancestors = topics.filter(other => other !== topic && topic.startsWith(`${other}-`));
19
+ const parent = ancestors.sort((a, b) => b.length - a.length)[0];
20
+ return { topic, depth: ancestors.length, label: parent ? topic.slice(parent.length + 1) : topic };
21
+ });
22
+ }
23
+
24
+ const fit = (text: string, width: number) => text.length > width ? `${text.slice(0, Math.max(0, width - 1))}…` : text.padEnd(width);
25
+ function wrap(text: string, width: number): string[] {
26
+ const out: string[] = [];
27
+ for (const line of text.split("\n")) {
28
+ if (!line) { out.push(""); continue; }
29
+ for (let i = 0; i < line.length; i += width) out.push(line.slice(i, i + width));
30
+ }
31
+ return out;
32
+ }
33
+
34
+ /** The screen as lines, without escape codes except the selection's inverse video: pure, so it can be tested. */
35
+ export function render(state: UiState, width: number, height: number): string[] {
36
+ const left = Math.max(20, Math.min(40, Math.floor(width / 3))), right = Math.max(10, width - left - 3), rows = Math.max(1, height - 2);
37
+ const nodes = tree(state.topics), selected = nodes[state.selected]?.topic;
38
+ const panel = nodes.map((node, i) => {
39
+ const count = String(state.messages.get(node.topic)?.length ?? 0);
40
+ const text = `${" ".repeat(node.depth)}${node.label}`;
41
+ const cell = `${fit(text, left - count.length - 1)} ${count}`;
42
+ return i === state.selected ? `\x1b[7m${cell}\x1b[0m` : cell;
43
+ });
44
+ if (!panel.length) panel.push(fit("no topics yet", left));
45
+ const body = selected ? (state.messages.get(selected) ?? []).flatMap(message => wrap(format(message), right)) : wrap("Nothing posted yet. Topics appear here as agents post to them.", right);
46
+ const end = Math.max(0, body.length - state.scroll), start = Math.max(0, end - rows), view = body.slice(start, end);
47
+ const first = Math.max(0, Math.min(state.selected - Math.floor(rows / 2), panel.length - rows));
48
+ const header = fit(` Delivery board ${state.url} · ${state.topics.length} topics · ↑↓ topic PgUp/PgDn scroll q quit`, width);
49
+ const lines = [header, `${"─".repeat(left)}─┬─${"─".repeat(right)}`];
50
+ for (let row = 0; row < rows - 1; row++) lines.push(`${panel[first + row] ?? " ".repeat(left)} │ ${fit(view[row] ?? "", right)}`);
51
+ lines.push(fit(selected ? ` ${selected}${state.scroll ? ` (scrolled up ${state.scroll})` : ""}` : "", width));
52
+ return lines;
53
+ }
54
+
55
+ /** Runs the view until q. Without a terminal it prints the topic list and exits. */
56
+ export async function runUi(url: string, env: Record<string, string | undefined> = process.env): Promise<number> {
57
+ const visible = async () => { const all = await knownTopics(url); return env.ATDD_AGENT ? all.filter(topic => allowedTopics(env).includes(topic)) : all; };
58
+ const state: UiState = { topics: [], messages: new Map(), selected: 0, scroll: 0, url };
59
+ const refresh = async () => {
60
+ state.topics = await visible();
61
+ for (const topic of state.topics) {
62
+ const known = state.messages.get(topic) ?? [], since = known.at(-1)?.id ?? "all";
63
+ const fresh = await read(url, [topic], since).catch(() => [] as Message[]);
64
+ if (fresh.length || !state.messages.has(topic)) state.messages.set(topic, [...known, ...fresh]);
65
+ }
66
+ };
67
+ await refresh();
68
+ if (!process.stdout.isTTY || !process.stdin.isTTY) {
69
+ for (const topic of state.topics) console.log(`${topic} ${state.messages.get(topic)?.length ?? 0}`);
70
+ return 0;
71
+ }
72
+ const draw = () => process.stdout.write(`\x1b[H\x1b[2J${render(state, process.stdout.columns || 100, process.stdout.rows || 30).join("\n")}`);
73
+ process.stdout.write("\x1b[?1049h\x1b[?25l");
74
+ process.stdin.setRawMode(true); process.stdin.resume();
75
+ let done: (code: number) => void = () => {};
76
+ const finished = new Promise<number>(resolve => { done = resolve; });
77
+ const quit = () => { clearInterval(timer); process.stdin.setRawMode(false); process.stdin.pause(); process.stdout.write("\x1b[?25h\x1b[?1049l"); done(0); };
78
+ process.stdin.on("data", (data: Buffer) => {
79
+ const key = data.toString();
80
+ if (key === "q" || key === "\x03") return quit();
81
+ if (key === "\x1b[A" || key === "k") { state.selected = Math.max(0, state.selected - 1); state.scroll = 0; }
82
+ if (key === "\x1b[B" || key === "j") { state.selected = Math.min(state.topics.length - 1, state.selected + 1); state.scroll = 0; }
83
+ if (key === "\x1b[5~") state.scroll += 10;
84
+ if (key === "\x1b[6~") state.scroll = Math.max(0, state.scroll - 10);
85
+ draw();
86
+ });
87
+ process.stdout.on("resize", draw);
88
+ const timer = setInterval(async () => { await refresh(); draw(); }, 1000);
89
+ draw();
90
+ return finished;
91
+ }
package/src/board.ts ADDED
@@ -0,0 +1,167 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { runUi } from "./board-ui";
6
+
7
+ /** The delivery board: a local ntfy server the coordinator, drivers, writers and reviewers talk through
8
+ * (the delivery.board convention). One topic per level (program, tranche, review conversation); every message
9
+ * carries a header naming its sender, recipients and subject. The board is where the work is visible; the
10
+ * evidence record stays the only thing the delivery rules judge. */
11
+
12
+ export const DEFAULT_BOARD_URL = "http://127.0.0.1:2586";
13
+ const LOCAL_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
14
+ const TOPIC_LIMIT = 64;
15
+ /** The board's directory: one message per topic, its name, posted with the topic's first message. */
16
+ export const DIRECTORY = "atdd-topics";
17
+
18
+ /** The board is opt-in: on only when atdd-bun.yaml names delivery.board. Its URL is $ATDD_BOARD_URL, else
19
+ * delivery.board.url, else the default; the environment moves the board on one machine but never switches it on. Only a
20
+ * local address is accepted, so a wrong setting can never send a message to a public server. */
21
+ export async function boardUrl(root = process.cwd(), env: Record<string, string | undefined> = process.env): Promise<string> {
22
+ let board: unknown;
23
+ const file = join(root, "atdd-bun.yaml");
24
+ if (existsSync(file)) try { board = (Bun.YAML.parse(await readFile(file, "utf8")) as { delivery?: { board?: unknown } } | null)?.delivery?.board; } catch { /* reported by delivery.config-schema */ }
25
+ if (!board || typeof board !== "object" || Array.isArray(board)) throw new Error("the board is not enabled; add delivery.board to atdd-bun.yaml (for example board: { url: http://127.0.0.1:2586 }) to have agents talk through it");
26
+ const configured = (board as { url?: unknown }).url;
27
+ return localUrl(env.ATDD_BOARD_URL || (typeof configured === "string" ? configured : DEFAULT_BOARD_URL));
28
+ }
29
+
30
+ export function localUrl(raw: string): string {
31
+ let url: URL;
32
+ try { url = new URL(raw); } catch { throw new Error(`board URL ${raw} is not a URL`); }
33
+ if (!/^https?:$/.test(url.protocol) || !LOCAL_HOSTS.has(url.hostname)) throw new Error(`board URL ${raw} is not local; the board runs on this machine (127.0.0.1, localhost or [::1])`);
34
+ return `${url.protocol}//${url.host}`;
35
+ }
36
+
37
+ /** A topic name derived from its level: atdd-<program>[-<tranche>[-<stage>-<round>]]. Parts are lowercased and
38
+ * reduced to [a-z0-9-]; a name longer than ntfy's 64 characters keeps its start and a hash of the whole. */
39
+ export function topicName(program: string, tranche?: string, stage?: string, round?: string | number): string {
40
+ const parts = [program, tranche, stage, round === undefined ? undefined : String(round)].filter((part): part is string => part !== undefined && part !== "");
41
+ if (!parts.length) throw new Error("a topic needs at least the program name");
42
+ if (stage !== undefined && round === undefined) throw new Error("a review conversation topic needs its stage and round");
43
+ const slug = (part: string) => part.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
44
+ const name = ["atdd", ...parts.map(slug)].join("-");
45
+ if (name.length <= TOPIC_LIMIT) return name;
46
+ const hash = createHash("sha256").update(name).digest("hex").slice(0, 8);
47
+ return `${name.slice(0, TOPIC_LIMIT - 9).replace(/-+$/, "")}-${hash}`;
48
+ }
49
+
50
+ /** Topics this agent may use, from $ATDD_TOPICS (comma-separated), given by whoever launched it. */
51
+ export function allowedTopics(env: Record<string, string | undefined> = process.env): string[] {
52
+ return (env.ATDD_TOPICS ?? "").split(",").map(topic => topic.trim()).filter(Boolean);
53
+ }
54
+
55
+ export function checkTopic(topic: string, env: Record<string, string | undefined> = process.env): void {
56
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(topic)) throw new Error(`${topic} is not a topic name; derive one with atdd-bun chat topic`);
57
+ const allowed = allowedTopics(env);
58
+ if (!allowed.includes(topic)) throw new Error(`${topic} is not one of this agent's topics (ATDD_TOPICS: ${allowed.join(", ") || "none"}); an agent reads and writes only the topics it was launched with`);
59
+ }
60
+
61
+ export const HEADER_FIELDS = ["kind", "stage", "reply-to", "repo", "branch", "worktree", "goal", "base", "sha"] as const;
62
+ export type Header = { from: string; to: string[]; participants: string[]; conversation: string } & Partial<Record<(typeof HEADER_FIELDS)[number], string>>;
63
+ export type Message = { id: string; time: number; topic: string; header: Partial<Header>; body: string; raw: string };
64
+
65
+ export function envelope(header: Header, body: string): string {
66
+ const value = (v: string | string[]) => Array.isArray(v) ? `[${v.join(", ")}]` : JSON.stringify(v);
67
+ return `---\n${Object.entries(header).filter(([, v]) => v !== undefined).map(([k, v]) => `${k}: ${value(v as string | string[])}`).join("\n")}\n---\n${body.trim()}`;
68
+ }
69
+
70
+ export function parseEnvelope(text: string): { header: Partial<Header>; body: string } {
71
+ const match = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
72
+ if (!match) return { header: {}, body: text };
73
+ try { return { header: (Bun.YAML.parse(match[1]) ?? {}) as Partial<Header>, body: match[2] }; } catch { return { header: {}, body: text }; }
74
+ }
75
+
76
+ const addressedTo = (message: Message, agent: string) => [message.header.to ?? []].flat().includes(agent);
77
+
78
+ /** Posts with a few retries (a busy or restarting board), then fails loudly: a message is never silently lost. The first
79
+ * message on a topic also names the topic in the directory, so the board's view can list it. */
80
+ export async function post(url: string, topic: string, header: Header, body: string): Promise<string> {
81
+ const first = await fetch(`${url}/${topic}/json?poll=1&since=latest`).then(res => res.ok ? res.text() : "x").then(text => !text.trim()).catch(() => false);
82
+ let error = "";
83
+ for (let attempt = 0; attempt < 5; attempt++) {
84
+ try {
85
+ const res = await fetch(`${url}/${topic}`, { method: "POST", body: envelope(header, body), headers: { Title: `${header.from} -> ${header.to.join(", ")}` } });
86
+ if (res.ok) {
87
+ const id = (await res.json() as { id: string }).id;
88
+ if (first) await fetch(`${url}/${DIRECTORY}`, { method: "POST", body: topic }).catch(() => undefined);
89
+ return id;
90
+ }
91
+ error = `${res.status} ${await res.text()}`;
92
+ } catch (e) { error = String(e); }
93
+ await Bun.sleep(250 * 2 ** attempt);
94
+ }
95
+ throw new Error(`post to ${topic} failed: ${error}`);
96
+ }
97
+
98
+ /** Every message on the topics after `since` (a message id, or "all"). */
99
+ export async function read(url: string, topics: string[], since = "all"): Promise<Message[]> {
100
+ const res = await fetch(`${url}/${topics.join(",")}/json?poll=1&since=${since}`);
101
+ if (!res.ok) throw new Error(`reading ${topics.join(", ")} failed: ${res.status} ${await res.text()}`);
102
+ return (await res.text()).split("\n").filter(Boolean).map(line => JSON.parse(line)).filter(m => m.event === "message")
103
+ .map(m => ({ id: m.id, time: m.time, topic: m.topic, raw: m.message, ...parseEnvelope(m.message) }));
104
+ }
105
+
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> {
109
+ const deadline = timeoutSeconds > 0 ? Date.now() + timeoutSeconds * 1000 : Infinity;
110
+ for (;;) {
111
+ 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; }
114
+ if (Date.now() >= deadline) return null;
115
+ await Bun.sleep(intervalMs);
116
+ }
117
+ }
118
+
119
+ export function format(message: Message, withTopic = false): string {
120
+ const time = new Date(message.time * 1000).toTimeString().slice(0, 8), h = message.header;
121
+ const route = `${h.from ?? "?"} -> ${[h.to ?? []].flat().join(", ") || "?"}`, tags = [h.kind, h.stage].filter(Boolean).join(" · ");
122
+ return `${withTopic ? `[${message.topic}] ` : ""}${time} ${route}${tags ? ` (${tags})` : ""} #${message.id}\n${message.body.trim()}\n`;
123
+ }
124
+
125
+ /** `atdd-bun chat` opens the board's terminal view; `atdd-bun chat <topic|post|read|wait> …` is for agents.
126
+ * Returns the exit code: 0 done, 1 error, 2 wait timed out. */
127
+ export async function chat(args: string[], env: Record<string, string | undefined> = process.env, root = process.cwd()): Promise<number> {
128
+ const [command, ...rest] = args;
129
+ const flag = (name: string) => { const i = rest.indexOf(`--${name}`); return i >= 0 ? rest[i + 1] : undefined; };
130
+ const list = (value?: string) => (value ?? "").split(",").map(item => item.trim()).filter(Boolean);
131
+ const positional = rest.filter((arg, i) => !arg.startsWith("--") && !(i > 0 && rest[i - 1].startsWith("--") && rest[i - 1] !== "--mine"));
132
+ const agent = () => { if (!env.ATDD_AGENT) throw new Error("ATDD_AGENT is not set: every agent is launched with its identity"); return env.ATDD_AGENT; };
133
+ try {
134
+ if (command === "topic") { console.log(topicName(positional[0], positional[1], positional[2], positional[3])); return 0; }
135
+ const url = await boardUrl(root, env);
136
+ if (command === undefined) return await runUi(url, env);
137
+ const topics = list(positional[0]);
138
+ if (!topics.length) throw new Error(`usage: atdd-bun chat ${command} <topic> …`);
139
+ if (topics.length !== 1) throw new Error(`${command} takes one topic`);
140
+ const topic = topics[0], me = agent();
141
+ checkTopic(topic, env);
142
+ if (command === "post") {
143
+ const to = list(flag("to"));
144
+ if (!to.length) throw new Error("--to is required: every message names its recipients");
145
+ const body = await new Response(Bun.stdin.stream()).text();
146
+ if (!body.trim()) throw new Error("the message body is read from standard input and is empty");
147
+ const header: Header = { from: me, to, participants: [...new Set([me, ...to, ...list(flag("participants"))])], conversation: topic };
148
+ for (const field of HEADER_FIELDS) if (flag(field)) header[field] = flag(field);
149
+ console.log(await post(url, topic, header, body));
150
+ return 0;
151
+ }
152
+ if (command === "read") {
153
+ for (const message of await read(url, [topic], flag("since") || "all")) if (!rest.includes("--mine") || addressedTo(message, me)) console.log(format(message));
154
+ return 0;
155
+ }
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
+ throw new Error(`unknown chat command ${command}; run atdd-bun chat alone for the board, or topic, post, read, wait`);
163
+ } catch (error) {
164
+ console.error(error instanceof Error ? error.message : String(error));
165
+ return 1;
166
+ }
167
+ }
package/src/cli.ts CHANGED
@@ -7,6 +7,7 @@ import { checkIntegrity, formatIntegrity, integrityInit, integrityStatus } from
7
7
  import { journeyDocs } from "./journey-docs";
8
8
  import { releaseCheck } from "./release";
9
9
  import { initializeRepository } from "./setup";
10
+ import { chat } from "./board";
10
11
 
11
12
  const args = process.argv.slice(2);
12
13
  const usage = {
@@ -21,6 +22,11 @@ const usage = {
21
22
  "atdd-bun integrity [init|status] [--replace]",
22
23
  "atdd-bun docs journeys [--out <dir>] [--check] [--force]",
23
24
  "atdd-bun release check",
25
+ "atdd-bun chat topic <program> [<tranche> [<stage> <round>]]",
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
+ "atdd-bun chat read <topic> [--mine] [--since ID]",
28
+ "atdd-bun chat wait <topic> [--since ID] [--timeout SECONDS]",
29
+ "atdd-bun chat (the board: every topic on the left, the conversation on the right)",
24
30
  ],
25
31
  profiles: profileNames,
26
32
  note: "Use a profile directly, for example: atdd-bun planner. --profile planner remains supported for compatibility.",
@@ -88,6 +94,7 @@ if (args[0] === "agent") {
88
94
  const result = args[1] === "init" ? await agentInit(process.cwd(), args.includes("--replace")) : args[1] === "status" ? await agentStatus() : fail("agent requires init or status");
89
95
  console[result.ok ? "log" : "error"](result.message); process.exit(result.ok ? 0 : 1);
90
96
  }
97
+ if (args[0] === "chat") process.exit(await chat(args.slice(1)));
91
98
  if (args[0] === "release") {
92
99
  if (args[1] !== "check") fail("release requires check");
93
100
  const result = await releaseCheck(); console[result.ok ? "log" : "error"](result.message); process.exit(result.ok ? 0 : 1);