@afokapu/atdd-bun 0.10.30 → 0.10.32
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 +2 -112
- package/conventions/atdd-bun.profiles/atdd-bun.profiles.presets.convention.yaml +2 -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/_shared/interlocking.mjs +15 -1
- package/detectors/bun_interlocking_coverage/checks/interlocking_train_sequence.mjs +7 -5
- package/detectors/planner_docs_capability/README.md +1 -1
- package/integrity.json +22 -50
- package/package.json +1 -1
- package/relationships.yaml +0 -151
- package/src/agent.ts +2 -18
- package/src/cli.ts +0 -8
- package/src/docs-capability.ts +6 -24
- package/src/enforce.ts +1 -2
- package/src/hooks.ts +1 -5
- package/src/index.ts +0 -2
- package/src/integrity.ts +3 -9
- package/src/setup.ts +1 -3
- package/templates/agents/AGENTS.block.md +0 -2
- 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/delivery/SKILL.md +0 -15
package/relationships.yaml
CHANGED
|
@@ -95,17 +95,6 @@ nodes:
|
|
|
95
95
|
- coder.htmx.verb-endpoint-is-routed
|
|
96
96
|
- coder.htmx.verb-endpoint-same-origin
|
|
97
97
|
- coder.htmx.verb-mutation-signals-progress
|
|
98
|
-
- delivery.approved-sha-resolves
|
|
99
|
-
- delivery.board
|
|
100
|
-
- delivery.config-schema
|
|
101
|
-
- delivery.evidence-schema
|
|
102
|
-
- delivery.findings-resolved
|
|
103
|
-
- delivery.merge-gate
|
|
104
|
-
- delivery.model-allowed
|
|
105
|
-
- delivery.operating-model
|
|
106
|
-
- delivery.review
|
|
107
|
-
- delivery.reviewer-independent
|
|
108
|
-
- delivery.stages-complete
|
|
109
98
|
- planner.acceptance.abstract-fields-required
|
|
110
99
|
- planner.acceptance.authoring-guidelines
|
|
111
100
|
- planner.acceptance.boundary-kind-vocabulary
|
|
@@ -5208,143 +5197,3 @@ edges:
|
|
|
5208
5197
|
target_ref: planner.telemetry.tracking-plan-schema
|
|
5209
5198
|
type: requires
|
|
5210
5199
|
origin: "@afokapu/atdd-bun"
|
|
5211
|
-
- confidence: 1
|
|
5212
|
-
constraint: mandatory
|
|
5213
|
-
control: internal
|
|
5214
|
-
foundation: finish_to_start
|
|
5215
|
-
reason: evidence is judged against the stages the policy configures
|
|
5216
|
-
source_ref: delivery.evidence-schema
|
|
5217
|
-
strength: important
|
|
5218
|
-
target_ref: delivery.config-schema
|
|
5219
|
-
type: requires
|
|
5220
|
-
origin: "@afokapu/atdd-bun"
|
|
5221
|
-
- confidence: 1
|
|
5222
|
-
constraint: mandatory
|
|
5223
|
-
control: internal
|
|
5224
|
-
foundation: finish_to_start
|
|
5225
|
-
reason: allowed models and their preference order come from the policy
|
|
5226
|
-
source_ref: delivery.model-allowed
|
|
5227
|
-
strength: important
|
|
5228
|
-
target_ref: delivery.config-schema
|
|
5229
|
-
type: requires
|
|
5230
|
-
origin: "@afokapu/atdd-bun"
|
|
5231
|
-
- confidence: 1
|
|
5232
|
-
constraint: mandatory
|
|
5233
|
-
control: internal
|
|
5234
|
-
foundation: finish_to_start
|
|
5235
|
-
reason: models are read from a well-formed review entry
|
|
5236
|
-
source_ref: delivery.model-allowed
|
|
5237
|
-
strength: important
|
|
5238
|
-
target_ref: delivery.evidence-schema
|
|
5239
|
-
type: requires
|
|
5240
|
-
origin: "@afokapu/atdd-bun"
|
|
5241
|
-
- confidence: 1
|
|
5242
|
-
constraint: mandatory
|
|
5243
|
-
control: internal
|
|
5244
|
-
foundation: finish_to_start
|
|
5245
|
-
reason: independence is judged over the author and reviewer runs the record names
|
|
5246
|
-
source_ref: delivery.reviewer-independent
|
|
5247
|
-
strength: important
|
|
5248
|
-
target_ref: delivery.evidence-schema
|
|
5249
|
-
type: requires
|
|
5250
|
-
origin: "@afokapu/atdd-bun"
|
|
5251
|
-
- confidence: 1
|
|
5252
|
-
constraint: mandatory
|
|
5253
|
-
control: internal
|
|
5254
|
-
foundation: finish_to_start
|
|
5255
|
-
reason: outcomes are read from well-formed findings
|
|
5256
|
-
source_ref: delivery.findings-resolved
|
|
5257
|
-
strength: important
|
|
5258
|
-
target_ref: delivery.evidence-schema
|
|
5259
|
-
type: requires
|
|
5260
|
-
origin: "@afokapu/atdd-bun"
|
|
5261
|
-
- confidence: 1
|
|
5262
|
-
constraint: mandatory
|
|
5263
|
-
control: internal
|
|
5264
|
-
foundation: finish_to_start
|
|
5265
|
-
reason: a stage is approved only once its earlier findings are settled
|
|
5266
|
-
source_ref: delivery.stages-complete
|
|
5267
|
-
strength: important
|
|
5268
|
-
target_ref: delivery.findings-resolved
|
|
5269
|
-
type: requires
|
|
5270
|
-
origin: "@afokapu/atdd-bun"
|
|
5271
|
-
- confidence: 1
|
|
5272
|
-
constraint: mandatory
|
|
5273
|
-
control: internal
|
|
5274
|
-
foundation: start_to_start
|
|
5275
|
-
reason: a reviewer works read-only in a fresh process, as the independence rule judges from the record
|
|
5276
|
-
source_ref: delivery.review
|
|
5277
|
-
strength: important
|
|
5278
|
-
target_ref: delivery.reviewer-independent
|
|
5279
|
-
type: requires
|
|
5280
|
-
origin: "@afokapu/atdd-bun"
|
|
5281
|
-
- confidence: 1
|
|
5282
|
-
constraint: mandatory
|
|
5283
|
-
control: internal
|
|
5284
|
-
foundation: start_to_start
|
|
5285
|
-
reason: a reviewer settles a rebuttal in one round, as the findings rule judges from the record
|
|
5286
|
-
source_ref: delivery.review
|
|
5287
|
-
strength: important
|
|
5288
|
-
target_ref: delivery.findings-resolved
|
|
5289
|
-
type: requires
|
|
5290
|
-
origin: "@afokapu/atdd-bun"
|
|
5291
|
-
- confidence: 1
|
|
5292
|
-
constraint: mandatory
|
|
5293
|
-
control: internal
|
|
5294
|
-
foundation: start_to_start
|
|
5295
|
-
reason: the driver runs every review as the review convention describes
|
|
5296
|
-
source_ref: delivery.operating-model
|
|
5297
|
-
strength: important
|
|
5298
|
-
target_ref: delivery.review
|
|
5299
|
-
type: requires
|
|
5300
|
-
origin: "@afokapu/atdd-bun"
|
|
5301
|
-
- confidence: 1
|
|
5302
|
-
constraint: mandatory
|
|
5303
|
-
control: internal
|
|
5304
|
-
foundation: finish_to_start
|
|
5305
|
-
reason: the operating model leaves the record the evidence schema judges
|
|
5306
|
-
source_ref: delivery.operating-model
|
|
5307
|
-
strength: important
|
|
5308
|
-
target_ref: delivery.evidence-schema
|
|
5309
|
-
type: requires
|
|
5310
|
-
origin: "@afokapu/atdd-bun"
|
|
5311
|
-
- confidence: 1
|
|
5312
|
-
constraint: mandatory
|
|
5313
|
-
control: internal
|
|
5314
|
-
foundation: start_to_start
|
|
5315
|
-
reason: the coordinator and drivers dispatch work and receive results through the board
|
|
5316
|
-
source_ref: delivery.operating-model
|
|
5317
|
-
strength: important
|
|
5318
|
-
target_ref: delivery.board
|
|
5319
|
-
type: requires
|
|
5320
|
-
origin: "@afokapu/atdd-bun"
|
|
5321
|
-
- confidence: 1
|
|
5322
|
-
constraint: mandatory
|
|
5323
|
-
control: internal
|
|
5324
|
-
foundation: finish_to_start
|
|
5325
|
-
reason: the approved SHA is the one the closing review approved
|
|
5326
|
-
source_ref: delivery.approved-sha-resolves
|
|
5327
|
-
strength: important
|
|
5328
|
-
target_ref: delivery.stages-complete
|
|
5329
|
-
type: requires
|
|
5330
|
-
origin: "@afokapu/atdd-bun"
|
|
5331
|
-
- confidence: 1
|
|
5332
|
-
constraint: mandatory
|
|
5333
|
-
control: internal
|
|
5334
|
-
foundation: finish_to_start
|
|
5335
|
-
reason: the gate compares the branch head with an approved SHA that exists
|
|
5336
|
-
source_ref: delivery.merge-gate
|
|
5337
|
-
strength: important
|
|
5338
|
-
target_ref: delivery.approved-sha-resolves
|
|
5339
|
-
type: requires
|
|
5340
|
-
origin: "@afokapu/atdd-bun"
|
|
5341
|
-
- confidence: 1
|
|
5342
|
-
constraint: mandatory
|
|
5343
|
-
control: internal
|
|
5344
|
-
foundation: start_to_start
|
|
5345
|
-
reason: the gate merges a tranche whose lifecycle closed its acceptances
|
|
5346
|
-
source_ref: delivery.merge-gate
|
|
5347
|
-
strength: important
|
|
5348
|
-
target_ref: traceability.plan.executable-acceptance-has-test
|
|
5349
|
-
type: requires
|
|
5350
|
-
origin: "@afokapu/atdd-bun"
|
package/src/agent.ts
CHANGED
|
@@ -1,22 +1,12 @@
|
|
|
1
|
-
import { mkdir, readFile,
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import { deliveryAdopted } from "./delivery";
|
|
5
4
|
|
|
6
5
|
const root = resolve(import.meta.dir, "..");
|
|
7
6
|
const version = (await Bun.file(join(root, "package.json")).json() as { version: string }).version;
|
|
8
7
|
const render = async (path: string) => (await readFile(join(root, "templates/agents", path), "utf8")).replace("{{VERSION}}", version);
|
|
9
8
|
// .agents/skills is the vendor-neutral Agent Skills path (Codex, Copilot, Cursor, Gemini CLI, …); .claude/skills is Claude Code's.
|
|
10
9
|
const skillPaths = [".agents/skills/atdd/SKILL.md", ".claude/skills/atdd/SKILL.md"];
|
|
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"];
|
|
15
|
-
/** Whether the repository's atdd-bun.yaml adopts the delivery profile. */
|
|
16
|
-
export async function deliveryInstalled(repo: string): Promise<boolean> {
|
|
17
|
-
const file = join(repo, "atdd-bun.yaml");
|
|
18
|
-
try { return existsSync(file) && deliveryAdopted(Bun.YAML.parse(await readFile(file, "utf8"))); } catch { return false; }
|
|
19
|
-
}
|
|
20
10
|
// AGENTS.md is read by Codex, Cursor and most agents; CLAUDE.md by Claude Code. Both carry the same block.
|
|
21
11
|
export const instructionPaths = ["AGENTS.md", "CLAUDE.md"];
|
|
22
12
|
const block = /<!-- atdd-bun:start[\s\S]*?<!-- atdd-bun:end -->\n?/;
|
|
@@ -32,12 +22,6 @@ export async function agentInit(repo = process.cwd(), replace = false) {
|
|
|
32
22
|
if (existsSync(output) && !replace) { kept.push(output); continue; }
|
|
33
23
|
await mkdir(dirname(output), { recursive: true }); await writeFile(output, await render("atdd/SKILL.md")); written.push(output);
|
|
34
24
|
}
|
|
35
|
-
if (await deliveryInstalled(repo)) for (const [path, template] of deliverySkillFiles) {
|
|
36
|
-
const output = join(repo, path);
|
|
37
|
-
if (existsSync(output) && !replace) { kept.push(output); continue; }
|
|
38
|
-
await mkdir(dirname(output), { recursive: true }); await writeFile(output, await render(template)); written.push(output);
|
|
39
|
-
}
|
|
40
|
-
for (const path of retiredFiles) if (existsSync(join(repo, path))) { await rm(join(repo, path)); written.push(`${join(repo, path)} (removed)`); }
|
|
41
25
|
const managed = await render("AGENTS.block.md");
|
|
42
26
|
for (const path of instructionPaths) {
|
|
43
27
|
const file = join(repo, path), current = existsSync(file) ? await readFile(file, "utf8") : "";
|
|
@@ -48,7 +32,7 @@ export async function agentInit(repo = process.cwd(), replace = false) {
|
|
|
48
32
|
return { ok: true, message: written.join("\n") };
|
|
49
33
|
}
|
|
50
34
|
export async function agentStatus(repo = process.cwd()) {
|
|
51
|
-
const expected =
|
|
35
|
+
const expected = skillPaths;
|
|
52
36
|
const missing = expected.map(path => join(repo, path)).filter(path => !existsSync(path));
|
|
53
37
|
for (const path of instructionPaths) { const agents = join(repo, path); if (!existsSync(agents) || !block.test(await readFile(agents, "utf8"))) missing.push(`${agents} (atdd-bun block)`); }
|
|
54
38
|
return { ok: !missing.length, message: missing.length ? `missing: ${missing.join(", ")}` : [...expected, ...instructionPaths].map(path => join(repo, path)).join("\n") };
|
package/src/cli.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { journeyDocs } from "./journey-docs";
|
|
|
8
8
|
import { adrRegister } from "./docs-capability";
|
|
9
9
|
import { releaseCheck } from "./release";
|
|
10
10
|
import { initializeRepository } from "./setup";
|
|
11
|
-
import { chat } from "./board";
|
|
12
11
|
|
|
13
12
|
const args = process.argv.slice(2);
|
|
14
13
|
const usage = {
|
|
@@ -24,12 +23,6 @@ const usage = {
|
|
|
24
23
|
"atdd-bun docs journeys [--out <dir>] [--check] [--force]",
|
|
25
24
|
"atdd-bun docs adr-register [--check]",
|
|
26
25
|
"atdd-bun release check",
|
|
27
|
-
"atdd-bun chat topic <program> [<tranche> [<stage> <round>]]",
|
|
28
|
-
"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)",
|
|
29
|
-
"atdd-bun chat read <topic> [--mine] [--since ID]",
|
|
30
|
-
"atdd-bun chat wait <topic[,topic...]> [--since ID] [--skip KIND,...] [--timeout SECONDS]",
|
|
31
|
-
"atdd-bun chat follow <topic[,topic...]> [--since ID] [--skip KIND,...] [--timeout SECONDS]",
|
|
32
|
-
"atdd-bun chat (the board: every topic on the left, the conversation on the right)",
|
|
33
26
|
],
|
|
34
27
|
profiles: profileNames,
|
|
35
28
|
note: "Use a profile directly, for example: atdd-bun planner. --profile planner remains supported for compatibility.",
|
|
@@ -101,7 +94,6 @@ if (args[0] === "agent") {
|
|
|
101
94
|
const result = args[1] === "init" ? await agentInit(process.cwd(), args.includes("--replace")) : args[1] === "status" ? await agentStatus() : fail("agent requires init or status");
|
|
102
95
|
console[result.ok ? "log" : "error"](result.message); process.exit(result.ok ? 0 : 1);
|
|
103
96
|
}
|
|
104
|
-
if (args[0] === "chat") process.exit(await chat(args.slice(1)));
|
|
105
97
|
if (args[0] === "release") {
|
|
106
98
|
if (args[1] !== "check") fail("release requires check");
|
|
107
99
|
const result = await releaseCheck(); console[result.ok ? "log" : "error"](result.message); process.exit(result.ok ? 0 : 1);
|
package/src/docs-capability.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { journeyDocs, journeyDocsApply } from "./journey-docs";
|
|
3
|
-
import { DEFAULT_ROOT, deliveryAdopted, deliveryPolicy } from "./delivery";
|
|
4
3
|
import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
|
|
5
4
|
import { tmpdir } from "node:os";
|
|
6
5
|
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
@@ -18,7 +17,7 @@ export type DocumentationRender = { findings: DocumentationViolation[]; couldNot
|
|
|
18
17
|
|
|
19
18
|
const DOCS = "docs";
|
|
20
19
|
const DIST = "docs/dist";
|
|
21
|
-
const AREAS = ["docs", "docs/purpose", "docs/architecture", "docs/architecture/decisions", "docs/
|
|
20
|
+
const AREAS = ["docs", "docs/purpose", "docs/architecture", "docs/architecture/decisions", "docs/archive"];
|
|
22
21
|
const VERBS = ["decides", "supersedes", "implements", "depends-on"];
|
|
23
22
|
const ADR_DIR = "docs/architecture/decisions";
|
|
24
23
|
const ADR_INDEX = `${ADR_DIR}/index.adoc`;
|
|
@@ -51,23 +50,6 @@ export function parseAttributes(text: string): { attrs: Record<string, string>;
|
|
|
51
50
|
return { attrs, lines };
|
|
52
51
|
}
|
|
53
52
|
|
|
54
|
-
/** The delivery profile's records folder, when it is adopted: tranche records and reports are YAML and data, judged by
|
|
55
|
-
* that profile, never authored documentation. The docs profile leaves the folder (by default docs/delivery/tranches) to
|
|
56
|
-
* it; the rest of docs/delivery, the program's reasoning, stays documentation. */
|
|
57
|
-
async function deliveryRecords(root: string): Promise<(path: string) => boolean> {
|
|
58
|
-
const file = join(root, "atdd-bun.yaml");
|
|
59
|
-
try {
|
|
60
|
-
const data = existsSync(file) ? Bun.YAML.parse(await readFile(file, "utf8")) as Record<string, unknown> | null : null;
|
|
61
|
-
if (!deliveryAdopted(data)) return () => false;
|
|
62
|
-
// Only the one folder the delivery profile may use under docs/; a root configured anywhere else in docs/ is a delivery
|
|
63
|
-
// finding and exempts nothing here.
|
|
64
|
-
if (deliveryPolicy(data!.delivery).root !== DEFAULT_ROOT) return () => false;
|
|
65
|
-
// Records and data only: AsciiDoc there stays documentation, judged by every docs rule (the delivery profile also
|
|
66
|
-
// reports it as a file that does not belong in the records folder).
|
|
67
|
-
return path => path.startsWith(`${DEFAULT_ROOT}/`) && !path.endsWith(".adoc");
|
|
68
|
-
} catch { return () => false; }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
53
|
async function documents(root: string): Promise<Document[]> {
|
|
72
54
|
const paths = await walk(join(root, DOCS), ".adoc");
|
|
73
55
|
return Promise.all(paths.filter(path => !generated(relative(root, path).replaceAll("\\", "/"))).map(async path => {
|
|
@@ -105,8 +87,8 @@ function adrViolations(docs: Document[]): DocumentationViolation[] {
|
|
|
105
87
|
}
|
|
106
88
|
|
|
107
89
|
export async function scanDocumentation(root: string): Promise<DocumentationViolation[]> {
|
|
108
|
-
const absolute = resolve(root), docs = await documents(absolute), output: DocumentationViolation[] = []
|
|
109
|
-
for (const path of await walk(join(absolute, DOCS), ".md")) { const file = relative(absolute, path).replaceAll("\\", "/"); if (!generated(file)
|
|
90
|
+
const absolute = resolve(root), docs = await documents(absolute), output: DocumentationViolation[] = [];
|
|
91
|
+
for (const path of await walk(join(absolute, DOCS), ".md")) { const file = relative(absolute, path).replaceAll("\\", "/"); if (!generated(file)) output.push(violation("planner.docs.asciidoc-only", file, 1, `authored markdown beneath docs/: ${file}. AsciiDoc is the only authored format; convert it, and convert a historical document INTO docs/archive/ rather than into a current area.`, lineAt(await readFile(path, "utf8"), 1))); }
|
|
110
92
|
for (const doc of docs) { const missing = ["doc-id", "status"].filter(name => !doc.attrs[name]); if (missing.length) output.push(violation("planner.docs.identity-required", doc.path, 1, `document declares no ${missing.map(name => `:${name}:`).join(" and no ")}. Identity and currency are both required: an id with no status is a node whose currency is unknown, a status with no id is a claim nothing can reference.`, lineAt(doc.text, 1))); }
|
|
111
93
|
const byId = new Map<string, Document[]>(); for (const doc of docs) if (doc.attrs["doc-id"]) byId.set(doc.attrs["doc-id"], [...(byId.get(doc.attrs["doc-id"]) ?? []), doc]);
|
|
112
94
|
for (const [id, group] of byId) if (group.length > 1) for (const doc of group) { const line = doc.lines["doc-id"] ?? 1; output.push(violation("planner.docs.doc-id-unique", doc.path, line, `doc-id ${JSON.stringify(id)} is declared by ${group.length} documents: ${group.map(d => d.path).join(", ")}. Resolution needs exactly one target per id.`, lineAt(doc.text, line))); }
|
|
@@ -124,12 +106,12 @@ async function journeyViewViolations(root: string): Promise<DocumentationViolati
|
|
|
124
106
|
return result.stale.map(file => violation("planner.docs.journey-view-current", file, 1, `${file} does not match what plan/ generates, so the journey documentation no longer shows the plan. Regenerate it with \`atdd-bun docs journeys\` and commit the result; never edit it by hand.`));
|
|
125
107
|
}
|
|
126
108
|
|
|
127
|
-
export function declarationViolations(declaration: DocumentationDeclaration | null, changeSet?: string[]
|
|
109
|
+
export function declarationViolations(declaration: DocumentationDeclaration | null, changeSet?: string[]): DocumentationViolation[] {
|
|
128
110
|
if (!declaration) return [];
|
|
129
111
|
const artifacts = Array.isArray(declaration.artifacts) ? declaration.artifacts : []; const output: DocumentationViolation[] = [];
|
|
130
112
|
if (declaration.impact !== "change" && declaration.impact !== "none") output.push(violation("planner.docs.artifact-path-shape", "<declaration>", 1, `declaration carries impact=${JSON.stringify(declaration.impact)}; the two total forms are ["change", "none"]. A malformed declaration is reported, never treated as nothing-to-check.`));
|
|
131
113
|
for (const [index, artifact] of artifacts.entries()) { const path = artifact.path ?? "", problems: string[] = []; if (!path) problems.push("declares no path"); else { if (!path.startsWith("docs/")) problems.push(`path ${JSON.stringify(path)} is outside the canonical tree (must begin "docs/")`); if (!path.endsWith(".adoc")) problems.push(`path ${JSON.stringify(path)} is not AsciiDoc (must end ".adoc")`); if (artifact.action === "archive" && !path.startsWith("docs/archive/")) problems.push(`archive destination ${JSON.stringify(path)} is outside "docs/archive/" — archiving must preserve history, never promote it into a current area`); } if (problems.length) output.push(violation("planner.docs.artifact-path-shape", path || "<declaration>", 1, `declared artifact[${index}] (action: ${artifact.action || "unset"}): ${problems.join("; ")}`)); }
|
|
132
|
-
if (changeSet) { const covered = new Set(artifacts.flatMap(a => [a.path, a.from]).filter((p): p is string => Boolean(p))); for (const path of [...new Set(changeSet)].sort()) if (path.startsWith("docs/") && !generated(path) && !
|
|
114
|
+
if (changeSet) { const covered = new Set(artifacts.flatMap(a => [a.path, a.from]).filter((p): p is string => Boolean(p))); for (const path of [...new Set(changeSet)].sort()) if (path.startsWith("docs/") && !generated(path) && !covered.has(path)) output.push(violation("planner.docs.undeclared-change", path, 1, `the change set touches ${path} and no declared artifact covers it. Declare it at RATIFY; if the change was not planned, the declaration was wrong at RATIFY and re-ratifying is the honest correction.`)); }
|
|
133
115
|
return output;
|
|
134
116
|
}
|
|
135
117
|
|
|
@@ -170,7 +152,7 @@ export async function renderDocumentation(root: string, timeoutMs = 120_000): Pr
|
|
|
170
152
|
async function checkDocumentationInner(input: { root: string; declaration: DocumentationDeclaration | null; changeSet: string[] | null; render?: () => Promise<DocumentationRender> }): Promise<DocumentationCheck> {
|
|
171
153
|
if (input.declaration?.impact === "none") return { verdict: "NOT_APPLICABLE", findings: [], checked: [] };
|
|
172
154
|
const corpus = await scanDocumentation(input.root); const findings: DocumentationCheck["findings"] = [...corpus]; const checked = (await documents(resolve(input.root))).map(d => d.path); let definite = corpus.length > 0;
|
|
173
|
-
if (input.declaration) { const declarationFindings = declarationViolations(input.declaration, input.changeSet ?? undefined
|
|
155
|
+
if (input.declaration) { const declarationFindings = declarationViolations(input.declaration, input.changeSet ?? undefined); findings.push(...declarationFindings); definite ||= declarationFindings.length > 0; checked.push("<declaration>"); if (input.declaration.impact === "change") for (const artifact of input.declaration.artifacts ?? []) if (["create", "modify"].includes(artifact.action ?? "") && artifact.path && !existsSync(join(input.root, artifact.path))) { findings.push(violation("planner.docs.artifact-path-shape", artifact.path, 1, `declared artifact ${JSON.stringify(artifact.path)} (action: ${artifact.action}) is not in the tree, so it was never examined; a declared document that was never written has not discharged the obligation.`)); definite = true; } } else findings.push({ rule_id: null, where: "<declaration>", message: "core supplied no documentation declaration, so no declaration-dependent rule could be evaluated. This is COULD_NOT_CHECK and it BLOCKS." });
|
|
174
156
|
if (input.declaration && input.changeSet === null) findings.push({ rule_id: null, where: "<change_set>", message: "core supplied no change set, so whether this diff touches docs/ without declaring it could not be established. This is COULD_NOT_CHECK and it BLOCKS." });
|
|
175
157
|
const rendered = await (input.render ?? (() => renderDocumentation(input.root)))(); findings.push(...rendered.findings); definite ||= rendered.findings.length > 0; if (rendered.couldNotCheck) findings.push({ rule_id: "planner.docs.reference-integrity", file: "docs/", line: 1, col: 1, evidence: rendered.couldNotCheck, source_line: "" }); else checked.push("<render:asciidoctor>");
|
|
176
158
|
// Fail closed on any rule id this capability does not declare, however it was built (a renderer can return anything).
|
package/src/enforce.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { topologyFor } from "./topology";
|
|
6
6
|
|
|
7
|
-
export type Profile = "traceability" | "topology" | "docs" | "planner" | "telemetry" | "
|
|
7
|
+
export type Profile = "traceability" | "topology" | "docs" | "planner" | "telemetry" | "coder" | "tester" | "security" | "architecture" | "metrics" | "runtime" | "interlocking" | "htmx" | "design" | "all";
|
|
8
8
|
|
|
9
9
|
export type Violation = {
|
|
10
10
|
rule_id: string;
|
|
@@ -28,7 +28,6 @@ const profiles: Record<Exclude<Profile, "all">, string[]> = {
|
|
|
28
28
|
docs: ["planner_docs_capability"],
|
|
29
29
|
planner: ["planner_plan_integrity", "planner_schema_validation", "planner_static_validators", "atdd_topology"],
|
|
30
30
|
telemetry: ["planner_telemetry_plan", "bun_telemetry_code", "bun_telemetry_test"],
|
|
31
|
-
delivery: ["delivery_evidence"],
|
|
32
31
|
coder: ["bun_green_traceability_detector", "bun_clean_architecture_detector", "bun_ts_metrics_detector", "bun_fullstack_detector", "bun_design_system_detector", "bun_responsive_detector", "atdd_topology"],
|
|
33
32
|
tester: ["bun_tester_discipline_detector", "htmx_e2e_detector", "atdd_topology"],
|
|
34
33
|
security: ["bun_security_hygiene_detector"],
|
package/src/hooks.ts
CHANGED
|
@@ -36,17 +36,13 @@ export type WorktreeLayout = { primary: string; worktreeRoot: string; current: s
|
|
|
36
36
|
async function layout(root: string, cfg: WorktreePolicy): Promise<WorktreeLayout | { error: string }> { const current = await realpath(resolve(root)), commonRoot = await primaryRoot(current); if (!commonRoot) return { error: "not inside a Git worktree" }; const primary = await realpath(commonRoot), expectedPrimary = join(dirname(primary), cfg.primary_directory); if (primary !== expectedPrimary) return { error: `primary checkout must be ${expectedPrimary}, not ${primary}` }; const branch = (await git(primary, ["symbolic-ref", "--quiet", "--short", "HEAD"])).out; if (branch !== cfg.primary_branch) return { error: `primary checkout ${primary} must be on ${cfg.primary_branch}, not ${branch || "a detached HEAD"}` }; return { primary, worktreeRoot: resolve(primary, cfg.root), current, branch: (await git(current, ["symbolic-ref", "--quiet", "--short", "HEAD"])).out }; }
|
|
37
37
|
async function worktreeCommitPolicy(root: string, cfg: WorktreePolicy): Promise<string | null> { if (!cfg.enabled || !cfg.require_linked_worktree) return null; const state = await layout(root, cfg); if ("error" in state) return state.error; if (state.current === state.primary) return `commits must use a linked worktree beneath ${state.worktreeRoot}; run: atdd-bun worktree start <branch>`; if (!inside(state.worktreeRoot, state.current)) return `linked worktree ${state.current} is outside required root ${state.worktreeRoot}`; if (!state.branch) return "commits require a named branch, not a detached HEAD"; return null; }
|
|
38
38
|
|
|
39
|
-
/** Where the pushed branch left its base branch. A delivery record unchanged since then was judged when it merged, so
|
|
40
|
-
* pre-push does not judge it again, exactly as the merge gate does not; with no base found, every record is judged. */
|
|
41
|
-
async function pushBase(root: string, remote: string, cfg: HookPolicy): Promise<string | null> { for (const ref of [`${remote}/HEAD`, ...cfg.protected_branches.map(name => `${remote}/${name}`)]) { const found = await git(root, ["merge-base", "HEAD", ref]); if (!found.code && found.out) return found.out; } return null; }
|
|
42
|
-
|
|
43
39
|
export async function runHook(event: HookEvent, root = process.cwd(), args: string[] = [], stdin = "") {
|
|
44
40
|
const cfg = await policy(root), branch = (await git(root, ["symbolic-ref", "--quiet", "--short", "HEAD"])).out;
|
|
45
41
|
if (["pre-commit", "pre-merge-commit"].includes(event) && cfg.protected_branches.includes(branch)) return bad(`protected branch ${branch} is blocked`);
|
|
46
42
|
if (["pre-commit", "pre-merge-commit"].includes(event)) { const violation = await worktreeCommitPolicy(root, cfg.worktrees); if (violation) return bad(violation); }
|
|
47
43
|
if (event === "pre-commit") { const staged = await files(root, ["diff", "--cached", "--name-only"]), registries = staged.filter(path => isRegistry(cfg, path)), generated = staged.filter(isGeneratedView), counted = staged.length - registries.length - generated.length, lines = (await stagedLineStats(root)).filter(row => !isRegistry(cfg, row.path) && !isGeneratedView(row.path)).reduce((n, row) => n + row.added + row.removed, 0); if (counted > cfg.max_staged_files) return bad(`staged files ${counted} exceed ${cfg.max_staged_files}`); if (lines > cfg.max_staged_changed_lines) return bad(`staged changed lines ${lines} exceed ${cfg.max_staged_changed_lines}`); return cfg.require_traceability || registries.length || generated.length ? validation(root, staged, false, registries.length > 0) : { ok: true, message: "ok" }; }
|
|
48
44
|
if (event === "commit-msg") { const deleted = (await files(root, ["diff", "--cached", "--name-only", "--diff-filter=D"])).length, stats = await stagedStats(root), lines = stats.reduce((n, row) => n + row.removed, 0), registryRemoved = stats.filter(row => isRegistry(cfg, row.path)).reduce((n, row) => n + row.removed - row.added, 0), message = args[0] && existsSync(args[0]) ? await readFile(args[0], "utf8") : "", approved = message.includes("[mass-delete-approved]"); if ((deleted > 50 || lines > 10_000) && !approved) return bad("mass delete requires [mass-delete-approved]"); return registryRemoved > cfg.max_registry_removed_lines && !approved ? bad(`registry removal of ${registryRemoved} net lines exceeds ${cfg.max_registry_removed_lines}; requires [mass-delete-approved]`) : { ok: true, message: "ok" }; }
|
|
49
|
-
if (event === "pre-push") { for (const row of stdin.split("\n").filter(Boolean).map(row => row.split(/\s+/))) { const [,,remote, remoteSha] = row, target = remote?.replace("refs/heads/", ""); if (target && cfg.protected_branches.includes(target)) return bad(`protected branch ${target} is blocked`); const local = row[1]; if (local && !/^0+$/.test(local)) { const range = !remoteSha || /^0+$/.test(remoteSha) ? `${local}^..${local}` : `${remoteSha}..${local}`, count = Number((await git(root, ["rev-list", "--count", range])).out); if (count > cfg.max_commits_per_push) return bad(`commits per push ${count} exceed ${cfg.max_commits_per_push}`); } }
|
|
45
|
+
if (event === "pre-push") { for (const row of stdin.split("\n").filter(Boolean).map(row => row.split(/\s+/))) { const [,,remote, remoteSha] = row, target = remote?.replace("refs/heads/", ""); if (target && cfg.protected_branches.includes(target)) return bad(`protected branch ${target} is blocked`); const local = row[1]; if (local && !/^0+$/.test(local)) { const range = !remoteSha || /^0+$/.test(remoteSha) ? `${local}^..${local}` : `${remoteSha}..${local}`, count = Number((await git(root, ["rev-list", "--count", range])).out); if (count > cfg.max_commits_per_push) return bad(`commits per push ${count} exceed ${cfg.max_commits_per_push}`); } } return validation(root, await files(root, ["diff", "--name-only", "HEAD~1..HEAD"]), true); }
|
|
50
46
|
if (event === "post-commit") { const result = await validation(root, await files(root, ["show", "--pretty=format:", "--name-only", "HEAD"]), false); return { ok: true, message: result.ok ? result.message : `advisory: ${result.message}` }; }
|
|
51
47
|
return { ok: true, message: "ok" };
|
|
52
48
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,8 +7,6 @@ export type { PlanArtifact, PlanFinding, PlanGraph, PlanKind } from "./planner-k
|
|
|
7
7
|
export { validateStaticPlannerConventions } from "./planner-validators";
|
|
8
8
|
export { validateTelemetryPlan, loadTelemetryFiles, telemetryDecisionOf, CONCRETE_URN } from "./telemetry-plan";
|
|
9
9
|
export type { TelemetryPlanItem, TelemetryFile } from "./telemetry-plan";
|
|
10
|
-
export { canonicalRoot, gateMode, deliveryAdopted, deliveryPolicy, loadEvidence, loosenedDelivery, STAGES, validateDelivery } from "./delivery";
|
|
11
|
-
export type { DeliveryOptions, GateMode, DeliveryPolicy, EvidenceFile, Independence, Stage, StagePolicy } from "./delivery";
|
|
12
10
|
export { PLANNER_SCHEMA_RULE_ID, validatePlannerSchemas } from "./planner-schema-validator";
|
|
13
11
|
export { defaultHookPolicy, hookEvents, hooksStatus, installHooks, runHook, uninstallHooks } from "./hooks";
|
|
14
12
|
export type { HookEvent, HookPolicy } from "./hooks";
|
package/src/integrity.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { instructionPaths } from "./agent";
|
|
5
5
|
import { renderWorkflow } from "./ci";
|
|
6
|
-
import { loosenedDelivery } from "./delivery";
|
|
7
6
|
import { defaultHookPolicy, type HookPolicy } from "./hooks";
|
|
8
7
|
|
|
9
8
|
/**
|
|
@@ -101,9 +100,6 @@ async function checkGenerated(root: string, packageRoot: string, skipWorkflow =
|
|
|
101
100
|
else if (!existsSync(workflow)) findings.push({ file: WORKFLOW, detail: "is missing", restore: "bun run atdd-bun ci init --replace" });
|
|
102
101
|
else if (unstamp(await readFile(workflow, "utf8")) !== unstamp(await renderWorkflow(root))) findings.push({ file: WORKFLOW, detail: "was edited; it must match what the package generates (protected_branches decides its push branches)", restore: "bun run atdd-bun ci init --replace" });
|
|
103
102
|
for (const skill of SKILLS) await same(skill, "templates/agents/atdd/SKILL.md", "bun run atdd-bun agent init --replace");
|
|
104
|
-
// Required while delivery is adopted; protected whenever present, so turning delivery off and on cannot launder an edit.
|
|
105
|
-
const adopted = await deliveryInstalled(root);
|
|
106
|
-
for (const [path, template] of deliverySkillFiles) if (adopted || existsSync(join(root, path))) await same(path, `templates/agents/${template}`, "bun run atdd-bun agent init --replace");
|
|
107
103
|
await same(relative(root, await testFilePath(root)), "templates/agents/atdd-bun.integrity.test.ts", "bun run atdd-bun integrity init --replace");
|
|
108
104
|
const canonical = (await readFile(join(packageRoot, "templates/agents/AGENTS.block.md"), "utf8")).match(BLOCK)![0];
|
|
109
105
|
for (const file of instructionPaths) {
|
|
@@ -119,8 +115,7 @@ async function checkGenerated(root: string, packageRoot: string, skipWorkflow =
|
|
|
119
115
|
const explicitProfiles = (config: { profiles?: unknown }): string[] | null => Array.isArray(config.profiles) ? config.profiles.map(String) : null;
|
|
120
116
|
|
|
121
117
|
/** A policy with null-valued hook keys (and worktrees children) removed: YAML gives null for a key with no value, and the
|
|
122
|
-
* hooks read null as absent, so the comparison must too. Other keys keep their null
|
|
123
|
-
* readers that tell null apart from absent (`delivery:` with no value adopts delivery). */
|
|
118
|
+
* hooks read null as absent, so the comparison must too. Other keys, including profiles, keep their null. */
|
|
124
119
|
function withoutNulls(config: Record<string, unknown>): Record<string, unknown> {
|
|
125
120
|
const out = Object.fromEntries(Object.entries(config).filter(([key, value]) => value !== null || !(key in defaultHookPolicy)));
|
|
126
121
|
const worktrees = out.worktrees;
|
|
@@ -149,7 +144,7 @@ export function policyShapeErrors(config: Record<string, unknown>): string[] {
|
|
|
149
144
|
}
|
|
150
145
|
|
|
151
146
|
/** Names of the policy fields in `current` that are looser than in `base`. */
|
|
152
|
-
export function loosenedPolicy(base: Partial<HookPolicy> & { profiles?: unknown
|
|
147
|
+
export function loosenedPolicy(base: Partial<HookPolicy> & { profiles?: unknown }, current: Partial<HookPolicy> & { profiles?: unknown }): string[] {
|
|
153
148
|
const b = { ...defaultHookPolicy, ...base, worktrees: { ...defaultHookPolicy.worktrees, ...base.worktrees } }, c = { ...defaultHookPolicy, ...current, worktrees: { ...defaultHookPolicy.worktrees, ...current.worktrees } };
|
|
154
149
|
const out: string[] = [];
|
|
155
150
|
for (const key of ["max_staged_files", "max_staged_changed_lines", "max_commits_per_push", "max_registry_removed_lines"] as const) if (Number(c[key]) > Number(b[key])) out.push(`${key} ${b[key]} → ${c[key]}`);
|
|
@@ -165,7 +160,6 @@ export function loosenedPolicy(base: Partial<HookPolicy> & { profiles?: unknown;
|
|
|
165
160
|
if (before && !after) out.push(`profiles becomes implicit: the explicit list [${before.join(", ")}] was removed`);
|
|
166
161
|
const dropped = before && after ? before.filter(name => !after.includes(name)) : [];
|
|
167
162
|
if (dropped.length) out.push(`profiles drops ${dropped.join(", ")}`);
|
|
168
|
-
out.push(...loosenedDelivery(base, current));
|
|
169
163
|
return out;
|
|
170
164
|
}
|
|
171
165
|
|
package/src/setup.ts
CHANGED
|
@@ -11,12 +11,10 @@ import { join } from "node:path";
|
|
|
11
11
|
* without this a greenfield repository's first `profiles:` line could switch most of them off unreported. A
|
|
12
12
|
* brownfield repository trims the list before its first commit; after that, dropping one is a reported loosening.
|
|
13
13
|
* An existing atdd-bun.yaml is never touched. */
|
|
14
|
-
/** Profiles a repository adopts on purpose, never by default: delivery gates every change on a reviewed tranche record. */
|
|
15
|
-
const OPT_IN = ["delivery"];
|
|
16
14
|
export async function policyInit(root = process.cwd()) {
|
|
17
15
|
const file = join(root, "atdd-bun.yaml");
|
|
18
16
|
if (existsSync(file)) return { ok: true, message: `${file} kept` };
|
|
19
|
-
await writeFile(file, `# Generated by atdd-bun init. The profiles this repository enforces; trim the list before the first commit to\n# adopt gradually. Once committed, removing a profile or the list is reported by the integrity check.\nprofiles: [${concreteProfiles.
|
|
17
|
+
await writeFile(file, `# Generated by atdd-bun init. The profiles this repository enforces; trim the list before the first commit to\n# adopt gradually. Once committed, removing a profile or the list is reported by the integrity check.\nprofiles: [${concreteProfiles.join(", ")}]\n`);
|
|
20
18
|
return { ok: true, message: file };
|
|
21
19
|
}
|
|
22
20
|
|
|
@@ -7,7 +7,5 @@ Before changing code, tests, or `plan/`, follow `.agents/skills/atdd/SKILL.md`:
|
|
|
7
7
|
|
|
8
8
|
When the work reaches a stage whose profiles are not active yet, ask the user what they want to do and propose the matching preset from the package convention `atdd-bun.profiles.presets`.
|
|
9
9
|
|
|
10
|
-
When `delivery` is active, deliver tranches through `.agents/skills/delivery/SKILL.md`: the reviews it requires are recorded under the delivery root and checked in CI.
|
|
11
|
-
|
|
12
10
|
Never modify the toolkit itself (`node_modules/@afokapu/atdd-bun`, or the files atdd-bun generates); change only the configuration it offers, and loosen it only when the user asks. The integrity test and CI fail if the toolkit is modified.
|
|
13
11
|
<!-- atdd-bun:end -->
|
|
@@ -21,11 +21,6 @@ jobs:
|
|
|
21
21
|
- run: bun install --frozen-lockfile
|
|
22
22
|
- run: bun run atdd-bun integrity
|
|
23
23
|
- run: bun run atdd-bun all
|
|
24
|
-
env:
|
|
25
|
-
# The delivery profile's gate: before the merge on pull requests and the merge queue, after it on a push.
|
|
26
|
-
ATDD_DELIVERY_GATE: ${{ github.event_name == 'push' && 'post-merge' || 'merge' }}
|
|
27
|
-
# On a push, the gate judges everything since the previous tip, not only the last commit.
|
|
28
|
-
ATDD_BASE_REF: ${{ github.event_name == 'push' && github.event.before || '' }}
|
|
29
24
|
- run: bun test
|
|
30
25
|
- run: bun run atdd-bun release check
|
|
31
26
|
- uses: actions/upload-artifact@v4
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
schema_version: 1.1.0
|
|
2
|
-
rule_id: delivery.approved-sha-resolves
|
|
3
|
-
kind: rule
|
|
4
|
-
status: active
|
|
5
|
-
name: The approved SHA is a commit in this repository
|
|
6
|
-
statement: >-
|
|
7
|
-
A ready record's approved_sha resolves to a commit in the repository's history, every work entry names a commit in approved_sha's history, and every reviewed stage's last approval does too, each containing the one before it in lifecycle order (REQUIRED).
|
|
8
|
-
terms:
|
|
9
|
-
- term_id: approved_sha
|
|
10
|
-
text: >-
|
|
11
|
-
the commit the closing review approved and the tranche asks to merge.
|
|
12
|
-
content:
|
|
13
|
-
summary: >-
|
|
14
|
-
An approval of a commit nobody can find proves nothing: it may be a typo, a rewritten history, or an invention.
|
|
15
|
-
normative_text: |
|
|
16
|
-
The validator asks Git whether the commit exists. A rebase after approval changes every SHA and therefore needs a fresh review; the record cannot be updated by hand to follow it.
|
|
17
|
-
fix_hint: |
|
|
18
|
-
Record the full or abbreviated SHA the final review named. After a rebase, run the final review again on the new head.
|
|
19
|
-
exceptions:
|
|
20
|
-
- >-
|
|
21
|
-
At the gate, a record the change does not touch is not judged again: it was judged when it merged, and its tranche branch may be gone. The pre-push hook scopes the same way, from where the branch left its base branch; with no base found it judges every record. Everywhere else every ready record is judged.
|
|
22
|
-
- >-
|
|
23
|
-
A shallow clone may lack the commit: CI must fetch full history (the generated workflow uses fetch-depth 0).
|
|
24
|
-
metadata:
|
|
25
|
-
aliases:
|
|
26
|
-
- DELIVERY-APPROVED-SHA-RESOLVES-001
|
|
27
|
-
severity: 3
|
|
28
|
-
# strict: atdd-bun fails on every finding; it has no advisory mode and no ratchet baseline.
|
|
29
|
-
disposition: strict
|
|
30
|
-
introduced_in: 0.8.0
|
|
31
|
-
implementation:
|
|
32
|
-
type: validator
|
|
33
|
-
ref: delivery_evidence
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
schema_version: 1.1.0
|
|
2
|
-
rule_id: delivery.board
|
|
3
|
-
kind: policy
|
|
4
|
-
status: active
|
|
5
|
-
name: Agents talk through a local board, one topic per conversation
|
|
6
|
-
statement: >-
|
|
7
|
-
Where atdd-bun.yaml sets delivery.board, the coordinator and drivers talk through a local message board with atdd-bun chat, never by typing into another agent's pane: one topic for the program, one per tranche, one per review conversation, each agent launched with its identity and the only topics it may use, every message naming its sender and recipients. The board makes the work visible; the evidence record stays the only thing the delivery rules judge.
|
|
8
|
-
terms:
|
|
9
|
-
- term_id: topics
|
|
10
|
-
text: >-
|
|
11
|
-
Three levels, each its own topic, named by atdd-bun chat topic so every agent spells them alike. atdd-<program>: coordinator, drivers and humans; status only (started, blocked, ready, merged, a decision a human must take). atdd-<program>-<tranche>: the tranche's driver; the tasks it gives its writers and their results. atdd-<program>-<tranche>-<stage>-<round>: the driver; one review's request and verdict. A rebuttal opens the next round's topic.
|
|
12
|
-
values:
|
|
13
|
-
program: atdd-bun chat topic <program>
|
|
14
|
-
tranche: atdd-bun chat topic <program> <tranche>
|
|
15
|
-
review: atdd-bun chat topic <program> <tranche> <stage> <round>
|
|
16
|
-
- term_id: identity
|
|
17
|
-
text: >-
|
|
18
|
-
<role>@<tranche> for the whole tranche (driver@auth, writer-glm@auth, reviewer-codex@auth; a later round adds it: reviewer-codex-2@auth); coordinator for the coordinator.
|
|
19
|
-
- term_id: launch
|
|
20
|
-
text: >-
|
|
21
|
-
Whoever starts a coordinator or driver gives it, in its environment, ATDD_AGENT (its identity) and ATDD_TOPICS (the topics it may use, comma-separated), and ATDD_BOARD_URL when the board is not the configured one. The coordinator gives a driver its tranche topic and the program topic; the driver adds each review topic as it opens it. atdd-bun chat refuses any other topic, to read or to write. Writers and reviewers get no board access: each is a headless run whose task is its launch prompt and whose answer is its output, which the driver posts (a verdict as "verdict of <reviewer>"), so their tools stay minimal and no sandbox has to reach the board.
|
|
22
|
-
values:
|
|
23
|
-
coordinator: ATDD_AGENT=coordinator@<program> ATDD_TOPICS=atdd-<program>
|
|
24
|
-
driver: ATDD_AGENT=driver@<tranche> ATDD_TOPICS=atdd-<program>,atdd-<program>-<tranche>
|
|
25
|
-
- term_id: message
|
|
26
|
-
text: >-
|
|
27
|
-
atdd-bun chat post writes the header: from, to, participants and conversation always; kind (task, result, review-request, verdict, rebuttal, status, blocked, heartbeat), stage, reply-to, repo, branch, worktree, goal, base and sha when they apply. The body carries the whole instruction or answer, however long: the thread is the history. A heartbeat carries nothing else; anything that needs an answer goes as blocked, never inside a heartbeat.
|
|
28
|
-
- term_id: commands
|
|
29
|
-
text: >-
|
|
30
|
-
Post with the body on standard input; read what is on a topic; wait for the next message addressed to you on one or several topics (comma-separated), skipping kinds you do not act on (--skip heartbeat), bounded by --timeout so a tool call ends (exit 2: nothing yet). wait remembers where each identity stopped on each set of topics (~/.atdd-board/cursors), so re-arming a watcher is always the same command; --since still overrides it; follow is the listener for a host that wakes its agent on each line a running process prints (Claude Code's Monitor tool): it streams every message addressed to the identity as it arrives and keeps running, keeping the same saved place; a host that wakes its agent only when a process exits runs wait and re-arms it after each wake-up. One listener per identity. When nothing arrives (wait's timeout, or follow's --check-in seconds, 20 minutes by default and 0 to turn it off), the listener prints the owner's check-in, asking where the agent is, whether it is moving or blocked and who knows, whether its stakeholder is informed, and whether it is avoiding ceremony and rabbit holes. The agent acts on it, moving the work or posting once to whoever it waits on, and never posts just to say it is still there. And, for a human, atdd-bun chat alone opens the board: every topic on the left, the selected conversation on the right, live.
|
|
31
|
-
values:
|
|
32
|
-
post: atdd-bun chat post <topic> --to <agent> --kind <kind> [--reply-to <id>] < message.md
|
|
33
|
-
read: atdd-bun chat read <topic> [--mine]
|
|
34
|
-
wait: atdd-bun chat wait <topic>[,<topic>...] [--since <id>] [--skip heartbeat] --timeout 540
|
|
35
|
-
follow: atdd-bun chat follow <topic>[,<topic>...] [--skip heartbeat,progress] [--timeout <seconds>] [--check-in <seconds>]
|
|
36
|
-
board: atdd-bun chat
|
|
37
|
-
- term_id: server
|
|
38
|
-
text: >-
|
|
39
|
-
A local ntfy server, bound to the loopback address, keeping its messages in a cache file. Every agent on the machine reaches it from the same address, so ntfy's per-client limits are raised: requests, subscriptions, daily messages, and new topics (one topic per conversation creates many). With the defaults, ten parallel tranches exhaust them and posts are refused.
|
|
40
|
-
values:
|
|
41
|
-
docker: docker run -d --name atdd-board -p 127.0.0.1:2586:80 -v ~/.atdd-board:/var/cache/ntfy -e NTFY_CACHE_FILE=/var/cache/ntfy/cache.db -e NTFY_CACHE_DURATION=720h -e NTFY_MESSAGE_SIZE_LIMIT=64k -e NTFY_VISITOR_REQUEST_LIMIT_BURST=100000 -e NTFY_VISITOR_REQUEST_LIMIT_REPLENISH=1ms -e NTFY_VISITOR_SUBSCRIPTION_LIMIT=10000 -e NTFY_VISITOR_MESSAGE_DAILY_LIMIT=0 -e NTFY_VISITOR_TOPIC_CREATION_LIMIT_BURST=0 binwiederhier/ntfy serve
|
|
42
|
-
content:
|
|
43
|
-
summary: >-
|
|
44
|
-
Typing into an agent's pane loses messages to startup screens, busy prompts and cut pastes. A board with one topic per conversation delivers every message, keeps the history, and lets a human follow any thread.
|
|
45
|
-
normative_text: |
|
|
46
|
-
A reviewer never reads the tranche's conversation: its brief is the review request in its launch prompt, and its verdict is its output. The driver records that verdict in evidence.yaml and posts it on the review topic; the board is never evidence.
|
|
47
|
-
Agents use atdd-bun chat only. It refuses an address that is not local and a topic the agent was not given. Never call ntfy directly: in the ntfy client -u means credentials, and a topic without a server goes to the public ntfy.sh.
|
|
48
|
-
A running session that must be woken is reached through its own channel (codex queue --thread <id>; --resume <session> for Claude and Pi), never by typing into its pane; a driver's launch prompt tells it to wait on its topics between steps.
|
|
49
|
-
fix_hint: |
|
|
50
|
-
Derive the topic with atdd-bun chat topic, launch coordinators and drivers with ATDD_AGENT and ATDD_TOPICS, and post with --to. To follow the work, run atdd-bun chat in a terminal pane. ntfy cannot list its topics, so the first message on a topic also posts the topic's name to the directory topic atdd-topics, which the view reads.
|
|
51
|
-
exceptions:
|
|
52
|
-
- >-
|
|
53
|
-
The board is opt-in: without a delivery.board block there is none, and atdd-bun chat refuses to post or read. With the block and no url it is at http://127.0.0.1:2586; ATDD_BOARD_URL moves it on one machine but never switches it on. Switching it on or off changes how agents talk, not what a record must prove, so it is not a loosening.
|
|
54
|
-
metadata:
|
|
55
|
-
aliases:
|
|
56
|
-
- DELIVERY-BOARD-001
|
|
57
|
-
introduced_in: 0.10.0
|
|
58
|
-
# A policy, not a rule: how agents talk cannot be validated from the repository. atdd-bun chat enforces its guard rails.
|
|
59
|
-
implementation:
|
|
60
|
-
type: none
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
schema_version: 1.1.0
|
|
2
|
-
rule_id: delivery.config-schema
|
|
3
|
-
kind: rule
|
|
4
|
-
status: active
|
|
5
|
-
name: The delivery policy in atdd-bun.yaml is well-formed
|
|
6
|
-
statement: >-
|
|
7
|
-
The `delivery:` block of atdd-bun.yaml validates against delivery-config.schema.json: known keys only, each stage naming a writer list, a reviewer list or both, models named in lowercase, independence one of fresh-process or different-model, the root in canonical form (no leading, trailing or doubled slash, no dot segment) not overlapping the plan, source, test, e2e or telemetry root, and, inside docs/, exactly docs/delivery/tranches (REQUIRED).
|
|
8
|
-
terms:
|
|
9
|
-
- term_id: policy
|
|
10
|
-
text: >-
|
|
11
|
-
the `delivery:` block: root, independence, stages (plan, red, green, refactor, final; per stage: writer, reviewer, optional independence), fallback and commands. Every key is optional; an omitted key takes the package default.
|
|
12
|
-
content:
|
|
13
|
-
summary: >-
|
|
14
|
-
The skill and the validators read the same policy. A misspelled key would be ignored by one and not the other, so a malformed block is a finding, and the rest of the run judges evidence against the package defaults.
|
|
15
|
-
normative_text: |
|
|
16
|
-
The policy decides which models may write and review each stage (an allowed set, in no order; "*" allows any), how independent a reviewer must be, and when a model counts as unavailable. It is read by the delivery skill when it dispatches work and by this profile when it judges the record, so both must see the same, valid document.
|
|
17
|
-
fix_hint: |
|
|
18
|
-
Correct the key the finding names. The defaults are:
|
|
19
|
-
|
|
20
|
-
delivery:
|
|
21
|
-
root: docs/delivery/tranches
|
|
22
|
-
independence: different-model
|
|
23
|
-
stages:
|
|
24
|
-
plan: { writer: [codex, claude-opus, kimi], reviewer: [glm, claude-opus, codex, kimi] }
|
|
25
|
-
red: { writer: [glm, claude-sonnet, deepseek-flash, claude-opus, codex, kimi] }
|
|
26
|
-
green: { writer: [glm, claude-sonnet, deepseek-flash, claude-opus, codex, kimi] }
|
|
27
|
-
refactor: { writer: [glm, claude-sonnet, deepseek-flash, claude-opus, codex, kimi] }
|
|
28
|
-
final: { reviewer: [codex, glm, claude-opus, kimi] }
|
|
29
|
-
|
|
30
|
-
independence defaults to different-model. A stage may name a writer, a reviewer or both. The 0.9
|
|
31
|
-
names plan_review, test_review, code_review and final_review are still read, as plan, red, refactor
|
|
32
|
-
and final, with authors as writers and reviewers as reviewers.
|
|
33
|
-
exceptions:
|
|
34
|
-
- >-
|
|
35
|
-
Adoption is not exempt from the gate: the pull request that adopts delivery brings the generated skill and workflow,
|
|
36
|
-
outside the delivery root, so it carries its own ready tranche record like any other change. A later change to
|
|
37
|
-
atdd-bun.yaml alone needs no record (delivery.merge-gate).
|
|
38
|
-
- >-
|
|
39
|
-
Inert until adoption: with no `delivery:` block and `delivery` absent from `profiles:`, no delivery rule emits anything.
|
|
40
|
-
metadata:
|
|
41
|
-
aliases:
|
|
42
|
-
- DELIVERY-CONFIG-SCHEMA-001
|
|
43
|
-
severity: 3
|
|
44
|
-
# strict: atdd-bun fails on every finding; it has no advisory mode and no ratchet baseline.
|
|
45
|
-
disposition: strict
|
|
46
|
-
introduced_in: 0.8.0
|
|
47
|
-
implementation:
|
|
48
|
-
type: validator
|
|
49
|
-
ref: delivery_evidence
|