tdd-enforcer 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapters/pi/index.ts +44 -1
- package/engine/git.ts +11 -1
- package/engine/index.ts +1 -1
- package/engine/transition.ts +3 -3
- package/package.json +4 -1
- package/skills/tdd-init/SKILL.md +90 -0
package/adapters/pi/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { loadPhaseState, loadConfig, savePhaseState, initGit, snapshot } from "../../engine/index.js";
|
|
4
|
+
import { loadPhaseState, loadConfig, savePhaseState, initGit, resetGit, snapshot } from "../../engine/index.js";
|
|
5
5
|
import { registerTools } from "./tools.js";
|
|
6
6
|
import { registerHooks } from "./hooks.js";
|
|
7
7
|
import { loadTddState } from "./helpers.js";
|
|
@@ -163,6 +163,49 @@ export default function (pi: ExtensionAPI) {
|
|
|
163
163
|
},
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
+
pi.registerCommand("tdd:reset", {
|
|
167
|
+
description:
|
|
168
|
+
"WARNING: Destroys ALL TDD snapshot history and resets to RED phase. " +
|
|
169
|
+
"Working tree is preserved. Run /tdd:on to re-enable after reset.",
|
|
170
|
+
handler: async (_args: string, ctx: ExtensionContext) => {
|
|
171
|
+
const root = ctx.cwd;
|
|
172
|
+
const tddDir = join(root, ".pi", "tdd");
|
|
173
|
+
|
|
174
|
+
tddLog(tddDir, "INFO", "tdd:reset: starting");
|
|
175
|
+
|
|
176
|
+
if (!existsSync(tddDir)) {
|
|
177
|
+
tddLog(tddDir, "WARN", "tdd:reset: missing .pi/tdd/ directory");
|
|
178
|
+
ctx.ui.notify("No .pi/tdd/ directory found — nothing to reset.", "error");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Nuke git history and re-init
|
|
183
|
+
try {
|
|
184
|
+
resetGit(root);
|
|
185
|
+
tddLog(tddDir, "INFO", "tdd:reset: git reset and re-initialised");
|
|
186
|
+
} catch (e) {
|
|
187
|
+
tddLog(tddDir, "ERROR", "tdd:reset: git reset failed", {
|
|
188
|
+
error: (e as Error).message,
|
|
189
|
+
});
|
|
190
|
+
ctx.ui.notify("Failed to reset private git repo.", "error");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Snapshot current working tree
|
|
195
|
+
snapshot(root, "red");
|
|
196
|
+
tddLog(tddDir, "INFO", "tdd:reset: snapshot taken");
|
|
197
|
+
|
|
198
|
+
// Reset state to RED (disabled, user must run /tdd:on)
|
|
199
|
+
savePhaseState(root, { enabled: false, current: "red" });
|
|
200
|
+
tddLog(tddDir, "INFO", "tdd:reset: complete");
|
|
201
|
+
|
|
202
|
+
ctx.ui.notify(
|
|
203
|
+
"TDD snapshot history reset. Run /tdd:on to re-enable enforcement.",
|
|
204
|
+
"warning",
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
166
209
|
registerTools(pi);
|
|
167
210
|
registerHooks(pi);
|
|
168
211
|
}
|
package/engine/git.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync, type ExecSyncOptions } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, unlinkSync, rmSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
5
|
const TDD_DIR = ".pi/tdd";
|
|
@@ -40,6 +40,16 @@ export function initGit(projectRoot: string): void {
|
|
|
40
40
|
gitExec('commit --allow-empty -m "tdd: init"', projectRoot, { stdio: "pipe" as const });
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** Destroy the private git repo and re-init from scratch. */
|
|
44
|
+
export function resetGit(projectRoot: string): void {
|
|
45
|
+
const tddPath = join(projectRoot, TDD_DIR);
|
|
46
|
+
const gitDir = join(tddPath, ".git");
|
|
47
|
+
if (existsSync(gitDir)) {
|
|
48
|
+
rmSync(gitDir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
initGit(projectRoot);
|
|
51
|
+
}
|
|
52
|
+
|
|
43
53
|
/** Stage all + commit with --allow-empty so every phase transition has a labeled commit. */
|
|
44
54
|
export function snapshot(projectRoot: string, phase: string): string {
|
|
45
55
|
gitExec("add -A", projectRoot, { stdio: "pipe" as const });
|
package/engine/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { isAllowed, disallowedFiles } from "./enforce.js";
|
|
2
|
-
export { initGit, snapshot, changesSinceSnapshot, changesSince, modifiedFiles, untrackedFiles, restoreFilesTo, gitStashCreate, headHash, headMessage, hasParent, resetHard, undoLastCommit } from "./git.js";
|
|
2
|
+
export { initGit, resetGit, snapshot, changesSinceSnapshot, changesSince, modifiedFiles, untrackedFiles, restoreFilesTo, gitStashCreate, headHash, headMessage, hasParent, resetHard, undoLastCommit } from "./git.js";
|
|
3
3
|
export { loadConfig } from "./config.js";
|
|
4
4
|
export { loadPhaseState, savePhaseState } from "./state.js";
|
|
5
5
|
export { nextPhase, checkGate, getDisallowedChanges } from "./transition.js";
|
package/engine/transition.ts
CHANGED
|
@@ -36,7 +36,7 @@ export async function checkGate(
|
|
|
36
36
|
if (result.passed) {
|
|
37
37
|
return {
|
|
38
38
|
passed: false,
|
|
39
|
-
message: "Tests
|
|
39
|
+
message: "Tests passed. Add a failing test before transitioning to GREEN.",
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
return { passed: true, message: "Tests fail — proceed to GREEN." };
|
|
@@ -45,7 +45,7 @@ export async function checkGate(
|
|
|
45
45
|
if (!result.passed) {
|
|
46
46
|
return {
|
|
47
47
|
passed: false,
|
|
48
|
-
message: "Tests
|
|
48
|
+
message: "Tests failed. Fix them before transitioning to REFACTOR.",
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
return { passed: true, message: "All tests pass — proceeding." };
|
|
@@ -54,7 +54,7 @@ export async function checkGate(
|
|
|
54
54
|
if (!result.passed) {
|
|
55
55
|
return {
|
|
56
56
|
passed: false,
|
|
57
|
-
message: "Tests
|
|
57
|
+
message: "Tests failed. Fix them before transitioning to RED.",
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
return { passed: true, message: "All tests pass — proceeding." };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tdd-enforcer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"pi": {
|
|
15
15
|
"extensions": [
|
|
16
16
|
"./adapters/pi/index.ts"
|
|
17
|
+
],
|
|
18
|
+
"skills": [
|
|
19
|
+
"./skills"
|
|
17
20
|
]
|
|
18
21
|
}
|
|
19
22
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tdd-init
|
|
3
|
+
description: Use when the TDD enforcer extension reports missing configuration — .pi/tdd/ directory, rules.json, or state.json not found. Also use when starting TDD on a new project.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TDD Init
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Set up the TDD enforcer. The extension enforces the Red-Green-Refactor cycle by restricting which files the agent can modify per phase.
|
|
11
|
+
|
|
12
|
+
## What to Create
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
project-root/
|
|
16
|
+
.pi/
|
|
17
|
+
tdd/
|
|
18
|
+
rules.json # File patterns and test commands
|
|
19
|
+
state.json # Phase state (create, then /tdd:on sets enabled)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## rules.json
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"allowedRedPhaseFiles": ["tests/**/*.test.ts"],
|
|
27
|
+
"allowedGreenPhaseFiles": ["src/**/*.ts"],
|
|
28
|
+
"testCommands": ["npm run test"],
|
|
29
|
+
"timeoutSeconds": 120
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
| Field | What | Why |
|
|
34
|
+
|-------|------|-----|
|
|
35
|
+
| `allowedRedPhaseFiles` | Glob patterns for files writable in RED phase | Typically test files |
|
|
36
|
+
| `allowedGreenPhaseFiles` | Glob patterns for files writable in GREEN phase | Typically implementation files |
|
|
37
|
+
| `testCommands` | Non-interactive commands (string or array) | Run on phase transitions to check gate |
|
|
38
|
+
| `timeoutSeconds` | Per-command timeout (default 120) | Prevents hung suites |
|
|
39
|
+
|
|
40
|
+
- Globs are relative to project root
|
|
41
|
+
- Files matching neither set are free in all phases
|
|
42
|
+
- All 3 array fields **must** be non-empty
|
|
43
|
+
|
|
44
|
+
## state.json
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"enabled": false,
|
|
49
|
+
"current": "red"
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Start with `enabled: false`. Run `/tdd:on` — it validates config, initialises the private git repo, snapshots the working tree, and sets `enabled: true`.
|
|
54
|
+
|
|
55
|
+
## Setup Steps
|
|
56
|
+
|
|
57
|
+
1. Create `.pi/tdd/` directory
|
|
58
|
+
2. Create `.pi/tdd/rules.json` with file patterns and test commands
|
|
59
|
+
3. Create `.pi/tdd/state.json` with `enabled: false, current: "red"`
|
|
60
|
+
4. Run `/tdd:on` to enable enforcement
|
|
61
|
+
|
|
62
|
+
## TDD Cycle
|
|
63
|
+
|
|
64
|
+
| Phase | Allowed | Gate to advance |
|
|
65
|
+
|-------|---------|-----------------|
|
|
66
|
+
| RED | Test files only | Tests must fail |
|
|
67
|
+
| GREEN | Implementation files only | Tests must pass |
|
|
68
|
+
| REFACTOR | All files | Tests must pass |
|
|
69
|
+
|
|
70
|
+
Use `next_tdd_phase` to advance, `previous_tdd_phase` to revert.
|
|
71
|
+
|
|
72
|
+
## Common Patterns
|
|
73
|
+
|
|
74
|
+
**Standard split:**
|
|
75
|
+
```json
|
|
76
|
+
"allowedRedPhaseFiles": ["tests/**/*.test.ts"],
|
|
77
|
+
"allowedGreenPhaseFiles": ["src/**/*.ts"]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Monorepo:**
|
|
81
|
+
```json
|
|
82
|
+
"testCommands": ["npm run test:unit", "npm run test:e2e"]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Recovery
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
/tdd:reset # Destroys snapshot history, resets to RED (disabled)
|
|
89
|
+
/tdd:on # Re-enable with fresh snapshot
|
|
90
|
+
```
|