@afokapu/atdd-bun 0.10.31 → 0.10.33

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.
Files changed (66) hide show
  1. package/README.md +14 -120
  2. package/conventions/_profiles/architecture.yaml +190 -0
  3. package/conventions/_profiles/coder.yaml +804 -0
  4. package/conventions/_profiles/design.yaml +219 -0
  5. package/conventions/_profiles/docs.yaml +136 -0
  6. package/conventions/_profiles/htmx.yaml +436 -0
  7. package/conventions/_profiles/interlocking.yaml +248 -0
  8. package/conventions/_profiles/metrics.yaml +80 -0
  9. package/conventions/_profiles/planner.yaml +789 -0
  10. package/conventions/_profiles/runtime.yaml +62 -0
  11. package/conventions/_profiles/security.yaml +70 -0
  12. package/conventions/_profiles/telemetry.yaml +196 -0
  13. package/conventions/_profiles/tester.yaml +734 -0
  14. package/conventions/_profiles/topology.yaml +192 -0
  15. package/conventions/_profiles/traceability.yaml +94 -0
  16. package/conventions/atdd-bun.profiles/atdd-bun.profiles.presets.convention.yaml +21 -3
  17. package/conventions/planner.docs/planner.docs.area-index-required.convention.yaml +2 -2
  18. package/conventions/planner.docs/planner.docs.artifact-path-shape.convention.yaml +1 -1
  19. package/conventions/planner.docs/planner.docs.asciidoc-only.convention.yaml +0 -1
  20. package/conventions/planner.docs/planner.docs.identity-required.convention.yaml +1 -1
  21. package/conventions/planner.docs/planner.docs.undeclared-change.convention.yaml +1 -2
  22. package/detectors/bun_interlocking_coverage/checks/interlocking_train_sequence.mjs +7 -5
  23. package/detectors/planner_docs_capability/README.md +1 -1
  24. package/integrity.json +36 -50
  25. package/package.json +1 -1
  26. package/relationships.yaml +0 -151
  27. package/src/agent.ts +15 -31
  28. package/src/cli.ts +7 -8
  29. package/src/docs-capability.ts +6 -24
  30. package/src/enforce.ts +8 -9
  31. package/src/hooks.ts +4 -8
  32. package/src/index.ts +1 -2
  33. package/src/integrity.ts +3 -11
  34. package/src/profile-registry.ts +74 -0
  35. package/src/setup.ts +3 -5
  36. package/templates/agents/AGENTS.block.md +4 -8
  37. package/templates/github/atdd-bun.yml +0 -5
  38. package/conventions/delivery/delivery.approved-sha-resolves.convention.yaml +0 -33
  39. package/conventions/delivery/delivery.board.convention.yaml +0 -60
  40. package/conventions/delivery/delivery.config-schema.convention.yaml +0 -49
  41. package/conventions/delivery/delivery.evidence-schema.convention.yaml +0 -34
  42. package/conventions/delivery/delivery.findings-resolved.convention.yaml +0 -42
  43. package/conventions/delivery/delivery.merge-gate.convention.yaml +0 -46
  44. package/conventions/delivery/delivery.model-allowed.convention.yaml +0 -29
  45. package/conventions/delivery/delivery.operating-model.convention.yaml +0 -97
  46. package/conventions/delivery/delivery.review.convention.yaml +0 -42
  47. package/conventions/delivery/delivery.reviewer-independent.convention.yaml +0 -34
  48. package/conventions/delivery/delivery.stages-complete.convention.yaml +0 -33
  49. package/detectors/delivery_evidence/atdd.implementation.yaml +0 -33
  50. package/detectors/delivery_evidence/detect.mjs +0 -9
  51. package/detectors/delivery_evidence/fixtures/clean/atdd-bun.yaml +0 -8
  52. package/detectors/delivery_evidence/fixtures/clean/docs/delivery/tranches/api/evidence.yaml +0 -54
  53. package/detectors/delivery_evidence/fixtures/dirty/atdd-bun.yaml +0 -3
  54. package/detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/api/evidence.yaml +0 -31
  55. package/detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/data/evidence.yaml +0 -8
  56. package/detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/orphan/README.md +0 -1
  57. package/detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/ui/evidence.yaml +0 -5
  58. package/detectors/planner_docs_capability/fixtures/dirty_missing_index/docs/delivery/release.adoc +0 -10
  59. package/detectors/planner_docs_capability/fixtures/dirty_missing_index/docs/purpose/index.adoc +0 -5
  60. package/planner-schemas/delivery-config.schema.json +0 -95
  61. package/planner-schemas/delivery-evidence.schema.json +0 -103
  62. package/src/board-ui.ts +0 -91
  63. package/src/board.ts +0 -222
  64. package/src/delivery.ts +0 -567
  65. package/templates/agents/atdd/SKILL.md +0 -20
  66. package/templates/agents/delivery/SKILL.md +0 -15
@@ -1,9 +0,0 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
- import { dirname } from "node:path";
3
- import { validateDelivery } from "../../src/delivery.ts";
4
-
5
- const roots = JSON.parse(process.env.ATDD_SCAN_ROOTS ?? "[]"), report = process.env.ATDD_VIOLATIONS_REPORT;
6
- if (!report) throw new Error("ATDD_VIOLATIONS_REPORT is required");
7
- const violations = (await Promise.all(roots.map(root => validateDelivery(root)))).flat().map(item => ({ ...item, line: 1, col: 1, source_line: "" }));
8
- await mkdir(dirname(report), { recursive: true });
9
- await writeFile(report, JSON.stringify({ violations }, null, 2));
@@ -1,8 +0,0 @@
1
- profiles: [delivery]
2
- delivery:
3
- independence: fresh-process
4
- stages:
5
- plan_review: { authors: [codex], reviewers: [glm, claude] }
6
- test_review: { authors: [glm, claude], reviewers: [codex, claude] }
7
- code_review: { authors: [glm, claude], reviewers: [glm, claude], independence: different-model }
8
- final_review: { authors: [codex], reviewers: [codex, claude] }
@@ -1,54 +0,0 @@
1
- # In progress: every stage reviewed, the code-review dispute settled, the PR not yet marked ready.
2
- tranche: api
3
- status: open
4
- base_sha: 3f2a91c
5
- reviews:
6
- - stage: plan_review
7
- sha: 8b10e44
8
- author: { model: codex, run: driver-api }
9
- reviewer: { model: glm, run: glm-plan-1 }
10
- verdict: approve
11
- checked: [ACC-API-001, ACC-API-002, planner.decomposition]
12
- - stage: test_review
13
- sha: 91aa0b2
14
- author: { model: glm, run: glm-red-1 }
15
- reviewer: { model: codex, run: codex-test-1 }
16
- verdict: approve
17
- checked: [ACC-API-001, ACC-API-002, tester.bun.red]
18
- - stage: code_review
19
- sha: a4c0f11
20
- author: { model: glm, run: glm-green-1 }
21
- reviewer: { model: claude, run: claude-code-1 }
22
- fallback:
23
- - { from: glm, kind: rate_limit, failures: 3, window: { from: "2026-09-25T09:00:00Z", to: "2026-09-25T09:08:00Z" }, reason: "rate limit: 3 failures in 10 minutes" }
24
- verdict: request_changes
25
- checked: [ACC-API-001, ACC-API-002, src/wagons/api, coder.bun.error-response]
26
- findings:
27
- - id: F1
28
- severity: high
29
- evidence: src/wagons/api/handler.ts:42 returns a bare string
30
- invariant: every error response carries a coded body
31
- affects: [ui]
32
- proposed_fix: return { code "API_NOT_FOUND" } instead of the bare string
33
- outcome: fixed
34
- - id: F2
35
- severity: medium
36
- evidence: src/wagons/api/handler.ts:57 retries without a bound
37
- invariant: outbound calls are bounded
38
- proposed_fix: cap the retry loop at three attempts
39
- rebuttal: "The loop is bounded by the caller's AbortSignal; see handler.test.ts:31 which proves it stops."
40
- outcome: withdrawn
41
- - stage: code_review
42
- sha: c77d0a2
43
- author: { model: glm, run: glm-green-2 }
44
- reviewer: { model: claude, run: claude-code-2 }
45
- fallback:
46
- - { from: glm, kind: rate_limit, failures: 3, window: { from: "2026-09-25T09:00:00Z", to: "2026-09-25T09:08:00Z" }, reason: "rate limit: 3 failures in 10 minutes" }
47
- verdict: approve
48
- checked: [ACC-API-001, ACC-API-002, src/wagons/api, F1, F2]
49
- - stage: final_review
50
- sha: c77d0a2
51
- author: { model: codex, run: driver-api }
52
- reviewer: { model: codex, run: codex-final-1 }
53
- verdict: approve
54
- checked: [src/wagons/api, contracts, ui]
@@ -1,3 +0,0 @@
1
- profiles: [delivery]
2
- delivery:
3
- independence: loose
@@ -1,31 +0,0 @@
1
- # Ready, but the approved SHA is not what final_review approved and exists nowhere, a reviewer is off-list,
2
- # one reviewer also authored, and a finding was never resolved.
3
- tranche: api
4
- status: ready
5
- base_sha: 3f2a91c
6
- approved_sha: "0000000"
7
- reviews:
8
- - stage: plan_review
9
- sha: 8b10e44
10
- author: { model: codex, run: driver-api }
11
- reviewer: { model: gpt, run: gpt-plan-1 }
12
- verdict: approve
13
- checked: [ACC-API-001]
14
- - stage: final_review
15
- sha: 91aa0b2
16
- author: { model: codex, run: codex-green-1 }
17
- reviewer: { model: codex, run: driver-api }
18
- verdict: request_changes
19
- checked: [ACC-API-001]
20
- findings:
21
- - id: F1
22
- severity: high
23
- evidence: src/wagons/api/handler.ts:42 returns a bare string
24
- invariant: every error response carries a coded body
25
- proposed_fix: return a coded error body
26
- - stage: final_review
27
- sha: a4c0f11
28
- author: { model: codex, run: codex-green-2 }
29
- reviewer: { model: codex, run: codex-final-2 }
30
- verdict: approve
31
- checked: [ACC-API-001, F1]
@@ -1,8 +0,0 @@
1
- # A reviewer without a verdict or a checklist.
2
- tranche: data
3
- status: open
4
- base_sha: 3f2a91c
5
- reviews:
6
- - stage: plan_review
7
- sha: 8b10e44
8
- reviewer: { model: glm, run: glm-plan-1 }
@@ -1 +0,0 @@
1
- The orphan tranche has a folder and no evidence.yaml.
@@ -1,5 +0,0 @@
1
- # The folder says ui, the record says web.
2
- tranche: web
3
- status: open
4
- base_sha: 3f2a91c
5
- reviews: []
@@ -1,10 +0,0 @@
1
- = Release
2
- :doc-id: delivery.release
3
- :status: current
4
-
5
- The delivery area exists and holds this document — but carries no index.adoc.
6
-
7
- This file is what makes the fixture REAL. Expressing "the area exists with no
8
- index" as an empty directory made the defect invisible to git, so the fixture
9
- arrived empty on a fresh clone and the rule it proves never fired. A fixture that
10
- only works on the machine that authored it proves nothing.
@@ -1,5 +0,0 @@
1
- = Purpose
2
- :doc-id: purpose.index
3
- :status: current
4
-
5
- WHY and WHAT.
@@ -1,95 +0,0 @@
1
- {
2
- "$schema": "http://json-schema.org/draft-07/schema#",
3
- "title": "Delivery Profile Configuration",
4
- "description": "The `delivery:` block of atdd-bun.yaml: which models may author and review each lifecycle stage, in preference order, how independent a reviewer must be, and when a model counts as unavailable. Every key is optional; an omitted key takes the package default (src/delivery.ts). `stages`, when given, replaces the default stage set: a stage it omits is not required.",
5
- "$comment": "Shipped by the delivery profile (delivery_evidence). Cross-field checks (a reviewer listed twice, a stage override naming an unknown mode) are schema-level; loosening against the base branch is judged by the integrity check.",
6
- "type": "object",
7
- "additionalProperties": false,
8
- "properties": {
9
- "root": {
10
- "type": "string",
11
- "pattern": "^[a-z0-9_][a-z0-9_.-]*(?:/[a-z0-9_][a-z0-9_.-]*)*$",
12
- "description": "Repository-relative folder holding one <tranche>/evidence.yaml per tranche, in canonical form: no leading, trailing or doubled slash, no dot segments. Default: docs/delivery/tranches, beside the program's reasoning in the docs profile's delivery area. Changing it after adoption is a loosening the integrity check reports."
13
- },
14
- "require_record": {
15
- "type": "boolean",
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
- },
18
- "multiplexer": {
19
- "type": "string",
20
- "pattern": "^[a-z][a-z0-9-]*$",
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
- },
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
- },
31
- "independence": { "$ref": "#/definitions/independence" },
32
- "stages": {
33
- "type": "object",
34
- "additionalProperties": false,
35
- "minProperties": 1,
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" },
42
- "plan_review": { "$ref": "#/definitions/stage" },
43
- "test_review": { "$ref": "#/definitions/stage" },
44
- "code_review": { "$ref": "#/definitions/stage" },
45
- "final_review": { "$ref": "#/definitions/stage" }
46
- }
47
- },
48
- "fallback": {
49
- "type": "object",
50
- "additionalProperties": false,
51
- "properties": {
52
- "after_failures": { "type": "integer", "minimum": 1, "description": "Failures (outage, rate limit, no auditable report) before a model counts as unavailable. A REQUEST CHANGES verdict is never a failure." },
53
- "within_minutes": { "type": "integer", "minimum": 1 },
54
- "when_exhausted": { "enum": ["block", "wait"], "description": "block: the tranche emits BLOCKED provider-unavailable and the coordinator reassigns the slot. wait: the driver keeps retrying the last model in the list." }
55
- }
56
- },
57
- "commands": {
58
- "type": "object",
59
- "description": "Headless invocation per model, overriding the skill's built-in defaults. {prompt} and {worktree} are substituted by the driver. Adding or changing an entry after adoption is a loosening the integrity check reports; removing one returns to the default.",
60
- "additionalProperties": {
61
- "type": "object",
62
- "additionalProperties": false,
63
- "properties": {
64
- "author": { "type": "string", "minLength": 1 },
65
- "review": { "type": "string", "minLength": 1 }
66
- }
67
- }
68
- }
69
- },
70
- "definitions": {
71
- "independence": {
72
- "enum": ["fresh-process", "different-model"],
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."
74
- },
75
- "models": {
76
- "type": "array",
77
- "minItems": 1,
78
- "uniqueItems": true,
79
- "items": { "type": "string", "pattern": "^(\\*|[a-z][a-z0-9-]*)$" },
80
- "description": "The models allowed for the role, in no order: any listed model may do the work, and \"*\" allows any model."
81
- },
82
- "stage": {
83
- "type": "object",
84
- "additionalProperties": false,
85
- "anyOf": [{ "required": ["writer"] }, { "required": ["reviewer"] }, { "required": ["reviewers"] }],
86
- "properties": {
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." },
91
- "independence": { "$ref": "#/definitions/independence" }
92
- }
93
- }
94
- }
95
- }
@@ -1,103 +0,0 @@
1
- {
2
- "$schema": "http://json-schema.org/draft-07/schema#",
3
- "title": "Delivery Evidence Schema",
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
- "$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
- "type": "object",
7
- "required": ["tranche", "status", "reviews"],
8
- "additionalProperties": false,
9
- "properties": {
10
- "tranche": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$", "description": "Must equal the tranche folder name." },
11
- "status": { "enum": ["open", "ready"], "description": "open: the lifecycle is in progress. ready: every configured stage is approved and approved_sha is the commit to merge." },
12
- "base_sha": { "$ref": "#/definitions/sha", "$comment": "Informational: the commit the tranche started from. Not checked; the gate derives its range from Git." },
13
- "approved_sha": { "$ref": "#/definitions/sha" },
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." },
16
- "reviews": { "type": "array", "items": { "$ref": "#/definitions/review" } }
17
- },
18
- "if": { "required": ["status"], "properties": { "status": { "const": "ready" } } },
19
- "then": { "required": ["approved_sha"] },
20
- "definitions": {
21
- "sha": { "type": "string", "pattern": "^[0-9a-f]{7,40}$" },
22
- "actor": {
23
- "type": "object",
24
- "required": ["model", "run"],
25
- "additionalProperties": false,
26
- "properties": {
27
- "model": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
28
- "run": { "type": "string", "minLength": 1, "description": "Identifies the process: a session id, a pane id plus start time, or the report file. Two entries with the same run are the same process." }
29
- }
30
- },
31
- "fallback": {
32
- "type": "object",
33
- "required": ["from", "kind", "reason"],
34
- "additionalProperties": false,
35
- "description": "Why a later model in the stage's list was used: counted failures of the preferred one, or the operator's order (kind operator), which references its ruling instead and is accepted only for a model the stage lists.",
36
- "if": { "required": ["kind"], "properties": { "kind": { "const": "operator" } } },
37
- "then": { "required": ["decided_by", "decided_at", "ruling"], "not": { "anyOf": [{ "required": ["failures"] }, { "required": ["window"] }] } },
38
- "else": { "required": ["failures", "window"] },
39
- "properties": {
40
- "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." },
41
- "from": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
42
- "kind": { "enum": ["outage", "rate_limit", "no_report", "timeout", "operator"], "description": "Why the model counts as unavailable, or operator when the operator ordered the substitution. A verdict is never one of these." },
43
- "failures": { "type": "integer", "minimum": 1, "description": "Failures observed before falling back; at least delivery.fallback.after_failures." },
44
- "window": {
45
- "type": "object",
46
- "required": ["from", "to"],
47
- "additionalProperties": false,
48
- "description": "When the first and last counted failures happened; the span is at most delivery.fallback.within_minutes.",
49
- "properties": { "from": { "type": "string", "format": "date-time" }, "to": { "type": "string", "format": "date-time" } }
50
- },
51
- "decided_by": { "type": "string", "minLength": 2, "description": "operator only: who ruled, by name or board identity. Never the driver on its own authority." },
52
- "decided_at": { "type": "string", "format": "date-time", "description": "operator only: when the ruling was given." },
53
- "ruling": { "type": "string", "minLength": 3, "description": "operator only: where the ruling is recorded (a board message id, an ADR or a PR)." },
54
- "reason": { "type": "string", "minLength": 10, "description": "Why the preferred model was not used: an outage, a rate limit, no auditable report, or the operator's order. Never a verdict." }
55
- }
56
- },
57
- "finding": {
58
- "type": "object",
59
- "required": ["id", "severity", "evidence", "invariant", "proposed_fix"],
60
- "additionalProperties": false,
61
- "properties": {
62
- "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$", "description": "Stable across re-reviews, so an upheld finding keeps its id." },
63
- "severity": { "enum": ["critical", "high", "medium", "low"] },
64
- "evidence": { "type": "string", "minLength": 5, "description": "Concrete: file:line, a failing command, a counter-example." },
65
- "invariant": { "type": "string", "minLength": 5 },
66
- "affects": { "type": "array", "items": { "type": "string", "minLength": 1 } },
67
- "proposed_fix": { "type": "string", "minLength": 5, "description": "A precise correction for the author to apply; the reviewer never applies it." },
68
- "rebuttal": { "type": "string", "minLength": 20, "description": "The author's written dispute, with evidence. One round only." },
69
- "outcome": { "enum": ["fixed", "withdrawn", "human"] },
70
- "decision": { "type": "string", "minLength": 5, "description": "The human's ruling when outcome is human." }
71
- }
72
- },
73
- "work": {
74
- "type": "object",
75
- "required": ["stage", "sha", "writer"],
76
- "additionalProperties": false,
77
- "properties": {
78
- "stage": { "enum": ["plan", "red", "green", "refactor", "final", "plan_review", "test_review", "code_review", "final_review"] },
79
- "sha": { "$ref": "#/definitions/sha", "description": "The commit the stage's work ended at." },
80
- "writer": { "$ref": "#/definitions/actor" },
81
- "fallback": { "type": "array", "items": { "$ref": "#/definitions/fallback" } }
82
- }
83
- },
84
- "review": {
85
- "type": "object",
86
- "required": ["stage", "sha", "reviewer", "verdict", "checked"],
87
- "additionalProperties": false,
88
- "properties": {
89
- "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." },
90
- "sha": { "$ref": "#/definitions/sha" },
91
- "author": { "$ref": "#/definitions/actor", "$comment": "0.9: the writer of the reviewed work. Records now name writers under work." },
92
- "reviewer": { "$ref": "#/definitions/actor" },
93
- "fallback": { "type": "array", "items": { "$ref": "#/definitions/fallback" } },
94
- "verdict": { "enum": ["approve", "request_changes"] },
95
- "checked": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 }, "description": "Everything the reviewer went through, not only what failed." },
96
- "findings": { "type": "array", "items": { "$ref": "#/definitions/finding" } },
97
- "report": { "type": "string", "minLength": 1, "description": "Path of the retained raw reviewer output, inside the tranche's folder." }
98
- },
99
- "if": { "required": ["verdict"], "properties": { "verdict": { "const": "request_changes" } } },
100
- "then": { "required": ["findings"], "properties": { "findings": { "minItems": 1 } } }
101
- }
102
- }
103
- }
package/src/board-ui.ts DELETED
@@ -1,91 +0,0 @@
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
- }