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.
@@ -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, restoreFiles } from "../../engine/git.js";
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
- const allowed = isAllowed(filePath, phase, config);
48
- tddLog(tddDir, "DEBUG", "tool_call: check", { toolName, filePath, phase, allowed });
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
- filePath,
55
+ relPath,
54
56
  phase,
55
57
  });
56
58
  return {
57
59
  block: true,
58
- reason: `TDD ${phase.toUpperCase()}: "${filePath}" is locked in this phase.`,
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, filePath, phase });
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, "INFO", "tool_result: reverting violations", {
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⚠️ TDD: Bash modified files locked in ${phase.toUpperCase()} phase. ` +
115
- `Reverted: ${violations.join(", ")}`,
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
  };
@@ -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
- const escaped = files.map((f) => `"${f}"`).join(" ");
67
- gitExec(`restore -- ${escaped}`, projectRoot, { stdio: "pipe" as const });
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tdd-enforcer",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "keywords": [
6
6
  "pi-package"