tdd-enforcer 0.1.0 → 0.1.1
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/hooks.ts +74 -13
- package/adapters/pi/index.ts +44 -4
- package/adapters/pi/log.ts +30 -0
- package/adapters/pi/tools.ts +95 -1
- package/package.json +7 -3
package/adapters/pi/hooks.ts
CHANGED
|
@@ -1,59 +1,120 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
1
2
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { isToolCallEventType, isBashToolResult } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
import { isAllowed } from "../../engine/enforce.js";
|
|
4
5
|
import { changesSinceSnapshot, restoreFiles } from "../../engine/git.js";
|
|
5
6
|
import { loadTddState } from "./helpers.js";
|
|
7
|
+
import { tddLog } from "./log.js";
|
|
6
8
|
|
|
7
9
|
export function registerHooks(pi: ExtensionAPI): void {
|
|
8
10
|
pi.on("tool_call", async (event, ctx: ExtensionContext) => {
|
|
9
11
|
const root = ctx.cwd;
|
|
12
|
+
const tddDir = join(root, ".pi", "tdd");
|
|
10
13
|
const tdd = loadTddState(root);
|
|
11
|
-
if (!tdd.ok)
|
|
14
|
+
if (!tdd.ok) {
|
|
15
|
+
tddLog(tddDir, "WARN", "tool_call: TDD not active, edit passes through", {
|
|
16
|
+
toolName: (event as any).toolName,
|
|
17
|
+
reason: tdd.reason,
|
|
18
|
+
});
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
12
21
|
|
|
13
22
|
const { state, config } = tdd;
|
|
14
23
|
const phase = state.current;
|
|
15
24
|
|
|
16
|
-
// write/edit pre-block
|
|
17
25
|
let filePath: string | undefined;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
let toolName: string | undefined;
|
|
27
|
+
if (isToolCallEventType("write", event)) {
|
|
28
|
+
toolName = "write";
|
|
29
|
+
filePath = (event as any).input?.path;
|
|
30
|
+
} else if (isToolCallEventType("edit", event)) {
|
|
31
|
+
toolName = "edit";
|
|
32
|
+
filePath = (event as any).input?.path;
|
|
33
|
+
} else {
|
|
34
|
+
tddLog(tddDir, "DEBUG", "tool_call: non-file tool, ignored", {
|
|
35
|
+
toolName: (event as any).toolName,
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!filePath) {
|
|
41
|
+
tddLog(tddDir, "WARN", "tool_call: no path in input, cannot block", {
|
|
42
|
+
toolName,
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
21
46
|
|
|
22
|
-
|
|
47
|
+
const allowed = isAllowed(filePath, phase, config);
|
|
48
|
+
tddLog(tddDir, "DEBUG", "tool_call: check", { toolName, filePath, phase, allowed });
|
|
23
49
|
|
|
24
|
-
if (!
|
|
50
|
+
if (!allowed) {
|
|
51
|
+
tddLog(tddDir, "INFO", "tool_call: blocked file modification", {
|
|
52
|
+
toolName,
|
|
53
|
+
filePath,
|
|
54
|
+
phase,
|
|
55
|
+
});
|
|
25
56
|
return {
|
|
26
57
|
block: true,
|
|
27
58
|
reason: `TDD ${phase.toUpperCase()}: "${filePath}" is locked in this phase.`,
|
|
28
59
|
};
|
|
29
60
|
}
|
|
61
|
+
|
|
62
|
+
tddLog(tddDir, "DEBUG", "tool_call: allowed", { toolName, filePath, phase });
|
|
30
63
|
});
|
|
31
64
|
|
|
32
65
|
pi.on("tool_result", async (event, ctx: ExtensionContext) => {
|
|
33
66
|
if (!isBashToolResult(event)) return;
|
|
34
67
|
|
|
35
68
|
const root = ctx.cwd;
|
|
69
|
+
const tddDir = join(root, ".pi", "tdd");
|
|
36
70
|
const tdd = loadTddState(root);
|
|
37
|
-
if (!tdd.ok)
|
|
71
|
+
if (!tdd.ok) {
|
|
72
|
+
tddLog(tddDir, "WARN", "tool_result: TDD not active, bash passes through", {
|
|
73
|
+
reason: tdd.reason,
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
38
77
|
|
|
39
78
|
const { state, config } = tdd;
|
|
40
79
|
const phase = state.current;
|
|
41
|
-
if (phase === "refactor")
|
|
80
|
+
if (phase === "refactor") {
|
|
81
|
+
tddLog(tddDir, "DEBUG", "tool_result: refactor phase, no check");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
42
84
|
|
|
43
85
|
const changed = changesSinceSnapshot(root);
|
|
44
|
-
if (changed.length === 0)
|
|
86
|
+
if (changed.length === 0) {
|
|
87
|
+
tddLog(tddDir, "DEBUG", "tool_result: no changes since snapshot");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
45
90
|
|
|
46
91
|
const violations = changed.filter((f) => !isAllowed(f, phase, config));
|
|
47
|
-
if (violations.length === 0)
|
|
92
|
+
if (violations.length === 0) {
|
|
93
|
+
tddLog(tddDir, "DEBUG", "tool_result: no violations among changed files", {
|
|
94
|
+
changed,
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
tddLog(tddDir, "INFO", "tool_result: reverting violations", {
|
|
100
|
+
phase,
|
|
101
|
+
violations,
|
|
102
|
+
allChanged: changed,
|
|
103
|
+
});
|
|
48
104
|
|
|
49
105
|
restoreFiles(root, violations);
|
|
50
106
|
|
|
51
107
|
const existingText = event.content.map((c) => ("text" in c ? c.text : "")).join("");
|
|
52
108
|
return {
|
|
53
109
|
content: [
|
|
54
|
-
{
|
|
110
|
+
{
|
|
111
|
+
type: "text",
|
|
112
|
+
text:
|
|
113
|
+
existingText +
|
|
114
|
+
`\n\n⚠️ TDD: Bash modified files locked in ${phase.toUpperCase()} phase. ` +
|
|
115
|
+
`Reverted: ${violations.join(", ")}`,
|
|
116
|
+
},
|
|
55
117
|
],
|
|
56
118
|
};
|
|
57
119
|
});
|
|
58
|
-
|
|
59
120
|
}
|
package/adapters/pi/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { loadPhaseState, loadConfig, savePhaseState, initGit } from "../../engin
|
|
|
5
5
|
import { registerTools } from "./tools.js";
|
|
6
6
|
import { registerHooks } from "./hooks.js";
|
|
7
7
|
import { loadTddState } from "./helpers.js";
|
|
8
|
+
import { tddLog } from "./log.js";
|
|
8
9
|
|
|
9
10
|
export default function (pi: ExtensionAPI) {
|
|
10
11
|
pi.registerCommand("tdd:on", {
|
|
@@ -13,19 +14,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
13
14
|
const root = ctx.cwd;
|
|
14
15
|
const tddDir = join(root, ".pi", "tdd");
|
|
15
16
|
|
|
17
|
+
tddLog(tddDir, "INFO", "tdd:on: starting");
|
|
18
|
+
|
|
16
19
|
if (!existsSync(tddDir)) {
|
|
20
|
+
tddLog(tddDir, "WARN", "tdd:on: missing .pi/tdd/ directory");
|
|
17
21
|
ctx.ui.notify("Missing .pi/tdd/ directory. See the tdd-init skill to learn how to set up TDD configs.", "error");
|
|
18
22
|
return;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
const rulesPath = join(tddDir, "rules.json");
|
|
22
26
|
if (!existsSync(rulesPath)) {
|
|
27
|
+
tddLog(tddDir, "WARN", "tdd:on: missing rules.json");
|
|
23
28
|
ctx.ui.notify("Missing .pi/tdd/rules.json. See the tdd-init skill to learn how to set up TDD configs.", "error");
|
|
24
29
|
return;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
const phasePath = join(tddDir, "phase.json");
|
|
28
33
|
if (!existsSync(phasePath)) {
|
|
34
|
+
tddLog(tddDir, "WARN", "tdd:on: missing phase.json");
|
|
29
35
|
ctx.ui.notify("Missing .pi/tdd/phase.json. See the tdd-init skill to learn how to set up TDD configs.", "error");
|
|
30
36
|
return;
|
|
31
37
|
}
|
|
@@ -33,19 +39,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
33
39
|
let state;
|
|
34
40
|
try {
|
|
35
41
|
state = loadPhaseState(root);
|
|
36
|
-
} catch {
|
|
42
|
+
} catch (e) {
|
|
43
|
+
tddLog(tddDir, "WARN", "tdd:on: invalid phase.json", {
|
|
44
|
+
error: (e as Error).message,
|
|
45
|
+
});
|
|
37
46
|
ctx.ui.notify("Invalid .pi/tdd/phase.json. Fix or delete it, then run /tdd:on again.", "error");
|
|
38
47
|
return;
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
try {
|
|
42
51
|
loadConfig(root);
|
|
43
|
-
} catch {
|
|
52
|
+
} catch (e) {
|
|
53
|
+
tddLog(tddDir, "WARN", "tdd:on: invalid rules.json", {
|
|
54
|
+
error: (e as Error).message,
|
|
55
|
+
});
|
|
44
56
|
ctx.ui.notify("Invalid .pi/tdd/rules.json. Fix or delete it, then run /tdd:on again.", "error");
|
|
45
57
|
return;
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
if (state.enabled) {
|
|
61
|
+
tddLog(tddDir, "INFO", "tdd:on: already enabled", {
|
|
62
|
+
phase: state.current,
|
|
63
|
+
});
|
|
49
64
|
ctx.ui.notify(`TDD already enabled — ${state.current.toUpperCase()} phase`, "info");
|
|
50
65
|
return;
|
|
51
66
|
}
|
|
@@ -53,14 +68,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
53
68
|
if (!existsSync(join(tddDir, ".git", "HEAD"))) {
|
|
54
69
|
try {
|
|
55
70
|
initGit(root);
|
|
56
|
-
|
|
71
|
+
tddLog(tddDir, "INFO", "tdd:on: git initialised");
|
|
72
|
+
} catch (e) {
|
|
73
|
+
tddLog(tddDir, "ERROR", "tdd:on: git init failed", {
|
|
74
|
+
error: (e as Error).message,
|
|
75
|
+
});
|
|
57
76
|
ctx.ui.notify("Failed to initialise private git repo.", "error");
|
|
58
77
|
return;
|
|
59
78
|
}
|
|
79
|
+
} else {
|
|
80
|
+
tddLog(tddDir, "DEBUG", "tdd:on: git repo already exists");
|
|
60
81
|
}
|
|
61
82
|
|
|
62
83
|
state.enabled = true;
|
|
63
84
|
savePhaseState(root, state);
|
|
85
|
+
tddLog(tddDir, "INFO", "tdd:on: enabled", {
|
|
86
|
+
phase: state.current,
|
|
87
|
+
});
|
|
64
88
|
ctx.ui.notify(`TDD enabled — ${state.current.toUpperCase()} phase`, "info");
|
|
65
89
|
},
|
|
66
90
|
});
|
|
@@ -69,22 +93,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
69
93
|
description: "Disable TDD enforcement",
|
|
70
94
|
handler: async (_args: string, ctx: ExtensionContext) => {
|
|
71
95
|
const root = ctx.cwd;
|
|
96
|
+
const tddDir = join(root, ".pi", "tdd");
|
|
72
97
|
|
|
73
98
|
let state;
|
|
74
99
|
try {
|
|
75
100
|
state = loadPhaseState(root);
|
|
76
|
-
} catch {
|
|
101
|
+
} catch (e) {
|
|
102
|
+
tddLog(tddDir, "WARN", "tdd:off: invalid phase.json", {
|
|
103
|
+
error: (e as Error).message,
|
|
104
|
+
});
|
|
77
105
|
ctx.ui.notify("Invalid .pi/tdd/phase.json. Fix or delete it, then run /tdd:off again.", "error");
|
|
78
106
|
return;
|
|
79
107
|
}
|
|
80
108
|
|
|
81
109
|
if (!state.enabled) {
|
|
110
|
+
tddLog(tddDir, "INFO", "tdd:off: already disabled");
|
|
82
111
|
ctx.ui.notify("TDD already disabled", "info");
|
|
83
112
|
return;
|
|
84
113
|
}
|
|
85
114
|
|
|
86
115
|
state.enabled = false;
|
|
87
116
|
savePhaseState(root, state);
|
|
117
|
+
tddLog(tddDir, "INFO", "tdd:off: disabled", {
|
|
118
|
+
was: state.current,
|
|
119
|
+
});
|
|
88
120
|
ctx.ui.notify("TDD disabled", "info");
|
|
89
121
|
},
|
|
90
122
|
});
|
|
@@ -93,9 +125,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
93
125
|
description: "Show TDD enforcement status",
|
|
94
126
|
handler: async (_args: string, ctx: ExtensionContext) => {
|
|
95
127
|
const root = ctx.cwd;
|
|
128
|
+
const tddDir = join(root, ".pi", "tdd");
|
|
96
129
|
const result = loadTddState(root);
|
|
97
130
|
|
|
98
131
|
if (!result.ok) {
|
|
132
|
+
tddLog(tddDir, "WARN", "tdd:status: TDD not active", {
|
|
133
|
+
reason: result.reason,
|
|
134
|
+
});
|
|
99
135
|
ctx.ui.notify(`TDD: ${result.reason}`, "error");
|
|
100
136
|
return;
|
|
101
137
|
}
|
|
@@ -106,6 +142,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
106
142
|
const greenGlobs = config.allowedGreenPhaseFiles.join(", ") || "(none)";
|
|
107
143
|
const commands = config.testCommands.join(", ") || "(none)";
|
|
108
144
|
|
|
145
|
+
tddLog(tddDir, "INFO", "tdd:status: queried", {
|
|
146
|
+
phase: state.current,
|
|
147
|
+
});
|
|
148
|
+
|
|
109
149
|
ctx.ui.notify(
|
|
110
150
|
`TDD enforcer enabled\n` +
|
|
111
151
|
`Current phase: ${phaseStr}\n` +
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const MAX_LINES = 1000;
|
|
5
|
+
|
|
6
|
+
export function tddLog(
|
|
7
|
+
tddDir: string,
|
|
8
|
+
level: "INFO" | "WARN" | "ERROR" | "DEBUG",
|
|
9
|
+
msg: string,
|
|
10
|
+
data?: Record<string, unknown>,
|
|
11
|
+
): void {
|
|
12
|
+
try {
|
|
13
|
+
const logPath = join(tddDir, "tdd.log");
|
|
14
|
+
const timestamp = new Date().toISOString();
|
|
15
|
+
const dataStr = data !== undefined ? ` ${JSON.stringify(data)}` : "";
|
|
16
|
+
const line = `[${timestamp}] [${level}] ${msg}${dataStr}\n`;
|
|
17
|
+
|
|
18
|
+
appendFileSync(logPath, line, "utf-8");
|
|
19
|
+
|
|
20
|
+
// Trim to last MAX_LINES
|
|
21
|
+
const content = readFileSync(logPath, "utf-8");
|
|
22
|
+
const lines = content.split("\n");
|
|
23
|
+
if (lines.length > MAX_LINES + 1) {
|
|
24
|
+
const trimmed = lines.slice(-MAX_LINES).join("\n") + "\n";
|
|
25
|
+
writeFileSync(logPath, trimmed, "utf-8");
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// Logging never throws — fail silently
|
|
29
|
+
}
|
|
30
|
+
}
|
package/adapters/pi/tools.ts
CHANGED
|
@@ -27,8 +27,10 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
27
27
|
parameters: Type.Object({}),
|
|
28
28
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
29
29
|
const root = ctx.cwd;
|
|
30
|
+
const tddDir = join(root, ".pi", "tdd");
|
|
30
31
|
const tdd = loadTddState(root);
|
|
31
32
|
if (!tdd.ok) {
|
|
33
|
+
tddLog(tddDir, "WARN", "next_tdd_phase: TDD not active", { reason: tdd.reason });
|
|
32
34
|
return { content: [{ type: "text", text: `TDD: ${tdd.reason}` }], details: {} };
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -36,12 +38,19 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
36
38
|
const from = state.current;
|
|
37
39
|
const to = nextPhase(from);
|
|
38
40
|
if (!to) {
|
|
41
|
+
tddLog(tddDir, "WARN", "next_tdd_phase: no next phase", { from });
|
|
39
42
|
return { content: [{ type: "text", text: `No next phase from ${from}.` }], details: {} };
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
tddLog(tddDir, "INFO", "next_tdd_phase: starting", { from, to });
|
|
46
|
+
|
|
42
47
|
// 1. Allowlist check
|
|
43
48
|
const violations = getDisallowedChanges(root, from, config);
|
|
44
49
|
if (violations.length > 0) {
|
|
50
|
+
tddLog(tddDir, "WARN", "next_tdd_phase: blocked by allowlist", {
|
|
51
|
+
from,
|
|
52
|
+
violations,
|
|
53
|
+
});
|
|
45
54
|
return {
|
|
46
55
|
content: [
|
|
47
56
|
{
|
|
@@ -80,16 +89,29 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
80
89
|
};
|
|
81
90
|
|
|
82
91
|
const gate = await checkGate(from, to, testRunner, config);
|
|
92
|
+
tddLog(tddDir, "DEBUG", "next_tdd_phase: gate result", {
|
|
93
|
+
from,
|
|
94
|
+
to,
|
|
95
|
+
passed: gate.passed,
|
|
96
|
+
message: gate.message,
|
|
97
|
+
});
|
|
98
|
+
|
|
83
99
|
if (!gate.passed) {
|
|
84
100
|
return { content: [{ type: "text", text: gate.message }], details: {} };
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
// 3. Snapshot — label with the phase the work was done in
|
|
88
|
-
snapshot(root, from);
|
|
104
|
+
const hash = snapshot(root, from);
|
|
105
|
+
tddLog(tddDir, "INFO", "next_tdd_phase: snapshot created", {
|
|
106
|
+
from,
|
|
107
|
+
to,
|
|
108
|
+
hash,
|
|
109
|
+
});
|
|
89
110
|
|
|
90
111
|
// 4. Save state
|
|
91
112
|
state.current = to;
|
|
92
113
|
savePhaseState(root, state);
|
|
114
|
+
tddLog(tddDir, "INFO", "next_tdd_phase: complete", { from, to });
|
|
93
115
|
|
|
94
116
|
return {
|
|
95
117
|
content: [{ type: "text", text: getNudgePrompt(to, config) }],
|
|
@@ -107,14 +129,21 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
107
129
|
parameters: Type.Object({}),
|
|
108
130
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
109
131
|
const root = ctx.cwd;
|
|
132
|
+
const tddDir = join(root, ".pi", "tdd");
|
|
110
133
|
const tdd = loadTddState(root);
|
|
111
134
|
if (!tdd.ok) {
|
|
135
|
+
tddLog(tddDir, "WARN", "previous_tdd_phase: TDD not active", {
|
|
136
|
+
reason: tdd.reason,
|
|
137
|
+
});
|
|
112
138
|
return { content: [{ type: "text", text: `TDD: ${tdd.reason}` }], details: {} };
|
|
113
139
|
}
|
|
114
140
|
|
|
115
141
|
const { state } = tdd;
|
|
116
142
|
|
|
117
143
|
if (!hasParent(root)) {
|
|
144
|
+
tddLog(tddDir, "WARN", "previous_tdd_phase: no parent commit", {
|
|
145
|
+
phase: state.current,
|
|
146
|
+
});
|
|
118
147
|
return {
|
|
119
148
|
content: [{ type: "text", text: "No previous phase to revert to." }],
|
|
120
149
|
details: {},
|
|
@@ -127,6 +156,9 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
127
156
|
const headMsg = headMessage(root);
|
|
128
157
|
const phaseMatch = headMsg.match(/^tdd: (red|green|refactor)/);
|
|
129
158
|
if (!phaseMatch) {
|
|
159
|
+
tddLog(tddDir, "ERROR", "previous_tdd_phase: invalid HEAD message", {
|
|
160
|
+
headMsg,
|
|
161
|
+
});
|
|
130
162
|
return {
|
|
131
163
|
content: [
|
|
132
164
|
{
|
|
@@ -140,16 +172,26 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
140
172
|
};
|
|
141
173
|
}
|
|
142
174
|
const prevPhase = phaseMatch[1] as Phase;
|
|
175
|
+
tddLog(tddDir, "INFO", "previous_tdd_phase: reverting", {
|
|
176
|
+
from: state.current,
|
|
177
|
+
to: prevPhase,
|
|
178
|
+
headMsg,
|
|
179
|
+
});
|
|
143
180
|
|
|
144
181
|
// 1. Nuke any uncommitted changes, WT matches HEAD
|
|
145
182
|
resetHard(root);
|
|
183
|
+
tddLog(tddDir, "DEBUG", "previous_tdd_phase: resetHard done");
|
|
146
184
|
|
|
147
185
|
// 2. Pop last snapshot commit, keep its content as unstaged
|
|
148
186
|
undoLastCommit(root);
|
|
187
|
+
tddLog(tddDir, "DEBUG", "previous_tdd_phase: undoLastCommit done");
|
|
149
188
|
|
|
150
189
|
// 3. Update phase label from the snapshot's own label
|
|
151
190
|
state.current = prevPhase;
|
|
152
191
|
savePhaseState(root, state);
|
|
192
|
+
tddLog(tddDir, "INFO", "previous_tdd_phase: complete", {
|
|
193
|
+
to: prevPhase,
|
|
194
|
+
});
|
|
153
195
|
|
|
154
196
|
return {
|
|
155
197
|
content: [
|
|
@@ -162,4 +204,56 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
162
204
|
};
|
|
163
205
|
},
|
|
164
206
|
});
|
|
207
|
+
|
|
208
|
+
pi.registerTool({
|
|
209
|
+
name: "tdd_status",
|
|
210
|
+
label: "TDD Status",
|
|
211
|
+
description:
|
|
212
|
+
"Show the current TDD enforcement status: enabled/disabled, current phase, " +
|
|
213
|
+
"allowed file globs, and test commands.",
|
|
214
|
+
parameters: Type.Object({}),
|
|
215
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
216
|
+
const root = ctx.cwd;
|
|
217
|
+
const tddDir = join(root, ".pi", "tdd");
|
|
218
|
+
const result = loadTddState(root);
|
|
219
|
+
|
|
220
|
+
if (!result.ok) {
|
|
221
|
+
tddLog(tddDir, "WARN", "tdd_status: TDD not active", {
|
|
222
|
+
reason: result.reason,
|
|
223
|
+
});
|
|
224
|
+
return { content: [{ type: "text", text: `TDD: ${result.reason}` }], details: {} };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const { state, config } = result;
|
|
228
|
+
const phaseStr = state.current.toUpperCase();
|
|
229
|
+
const redGlobs = config.allowedRedPhaseFiles.join(", ") || "(none)";
|
|
230
|
+
const greenGlobs = config.allowedGreenPhaseFiles.join(", ") || "(none)";
|
|
231
|
+
const commands = config.testCommands.join(", ") || "(none)";
|
|
232
|
+
|
|
233
|
+
tddLog(tddDir, "INFO", "tdd_status: queried", {
|
|
234
|
+
phase: state.current,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
content: [
|
|
239
|
+
{
|
|
240
|
+
type: "text",
|
|
241
|
+
text:
|
|
242
|
+
`TDD enforcer enabled\n` +
|
|
243
|
+
`Current phase: ${phaseStr}\n` +
|
|
244
|
+
`Test files: ${redGlobs}\n` +
|
|
245
|
+
`Impl files: ${greenGlobs}\n` +
|
|
246
|
+
`Test commands: ${commands}`,
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
details: {
|
|
250
|
+
enabled: true,
|
|
251
|
+
phase: state.current,
|
|
252
|
+
allowedRedPhaseFiles: config.allowedRedPhaseFiles,
|
|
253
|
+
allowedGreenPhaseFiles: config.allowedGreenPhaseFiles,
|
|
254
|
+
testCommands: config.testCommands,
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
});
|
|
165
259
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tdd-enforcer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package"
|
|
7
|
+
],
|
|
6
8
|
"dependencies": {
|
|
7
9
|
"picomatch": "^4.0.4"
|
|
8
10
|
},
|
|
@@ -10,6 +12,8 @@
|
|
|
10
12
|
"vitest": "^3"
|
|
11
13
|
},
|
|
12
14
|
"pi": {
|
|
13
|
-
"extensions": [
|
|
15
|
+
"extensions": [
|
|
16
|
+
"./adapters/pi/index.ts"
|
|
17
|
+
]
|
|
14
18
|
}
|
|
15
19
|
}
|