tdd-enforcer 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/adapters/pi/hooks.ts +13 -13
- package/adapters/pi/index.ts +7 -1
- package/engine/git.ts +24 -3
- package/package.json +1 -1
package/adapters/pi/hooks.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
1
|
+
import { join, relative } from "node:path";
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { isToolCallEventType, isBashToolResult } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { isAllowed } from "../../engine/enforce.js";
|
|
5
|
-
import { changesSinceSnapshot
|
|
5
|
+
import { changesSinceSnapshot } from "../../engine/git.js";
|
|
6
6
|
import { loadTddState } from "./helpers.js";
|
|
7
7
|
import { tddLog } from "./log.js";
|
|
8
8
|
|
|
@@ -44,22 +44,24 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
// Patterns in rules.json are relative to repo root; convert absolute path
|
|
48
|
+
const relPath = relative(root, filePath);
|
|
49
|
+
const allowed = isAllowed(relPath, phase, config);
|
|
50
|
+
tddLog(tddDir, "DEBUG", "tool_call: check", { toolName, relPath, phase, allowed });
|
|
49
51
|
|
|
50
52
|
if (!allowed) {
|
|
51
53
|
tddLog(tddDir, "INFO", "tool_call: blocked file modification", {
|
|
52
54
|
toolName,
|
|
53
|
-
|
|
55
|
+
relPath,
|
|
54
56
|
phase,
|
|
55
57
|
});
|
|
56
58
|
return {
|
|
57
59
|
block: true,
|
|
58
|
-
reason: `TDD ${phase.toUpperCase()}: "${
|
|
60
|
+
reason: `TDD ${phase.toUpperCase()}: "${relPath}" is locked in this phase.`,
|
|
59
61
|
};
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
tddLog(tddDir, "DEBUG", "tool_call: allowed", { toolName,
|
|
64
|
+
tddLog(tddDir, "DEBUG", "tool_call: allowed", { toolName, relPath, phase });
|
|
63
65
|
});
|
|
64
66
|
|
|
65
67
|
pi.on("tool_result", async (event, ctx: ExtensionContext) => {
|
|
@@ -96,14 +98,11 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|
|
96
98
|
return;
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
tddLog(tddDir, "
|
|
101
|
+
tddLog(tddDir, "WARN", "tool_result: locked files modified by bash", {
|
|
100
102
|
phase,
|
|
101
103
|
violations,
|
|
102
|
-
allChanged: changed,
|
|
103
104
|
});
|
|
104
105
|
|
|
105
|
-
restoreFiles(root, violations);
|
|
106
|
-
|
|
107
106
|
const existingText = event.content.map((c) => ("text" in c ? c.text : "")).join("");
|
|
108
107
|
return {
|
|
109
108
|
content: [
|
|
@@ -111,8 +110,9 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|
|
111
110
|
type: "text",
|
|
112
111
|
text:
|
|
113
112
|
existingText +
|
|
114
|
-
`\n\n⚠️
|
|
115
|
-
`
|
|
113
|
+
`\n\n⚠️ ${phase.toUpperCase()}: bash modified locked files: ${violations.join(", ")}\n` +
|
|
114
|
+
`next_tdd_phase blocked until reverted. ` +
|
|
115
|
+
`Inspect with: cd .pi/tdd && git diff HEAD -- ${violations[0]}`,
|
|
116
116
|
},
|
|
117
117
|
],
|
|
118
118
|
};
|
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 } from "../../engine/index.js";
|
|
4
|
+
import { loadPhaseState, loadConfig, savePhaseState, initGit, 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";
|
|
@@ -80,6 +80,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
80
80
|
tddLog(tddDir, "DEBUG", "tdd:on: git repo already exists");
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// Snapshot working tree so stale baseline doesn't nuke user changes
|
|
84
|
+
snapshot(root, state.current);
|
|
85
|
+
tddLog(tddDir, "INFO", "tdd:on: snapshot taken", {
|
|
86
|
+
phase: state.current,
|
|
87
|
+
});
|
|
88
|
+
|
|
83
89
|
state.enabled = true;
|
|
84
90
|
savePhaseState(root, state);
|
|
85
91
|
tddLog(tddDir, "INFO", "tdd:on: enabled", {
|
package/engine/git.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync, type ExecSyncOptions } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
5
|
const TDD_DIR = ".pi/tdd";
|
|
@@ -63,8 +63,29 @@ export function changesSinceSnapshot(projectRoot: string): string[] {
|
|
|
63
63
|
|
|
64
64
|
export function restoreFiles(projectRoot: string, files: string[]): void {
|
|
65
65
|
if (files.length === 0) return;
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
|
|
67
|
+
// Separate tracked (git restore) from untracked (delete)
|
|
68
|
+
const tracked = gitExec("ls-files", projectRoot)
|
|
69
|
+
.trim()
|
|
70
|
+
.split("\n")
|
|
71
|
+
.filter(Boolean);
|
|
72
|
+
const trackedSet = new Set(tracked);
|
|
73
|
+
|
|
74
|
+
const trackedFiles = files.filter((f) => trackedSet.has(f));
|
|
75
|
+
const untrackedFiles = files.filter((f) => !trackedSet.has(f));
|
|
76
|
+
|
|
77
|
+
if (trackedFiles.length > 0) {
|
|
78
|
+
const escaped = trackedFiles.map((f) => `"${f}"`).join(" ");
|
|
79
|
+
gitExec(`restore -- ${escaped}`, projectRoot, { stdio: "pipe" as const });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const f of untrackedFiles) {
|
|
83
|
+
try {
|
|
84
|
+
unlinkSync(f);
|
|
85
|
+
} catch {
|
|
86
|
+
// File may already be gone, ignore
|
|
87
|
+
}
|
|
88
|
+
}
|
|
68
89
|
}
|
|
69
90
|
|
|
70
91
|
export function headHash(projectRoot: string): string {
|