@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.
- package/README.md +14 -120
- package/conventions/_profiles/architecture.yaml +190 -0
- package/conventions/_profiles/coder.yaml +804 -0
- package/conventions/_profiles/design.yaml +219 -0
- package/conventions/_profiles/docs.yaml +136 -0
- package/conventions/_profiles/htmx.yaml +436 -0
- package/conventions/_profiles/interlocking.yaml +248 -0
- package/conventions/_profiles/metrics.yaml +80 -0
- package/conventions/_profiles/planner.yaml +789 -0
- package/conventions/_profiles/runtime.yaml +62 -0
- package/conventions/_profiles/security.yaml +70 -0
- package/conventions/_profiles/telemetry.yaml +196 -0
- package/conventions/_profiles/tester.yaml +734 -0
- package/conventions/_profiles/topology.yaml +192 -0
- package/conventions/_profiles/traceability.yaml +94 -0
- package/conventions/atdd-bun.profiles/atdd-bun.profiles.presets.convention.yaml +21 -3
- package/conventions/planner.docs/planner.docs.area-index-required.convention.yaml +2 -2
- package/conventions/planner.docs/planner.docs.artifact-path-shape.convention.yaml +1 -1
- package/conventions/planner.docs/planner.docs.asciidoc-only.convention.yaml +0 -1
- package/conventions/planner.docs/planner.docs.identity-required.convention.yaml +1 -1
- package/conventions/planner.docs/planner.docs.undeclared-change.convention.yaml +1 -2
- package/detectors/bun_interlocking_coverage/checks/interlocking_train_sequence.mjs +7 -5
- package/detectors/planner_docs_capability/README.md +1 -1
- package/integrity.json +36 -50
- package/package.json +1 -1
- package/relationships.yaml +0 -151
- package/src/agent.ts +15 -31
- package/src/cli.ts +7 -8
- package/src/docs-capability.ts +6 -24
- package/src/enforce.ts +8 -9
- package/src/hooks.ts +4 -8
- package/src/index.ts +1 -2
- package/src/integrity.ts +3 -11
- package/src/profile-registry.ts +74 -0
- package/src/setup.ts +3 -5
- package/templates/agents/AGENTS.block.md +4 -8
- package/templates/github/atdd-bun.yml +0 -5
- package/conventions/delivery/delivery.approved-sha-resolves.convention.yaml +0 -33
- package/conventions/delivery/delivery.board.convention.yaml +0 -60
- package/conventions/delivery/delivery.config-schema.convention.yaml +0 -49
- package/conventions/delivery/delivery.evidence-schema.convention.yaml +0 -34
- package/conventions/delivery/delivery.findings-resolved.convention.yaml +0 -42
- package/conventions/delivery/delivery.merge-gate.convention.yaml +0 -46
- package/conventions/delivery/delivery.model-allowed.convention.yaml +0 -29
- package/conventions/delivery/delivery.operating-model.convention.yaml +0 -97
- package/conventions/delivery/delivery.review.convention.yaml +0 -42
- package/conventions/delivery/delivery.reviewer-independent.convention.yaml +0 -34
- package/conventions/delivery/delivery.stages-complete.convention.yaml +0 -33
- package/detectors/delivery_evidence/atdd.implementation.yaml +0 -33
- package/detectors/delivery_evidence/detect.mjs +0 -9
- package/detectors/delivery_evidence/fixtures/clean/atdd-bun.yaml +0 -8
- package/detectors/delivery_evidence/fixtures/clean/docs/delivery/tranches/api/evidence.yaml +0 -54
- package/detectors/delivery_evidence/fixtures/dirty/atdd-bun.yaml +0 -3
- package/detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/api/evidence.yaml +0 -31
- package/detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/data/evidence.yaml +0 -8
- package/detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/orphan/README.md +0 -1
- package/detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/ui/evidence.yaml +0 -5
- package/detectors/planner_docs_capability/fixtures/dirty_missing_index/docs/delivery/release.adoc +0 -10
- package/detectors/planner_docs_capability/fixtures/dirty_missing_index/docs/purpose/index.adoc +0 -5
- package/planner-schemas/delivery-config.schema.json +0 -95
- package/planner-schemas/delivery-evidence.schema.json +0 -103
- package/src/board-ui.ts +0 -91
- package/src/board.ts +0 -222
- package/src/delivery.ts +0 -567
- package/templates/agents/atdd/SKILL.md +0 -20
- 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,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 +0,0 @@
|
|
|
1
|
-
The orphan tranche has a folder and no evidence.yaml.
|
package/detectors/planner_docs_capability/fixtures/dirty_missing_index/docs/delivery/release.adoc
DELETED
|
@@ -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,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
|
-
}
|