@afokapu/atdd-bun 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -137,16 +137,17 @@ bun run atdd-bun init
137
137
  ```
138
138
 
139
139
  It creates `.githooks/` dispatchers, sets a worktree-local `core.hooksPath`,
140
- and generates `.github/workflows/atdd-bun.yml` when it is absent. It never
141
- overwrites another hook path or an existing generated workflow unless you
142
- explicitly pass `--replace`. Installing the dependency alone deliberately does
140
+ generates `.github/workflows/atdd-bun.yml`, and installs the coding-agent skill
141
+ when they are absent. It never overwrites another hook path, an existing
142
+ generated workflow, or an existing skill unless you explicitly pass `--replace`. Installing the dependency alone deliberately does
143
143
  neither: package installation must not mutate a repository through postinstall.
144
144
 
145
- Use `hooks install` or `ci init` when only one surface is wanted:
145
+ Use `hooks install`, `ci init`, or `agent init` when only one surface is wanted:
146
146
 
147
147
  ```sh
148
148
  bun run atdd-bun hooks install
149
149
  bun run atdd-bun ci init
150
+ bun run atdd-bun agent init
150
151
  ```
151
152
 
152
153
  The hooks enforce protected-branch blocking, micro-commit limits, mass-delete
@@ -161,6 +162,24 @@ bun run atdd-bun hooks uninstall
161
162
  Hooks can be bypassed by Git and therefore are never the merge gate. The CI
162
163
  workflow and GitHub branch ruleset are the authority for merging.
163
164
 
165
+ ## Agent skill: the lifecycle in the agent's context
166
+
167
+ `agent init` gives every coding agent the same short ATDD skill:
168
+
169
+ - `.agents/skills/atdd/SKILL.md`: the vendor-neutral Agent Skills path (Codex,
170
+ GitHub Copilot, Cursor, Gemini CLI, and others);
171
+ - `.claude/skills/atdd/SKILL.md`: Claude Code;
172
+ - a managed `<!-- atdd-bun:start -->` block in `AGENTS.md` pointing at the skill,
173
+ for agents that read `AGENTS.md` but not skills. The rest of `AGENTS.md` is
174
+ never touched.
175
+
176
+ The skill names the lifecycle PLAN → RED → GREEN → SMOKE → REFACTOR → TRACE, the
177
+ conventions each stage follows, and the `atdd-bun` profile that gates it. It
178
+ points at the conventions shipped in this package instead of restating them, so
179
+ it stays correct as they change; after upgrading, refresh it with
180
+ `bun run atdd-bun agent init --replace`. The skill steers the agent; the
181
+ profiles, hooks, and CI remain the enforcement.
182
+
164
183
  ## CI: the merge gate
165
184
 
166
185
  Generate the repository-owned workflow with:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-bun",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/afokapu/atdd-bun.git"
package/src/agent.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+
5
+ const root = resolve(import.meta.dir, "..");
6
+ const version = (await Bun.file(join(root, "package.json")).json() as { version: string }).version;
7
+ const render = async (path: string) => (await readFile(join(root, "templates/agents", path), "utf8")).replace("{{VERSION}}", version);
8
+ // .agents/skills is the vendor-neutral Agent Skills path (Codex, Copilot, Cursor, Gemini CLI, …); .claude/skills is Claude Code's.
9
+ const skillPaths = [".agents/skills/atdd/SKILL.md", ".claude/skills/atdd/SKILL.md"];
10
+ const block = /<!-- atdd-bun:start[\s\S]*?<!-- atdd-bun:end -->\n?/;
11
+
12
+ /** Write the ATDD skill for every agent and a managed pointer block in AGENTS.md. Existing files and an
13
+ * existing block are kept unless `replace`; the rest of AGENTS.md is never touched. */
14
+ export async function agentInit(repo = process.cwd(), replace = false) {
15
+ const written: string[] = [], kept: string[] = [];
16
+ for (const path of skillPaths) {
17
+ const output = join(repo, path);
18
+ if (existsSync(output) && !replace) { kept.push(output); continue; }
19
+ await mkdir(dirname(output), { recursive: true }); await writeFile(output, await render("atdd/SKILL.md")); written.push(output);
20
+ }
21
+ const agents = join(repo, "AGENTS.md"), current = existsSync(agents) ? await readFile(agents, "utf8") : "", managed = await render("AGENTS.block.md");
22
+ if (block.test(current) && !replace) kept.push(agents);
23
+ else { await writeFile(agents, block.test(current) ? current.replace(block, managed) : current + (current && !current.endsWith("\n\n") ? (current.endsWith("\n") ? "\n" : "\n\n") : "") + managed); written.push(agents); }
24
+ if (!written.length) return { ok: false, message: `${kept.join(", ")} exist; use --replace` };
25
+ return { ok: true, message: written.join("\n") };
26
+ }
27
+ export async function agentStatus(repo = process.cwd()) {
28
+ const agents = join(repo, "AGENTS.md"), missing = skillPaths.map(path => join(repo, path)).filter(path => !existsSync(path));
29
+ if (!existsSync(agents) || !block.test(await readFile(agents, "utf8"))) missing.push(`${agents} (atdd-bun block)`);
30
+ return { ok: !missing.length, message: missing.length ? `missing: ${missing.join(", ")}` : [...skillPaths, "AGENTS.md"].map(path => join(repo, path)).join("\n") };
31
+ }
package/src/cli.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { enforce, profileNames, type Profile } from "./enforce";
3
3
  import { finishWorktree, hookEvents, hooksStatus, installHooks, runHook, startWorktree, uninstallHooks, worktreeStatus } from "./hooks";
4
4
  import { ciInit, ciStatus } from "./ci";
5
+ import { agentInit, agentStatus } from "./agent";
5
6
  import { releaseCheck } from "./release";
6
7
  import { initializeRepository } from "./setup";
7
8
 
@@ -14,6 +15,7 @@ const usage = {
14
15
  "atdd-bun hooks <install|uninstall|status> [--replace]",
15
16
  "atdd-bun worktree <start|finish|status>",
16
17
  "atdd-bun ci <init|status> [--replace]",
18
+ "atdd-bun agent <init|status> [--replace]",
17
19
  "atdd-bun release check",
18
20
  ],
19
21
  profiles: profileNames,
@@ -64,6 +66,10 @@ if (args[0] === "ci") {
64
66
  const result = args[1] === "init" ? await ciInit(process.cwd(), args.includes("--replace")) : args[1] === "status" ? await ciStatus() : fail("ci requires init or status");
65
67
  console[result.ok ? "log" : "error"](result.message); process.exit(result.ok ? 0 : 1);
66
68
  }
69
+ if (args[0] === "agent") {
70
+ const result = args[1] === "init" ? await agentInit(process.cwd(), args.includes("--replace")) : args[1] === "status" ? await agentStatus() : fail("agent requires init or status");
71
+ console[result.ok ? "log" : "error"](result.message); process.exit(result.ok ? 0 : 1);
72
+ }
67
73
  if (args[0] === "release") {
68
74
  if (args[1] !== "check") fail("release requires check");
69
75
  const result = await releaseCheck(); console[result.ok ? "log" : "error"](result.message); process.exit(result.ok ? 0 : 1);
package/src/setup.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { ciInit, ciStatus } from "./ci";
2
2
  import { hooksStatus, installHooks } from "./hooks";
3
+ import { agentInit, agentStatus } from "./agent";
3
4
 
4
- /** Install the package's two opt-in local surfaces without touching unrelated
5
+ /** Install the package's three opt-in local surfaces without touching unrelated
5
6
  * workflows or hook paths. Dependency installation itself never calls this. */
6
7
  export async function initializeRepository(root = process.cwd(), replace = false) {
7
8
  const existingHooks = await hooksStatus(root);
@@ -11,5 +12,9 @@ export async function initializeRepository(root = process.cwd(), replace = false
11
12
  const existingCi = await ciStatus(root);
12
13
  const ci = replace || !existingCi.ok ? await ciInit(root, replace) : existingCi;
13
14
  if (!ci.ok) return { ok: false, message: `CI workflow: ${ci.message}` };
14
- return { ok: true, message: `hooks: ${hooks.message}\nCI workflow: ${ci.message}` };
15
+
16
+ const existingAgent = await agentStatus(root);
17
+ const agent = replace || !existingAgent.ok ? await agentInit(root, replace) : existingAgent;
18
+ if (!agent.ok) return { ok: false, message: `agent skill: ${agent.message}` };
19
+ return { ok: true, message: `hooks: ${hooks.message}\nCI workflow: ${ci.message}\nagent skill: ${agent.message}` };
15
20
  }
@@ -0,0 +1,5 @@
1
+ <!-- atdd-bun:start — generated by @afokapu/atdd-bun {{VERSION}}; run `atdd-bun agent init --replace` to refresh -->
2
+ ## ATDD lifecycle
3
+
4
+ Before changing code, tests, or `plan/`, follow `.agents/skills/atdd/SKILL.md`: PLAN → RED → GREEN → SMOKE → REFACTOR → TRACE, passing each stage's `atdd-bun` gate before starting the next.
5
+ <!-- atdd-bun:end -->
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: atdd
3
+ description: Use before writing or changing code, tests, or plan/ files in this repository. New intent is decomposed and delivered through the ATDD lifecycle PLAN → RED → GREEN → SMOKE → REFACTOR → TRACE, each stage gated by atdd-bun. Skip PLAN only when the change fits an acceptance that already exists.
4
+ ---
5
+ <!-- Generated by @afokapu/atdd-bun {{VERSION}}. Do not edit; run `atdd-bun agent init --replace`. -->
6
+
7
+ Conventions live in `node_modules/@afokapu/atdd-bun/` (`planner-nodes/nodes/`, `conventions/`). Read the ones a stage names; do not restate them. Finish each stage by passing its gate before starting the next.
8
+
9
+ 1. PLAN — Decompose the intent into wagon → WMBT → acceptance → train/interlocking → journey → contract under `plan/`, following `planner.decomposition.*`; every WMBT declares a SMOKE acceptance. Gate: `bun run atdd-bun planner`.
10
+ 2. RED — For each acceptance, write a test headed `// URN: test:{wagon}:{feature}:{ACC-ID}` and `// Phase: RED` that fails for the missing behaviour (`tester.bun.red-*`). Gate: `bun run atdd-bun tester`.
11
+ 3. GREEN — Write the least code that passes; each source file carries `URN: component:{wagon}:{feature}:{Name}:{side}:{layer}` and a `Tested-By:` block (`coder.bun.green-*`). Gate: `bun test`.
12
+ 4. SMOKE — Prove the SMOKE acceptance through the real entry point with no mocks or spies, asserting only on observable output: HTTP, markup, stdout, exit code (`tester.bun.smoke-*`, `planner.smoke.*`). Gate: `bun run atdd-bun tester`.
13
+ 5. REFACTOR — With tests green, reduce complexity, fix layering, and remove security faults until the rules pass, without changing behaviour (`coder.bun.complexity-*`, `quality-*`, `composition-*`, `security-*`). Gate: `bun run atdd-bun coder security`.
14
+ 6. TRACE — Every acceptance has a test, every test resolves to a declared acceptance, every source file resolves to its tests. Gate: `bun run atdd-bun traceability`, then `bun run atdd-bun all`.
15
+
16
+ When a gate fails, open `<rule_id>.convention.yaml` for the reported rule ID and fix the artifact. Never skip, suppress, or edit a convention to get green.