@tianhai/pi-workflow-kit 0.4.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/LICENSE +22 -0
- package/README.md +509 -0
- package/ROADMAP.md +16 -0
- package/agents/code-reviewer.md +18 -0
- package/agents/config.ts +5 -0
- package/agents/implementer.md +26 -0
- package/agents/spec-reviewer.md +13 -0
- package/agents/worker.md +17 -0
- package/banner.jpg +0 -0
- package/docs/developer-usage-guide.md +463 -0
- package/docs/oversight-model.md +49 -0
- package/docs/workflow-phases.md +71 -0
- package/extensions/constants.ts +9 -0
- package/extensions/lib/logging.ts +138 -0
- package/extensions/plan-tracker.ts +496 -0
- package/extensions/subagent/agents.ts +144 -0
- package/extensions/subagent/concurrency.ts +52 -0
- package/extensions/subagent/env.ts +47 -0
- package/extensions/subagent/index.ts +1116 -0
- package/extensions/subagent/lifecycle.ts +25 -0
- package/extensions/subagent/timeout.ts +13 -0
- package/extensions/workflow-monitor/debug-monitor.ts +98 -0
- package/extensions/workflow-monitor/git.ts +31 -0
- package/extensions/workflow-monitor/heuristics.ts +58 -0
- package/extensions/workflow-monitor/investigation.ts +52 -0
- package/extensions/workflow-monitor/reference-tool.ts +42 -0
- package/extensions/workflow-monitor/skip-confirmation.ts +19 -0
- package/extensions/workflow-monitor/tdd-monitor.ts +137 -0
- package/extensions/workflow-monitor/test-runner.ts +37 -0
- package/extensions/workflow-monitor/verification-monitor.ts +61 -0
- package/extensions/workflow-monitor/warnings.ts +81 -0
- package/extensions/workflow-monitor/workflow-handler.ts +358 -0
- package/extensions/workflow-monitor/workflow-tracker.ts +231 -0
- package/extensions/workflow-monitor/workflow-transitions.ts +55 -0
- package/extensions/workflow-monitor.ts +885 -0
- package/package.json +49 -0
- package/skills/brainstorming/SKILL.md +70 -0
- package/skills/dispatching-parallel-agents/SKILL.md +194 -0
- package/skills/executing-tasks/SKILL.md +247 -0
- package/skills/receiving-code-review/SKILL.md +196 -0
- package/skills/systematic-debugging/SKILL.md +170 -0
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/skills/systematic-debugging/find-polluter.sh +63 -0
- package/skills/systematic-debugging/reference/rationalizations.md +61 -0
- package/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/skills/test-driven-development/SKILL.md +266 -0
- package/skills/test-driven-development/reference/examples.md +101 -0
- package/skills/test-driven-development/reference/rationalizations.md +67 -0
- package/skills/test-driven-development/reference/when-stuck.md +33 -0
- package/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/skills/using-git-worktrees/SKILL.md +231 -0
- package/skills/writing-plans/SKILL.md +149 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export class ProcessTracker {
|
|
4
|
+
private processes = new Set<ChildProcess>();
|
|
5
|
+
|
|
6
|
+
get size(): number {
|
|
7
|
+
return this.processes.size;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
add(proc: ChildProcess): void {
|
|
11
|
+
this.processes.add(proc);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
remove(proc: ChildProcess): void {
|
|
15
|
+
this.processes.delete(proc);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
killAll(): void {
|
|
19
|
+
for (const proc of this.processes) {
|
|
20
|
+
if (!proc.killed) {
|
|
21
|
+
proc.kill("SIGTERM");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const DEFAULT_SUBAGENT_TIMEOUT_MS = 600_000; // 10 minutes
|
|
2
|
+
|
|
3
|
+
export function getSubagentTimeoutMs(agentTimeout?: number): number {
|
|
4
|
+
if (agentTimeout !== undefined && agentTimeout > 0) return agentTimeout;
|
|
5
|
+
|
|
6
|
+
const envVal = process.env.PI_SUBAGENT_TIMEOUT_MS;
|
|
7
|
+
if (envVal) {
|
|
8
|
+
const parsed = Number.parseInt(envVal, 10);
|
|
9
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return DEFAULT_SUBAGENT_TIMEOUT_MS;
|
|
13
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { isSourceFile } from "./heuristics";
|
|
2
|
+
|
|
3
|
+
const EXCESSIVE_FIX_THRESHOLD = 3;
|
|
4
|
+
|
|
5
|
+
export type DebugViolationType = "fix-without-investigation" | "excessive-fix-attempts";
|
|
6
|
+
|
|
7
|
+
export interface DebugViolation {
|
|
8
|
+
type: DebugViolationType;
|
|
9
|
+
file: string;
|
|
10
|
+
fixAttempts: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class DebugMonitor {
|
|
14
|
+
private active = false;
|
|
15
|
+
private investigated = false;
|
|
16
|
+
private fixAttempts_ = 0;
|
|
17
|
+
private sourceWrittenSinceLastTest = false;
|
|
18
|
+
|
|
19
|
+
getState(): { active: boolean; investigated: boolean; fixAttempts: number } {
|
|
20
|
+
return {
|
|
21
|
+
active: this.active,
|
|
22
|
+
investigated: this.investigated,
|
|
23
|
+
fixAttempts: this.fixAttempts_,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
setState(state: { active: boolean; investigated: boolean; fixAttempts: number }): void {
|
|
28
|
+
this.active = state.active;
|
|
29
|
+
this.investigated = state.investigated;
|
|
30
|
+
this.fixAttempts_ = state.fixAttempts;
|
|
31
|
+
this.sourceWrittenSinceLastTest = false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
isActive(): boolean {
|
|
35
|
+
return this.active;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
hasInvestigated(): boolean {
|
|
39
|
+
return this.investigated;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getFixAttempts(): number {
|
|
43
|
+
return this.fixAttempts_;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
onTestFailed(): void {
|
|
47
|
+
if (this.active && this.sourceWrittenSinceLastTest) {
|
|
48
|
+
this.fixAttempts_++;
|
|
49
|
+
}
|
|
50
|
+
this.active = true;
|
|
51
|
+
this.investigated = false;
|
|
52
|
+
this.sourceWrittenSinceLastTest = false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onTestPassed(): void {
|
|
56
|
+
this.reset();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
onInvestigation(): void {
|
|
60
|
+
this.investigated = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
onSourceWritten(path: string): DebugViolation | null {
|
|
64
|
+
if (!this.active) return null;
|
|
65
|
+
if (!isSourceFile(path)) return null;
|
|
66
|
+
|
|
67
|
+
this.sourceWrittenSinceLastTest = true;
|
|
68
|
+
|
|
69
|
+
if (this.fixAttempts_ >= EXCESSIVE_FIX_THRESHOLD) {
|
|
70
|
+
return {
|
|
71
|
+
type: "excessive-fix-attempts",
|
|
72
|
+
file: path,
|
|
73
|
+
fixAttempts: this.fixAttempts_,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!this.investigated) {
|
|
78
|
+
return {
|
|
79
|
+
type: "fix-without-investigation",
|
|
80
|
+
file: path,
|
|
81
|
+
fixAttempts: this.fixAttempts_,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
onCommit(): void {
|
|
89
|
+
this.reset();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private reset(): void {
|
|
93
|
+
this.active = false;
|
|
94
|
+
this.investigated = false;
|
|
95
|
+
this.fixAttempts_ = 0;
|
|
96
|
+
this.sourceWrittenSinceLastTest = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { log } from "../lib/logging.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns the current git branch name, or (if detached) the short HEAD SHA.
|
|
6
|
+
* Returns null if the current working directory is not in a git repo.
|
|
7
|
+
*/
|
|
8
|
+
export function getCurrentGitRef(cwd: string = process.cwd()): string | null {
|
|
9
|
+
try {
|
|
10
|
+
const branch = execSync("git branch --show-current", {
|
|
11
|
+
cwd,
|
|
12
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
13
|
+
})
|
|
14
|
+
.toString()
|
|
15
|
+
.trim();
|
|
16
|
+
|
|
17
|
+
if (branch) return branch;
|
|
18
|
+
|
|
19
|
+
const sha = execSync("git rev-parse --short HEAD", {
|
|
20
|
+
cwd,
|
|
21
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
22
|
+
})
|
|
23
|
+
.toString()
|
|
24
|
+
.trim();
|
|
25
|
+
|
|
26
|
+
return sha || null;
|
|
27
|
+
} catch (err) {
|
|
28
|
+
log.warn(`Failed to determine git ref in ${cwd}: ${err instanceof Error ? err.message : err}`);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
const TEST_PATTERNS = [
|
|
4
|
+
/\.(test|spec)\.(ts|js|tsx|jsx|py|rs|go|java|rb|swift|kt)$/,
|
|
5
|
+
/(^|\/)tests?\//,
|
|
6
|
+
/\/__tests__\//,
|
|
7
|
+
/^test_\w+\.py$/,
|
|
8
|
+
/\/test_\w+\.py$/,
|
|
9
|
+
/\w+_test\.py$/,
|
|
10
|
+
/\w+_test\.go$/,
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const SOURCE_EXTENSIONS = /\.(ts|js|tsx|jsx|py|rs|go|java|rb|swift|kt)$/;
|
|
14
|
+
|
|
15
|
+
const CONFIG_PATTERNS = [
|
|
16
|
+
/\.config\.(ts|js|mjs|cjs)$/,
|
|
17
|
+
/^\./, // dotfiles
|
|
18
|
+
/package\.json$/,
|
|
19
|
+
/tsconfig.*\.json$/,
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export function isTestFile(path: string): boolean {
|
|
23
|
+
return TEST_PATTERNS.some((p) => p.test(path));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isSourceFile(path: string): boolean {
|
|
27
|
+
if (!SOURCE_EXTENSIONS.test(path)) return false;
|
|
28
|
+
if (isTestFile(path)) return false;
|
|
29
|
+
if (CONFIG_PATTERNS.some((p) => p.test(path))) return false;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function findCorrespondingTestFile(filePath: string): string[] {
|
|
34
|
+
const parsed = path.posix.parse(filePath);
|
|
35
|
+
const baseDir = parsed.dir;
|
|
36
|
+
const stem = parsed.name;
|
|
37
|
+
const ext = parsed.ext;
|
|
38
|
+
|
|
39
|
+
if (!stem || !ext) return [];
|
|
40
|
+
|
|
41
|
+
const candidates = [
|
|
42
|
+
path.posix.join(baseDir, `${stem}.test${ext}`),
|
|
43
|
+
path.posix.join(baseDir, `${stem}.spec${ext}`),
|
|
44
|
+
path.posix.join(baseDir, "__tests__", `${stem}.test${ext}`),
|
|
45
|
+
path.posix.join(baseDir, "__tests__", `${stem}.spec${ext}`),
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
if (filePath.startsWith("src/")) {
|
|
49
|
+
const relFromSrc = filePath.slice("src/".length);
|
|
50
|
+
const relParsed = path.posix.parse(relFromSrc);
|
|
51
|
+
candidates.push(
|
|
52
|
+
path.posix.join("tests", relParsed.dir, `${relParsed.name}.test${relParsed.ext}`),
|
|
53
|
+
path.posix.join("tests", relParsed.dir, `${relParsed.name}.spec${relParsed.ext}`),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return candidates;
|
|
58
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const INVESTIGATION_PATTERNS = [
|
|
2
|
+
/\bgrep\b/,
|
|
3
|
+
/\brg\b/,
|
|
4
|
+
/\bag\b/,
|
|
5
|
+
/\bgit\s+(log|diff|show|blame)\b/,
|
|
6
|
+
/\bfind\b/,
|
|
7
|
+
/\bls\b/,
|
|
8
|
+
/\bcat\b/,
|
|
9
|
+
/\bhead\b/,
|
|
10
|
+
/\btail\b/,
|
|
11
|
+
/\bless\b/,
|
|
12
|
+
/\bwc\b/,
|
|
13
|
+
/\becho\b/,
|
|
14
|
+
/\bprintf\b/,
|
|
15
|
+
/\benv\b/,
|
|
16
|
+
/\bprintenv\b/,
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export function isInvestigationCommand(command: string): boolean {
|
|
20
|
+
return INVESTIGATION_PATTERNS.some((p) => p.test(command));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const INVESTIGATION_TOOL_NAMES = new Set([
|
|
24
|
+
"kota_search",
|
|
25
|
+
"kota_deps",
|
|
26
|
+
"kota_usages",
|
|
27
|
+
"kota_impact",
|
|
28
|
+
"kota_task_context",
|
|
29
|
+
"web_search",
|
|
30
|
+
"fetch_content",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const INVESTIGATION_LSP_ACTIONS = new Set([
|
|
34
|
+
"definition",
|
|
35
|
+
"references",
|
|
36
|
+
"hover",
|
|
37
|
+
"symbols",
|
|
38
|
+
"diagnostics",
|
|
39
|
+
"workspace-diagnostics",
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
export function isInvestigationToolCall(toolName: string, params?: Record<string, unknown>): boolean {
|
|
43
|
+
if (INVESTIGATION_TOOL_NAMES.has(toolName)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (toolName === "lsp" && params?.action && INVESTIGATION_LSP_ACTIONS.has(params.action as string)) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { log } from "../lib/logging.js";
|
|
5
|
+
|
|
6
|
+
// extensions/workflow-monitor/reference-tool.ts is 2 levels below package root
|
|
7
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
8
|
+
|
|
9
|
+
const TOPIC_MAP: Record<string, string> = {
|
|
10
|
+
"tdd-rationalizations": "skills/test-driven-development/reference/rationalizations.md",
|
|
11
|
+
"tdd-examples": "skills/test-driven-development/reference/examples.md",
|
|
12
|
+
"tdd-when-stuck": "skills/test-driven-development/reference/when-stuck.md",
|
|
13
|
+
"tdd-anti-patterns": "skills/test-driven-development/testing-anti-patterns.md",
|
|
14
|
+
"debug-rationalizations": "skills/systematic-debugging/reference/rationalizations.md",
|
|
15
|
+
"debug-tracing": "skills/systematic-debugging/root-cause-tracing.md",
|
|
16
|
+
"debug-defense-in-depth": "skills/systematic-debugging/defense-in-depth.md",
|
|
17
|
+
"debug-condition-waiting": "skills/systematic-debugging/condition-based-waiting.md",
|
|
18
|
+
"brainstorming-guide": "skills/brainstorming/SKILL.md",
|
|
19
|
+
"writing-plans-guide": "skills/writing-plans/SKILL.md",
|
|
20
|
+
"executing-tasks-guide": "skills/executing-tasks/SKILL.md",
|
|
21
|
+
"dispatching-agents-guide": "skills/dispatching-parallel-agents/SKILL.md",
|
|
22
|
+
"receiving-review-guide": "skills/receiving-code-review/SKILL.md",
|
|
23
|
+
"worktree-guide": "skills/using-git-worktrees/SKILL.md",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const REFERENCE_TOPICS = Object.keys(TOPIC_MAP);
|
|
27
|
+
|
|
28
|
+
export async function loadReference(topic: string): Promise<string> {
|
|
29
|
+
const relativePath = TOPIC_MAP[topic];
|
|
30
|
+
if (!relativePath) {
|
|
31
|
+
return `Unknown topic: "${topic}". Available topics: ${REFERENCE_TOPICS.join(", ")}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const fullPath = resolve(PACKAGE_ROOT, relativePath);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
return await readFile(fullPath, "utf-8");
|
|
38
|
+
} catch (err) {
|
|
39
|
+
log.warn(`Failed to load reference "${topic}" from ${fullPath}: ${err instanceof Error ? err.message : err}`);
|
|
40
|
+
return `Error loading reference "${topic}": file not found at ${fullPath}`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Phase, type PhaseStatus, WORKFLOW_PHASES, type WorkflowTrackerState } from "./workflow-tracker";
|
|
2
|
+
|
|
3
|
+
export function isPhaseUnresolved(status: PhaseStatus): boolean {
|
|
4
|
+
return status === "pending";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getUnresolvedPhasesBefore(target: Phase, state: WorkflowTrackerState): Phase[] {
|
|
8
|
+
const targetIndex = WORKFLOW_PHASES.indexOf(target);
|
|
9
|
+
if (targetIndex === -1) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const phasesBefore = WORKFLOW_PHASES.slice(0, targetIndex);
|
|
14
|
+
return getUnresolvedPhases(phasesBefore, state);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getUnresolvedPhases(phases: Phase[], state: WorkflowTrackerState): Phase[] {
|
|
18
|
+
return phases.filter((phase) => isPhaseUnresolved(state.phases[phase]));
|
|
19
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { findCorrespondingTestFile, isSourceFile, isTestFile } from "./heuristics";
|
|
3
|
+
|
|
4
|
+
export type TddPhase = "idle" | "red-pending" | "red" | "green" | "refactor";
|
|
5
|
+
|
|
6
|
+
export type TddViolationType = "source-before-test" | "source-during-red" | "existing-tests-not-run-before-change";
|
|
7
|
+
|
|
8
|
+
export interface TddViolation {
|
|
9
|
+
type: TddViolationType;
|
|
10
|
+
file: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class TddMonitor {
|
|
14
|
+
private phase: TddPhase = "idle";
|
|
15
|
+
private testFilesWritten = new Set<string>();
|
|
16
|
+
private sourceFilesWritten = new Set<string>();
|
|
17
|
+
private redVerificationPending = false;
|
|
18
|
+
private nonCodeMode = false;
|
|
19
|
+
private testsRunBeforeLastWrite = false;
|
|
20
|
+
private fileExists: (path: string) => boolean;
|
|
21
|
+
|
|
22
|
+
constructor(fileExists?: (path: string) => boolean) {
|
|
23
|
+
this.fileExists = fileExists ?? ((filePath) => fs.existsSync(filePath));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getPhase(): TddPhase {
|
|
27
|
+
return this.phase;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
isRedVerificationPending(): boolean {
|
|
31
|
+
return this.phase === "red-pending" && this.redVerificationPending;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setNonCodeMode(value: boolean): void {
|
|
35
|
+
this.nonCodeMode = value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onFileWritten(path: string): TddViolation | null {
|
|
39
|
+
if (this.nonCodeMode) return null;
|
|
40
|
+
|
|
41
|
+
if (isTestFile(path)) {
|
|
42
|
+
this.testFilesWritten.add(path);
|
|
43
|
+
this.phase = "red-pending";
|
|
44
|
+
this.redVerificationPending = true;
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (isSourceFile(path)) {
|
|
49
|
+
this.sourceFilesWritten.add(path);
|
|
50
|
+
|
|
51
|
+
const wasTestsRun = this.testsRunBeforeLastWrite;
|
|
52
|
+
this.testsRunBeforeLastWrite = false;
|
|
53
|
+
|
|
54
|
+
if (this.testFilesWritten.size === 0) {
|
|
55
|
+
const existingTestFile = findCorrespondingTestFile(path).some((candidatePath) =>
|
|
56
|
+
this.fileExists(candidatePath),
|
|
57
|
+
);
|
|
58
|
+
if (!existingTestFile) {
|
|
59
|
+
return { type: "source-before-test", file: path };
|
|
60
|
+
}
|
|
61
|
+
// Existing test coverage detected — Scenario 2 check
|
|
62
|
+
if (!wasTestsRun) {
|
|
63
|
+
return { type: "existing-tests-not-run-before-change", file: path };
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (this.phase === "red-pending") {
|
|
69
|
+
return { type: "source-during-red", file: path };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (this.phase === "green") {
|
|
73
|
+
this.phase = "refactor";
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
onTestResult(passed: boolean): void {
|
|
82
|
+
this.testsRunBeforeLastWrite = true;
|
|
83
|
+
|
|
84
|
+
if (this.phase === "red-pending") {
|
|
85
|
+
this.redVerificationPending = false;
|
|
86
|
+
if (passed) {
|
|
87
|
+
this.phase = "green";
|
|
88
|
+
} else {
|
|
89
|
+
this.phase = "red";
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (passed && (this.phase === "red" || this.phase === "refactor")) {
|
|
95
|
+
this.phase = "green";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
onCommit(): void {
|
|
100
|
+
this.phase = "idle";
|
|
101
|
+
this.redVerificationPending = false;
|
|
102
|
+
this.testFilesWritten.clear();
|
|
103
|
+
this.sourceFilesWritten.clear();
|
|
104
|
+
this.testsRunBeforeLastWrite = false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
setState(
|
|
108
|
+
phase: TddPhase,
|
|
109
|
+
testFiles: string[],
|
|
110
|
+
sourceFiles: string[],
|
|
111
|
+
redVerificationPending = false,
|
|
112
|
+
nonCodeMode = false,
|
|
113
|
+
): void {
|
|
114
|
+
this.phase = phase;
|
|
115
|
+
this.testFilesWritten = new Set(testFiles);
|
|
116
|
+
this.sourceFilesWritten = new Set(sourceFiles);
|
|
117
|
+
this.redVerificationPending = redVerificationPending;
|
|
118
|
+
this.nonCodeMode = nonCodeMode;
|
|
119
|
+
this.testsRunBeforeLastWrite = false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getState(): {
|
|
123
|
+
phase: TddPhase;
|
|
124
|
+
testFiles: string[];
|
|
125
|
+
sourceFiles: string[];
|
|
126
|
+
redVerificationPending: boolean;
|
|
127
|
+
nonCodeMode: boolean;
|
|
128
|
+
} {
|
|
129
|
+
return {
|
|
130
|
+
phase: this.phase,
|
|
131
|
+
testFiles: [...this.testFilesWritten],
|
|
132
|
+
sourceFiles: [...this.sourceFilesWritten],
|
|
133
|
+
redVerificationPending: this.redVerificationPending,
|
|
134
|
+
nonCodeMode: this.nonCodeMode,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const TEST_COMMANDS = [
|
|
2
|
+
/\bnpm\s+test\b/,
|
|
3
|
+
/\bnpx\s+(vitest|jest)\b/,
|
|
4
|
+
/\bpytest\b/,
|
|
5
|
+
/\bgo\s+test\b/,
|
|
6
|
+
/\bcargo\s+test\b/,
|
|
7
|
+
/\bjest\b/,
|
|
8
|
+
/\bvitest\b/,
|
|
9
|
+
/\bmocha\b/,
|
|
10
|
+
/\brspec\b/,
|
|
11
|
+
/\bphpunit\b/,
|
|
12
|
+
/\bdotnet\s+test\b/,
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const PASS_PATTERNS = [
|
|
16
|
+
/\d+\s+(tests?\s+)?passed/i,
|
|
17
|
+
/^ok\s+/m,
|
|
18
|
+
/Tests:\s+\d+ passed/,
|
|
19
|
+
/\d+ passing/,
|
|
20
|
+
/BUILD SUCCESSFUL/,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const FAIL_PATTERNS = [/\bfailed\b/i, /^FAIL\b/m, /\d+ failing/, /BUILD FAILED/, /ERRORS!/];
|
|
24
|
+
|
|
25
|
+
export function parseTestCommand(command: string): boolean {
|
|
26
|
+
return TEST_COMMANDS.some((p) => p.test(command));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function parseTestResult(output: string, exitCode: number | undefined): boolean | null {
|
|
30
|
+
const hasFail = FAIL_PATTERNS.some((p) => p.test(output));
|
|
31
|
+
const hasPass = PASS_PATTERNS.some((p) => p.test(output));
|
|
32
|
+
|
|
33
|
+
if (hasFail && !hasPass) return false;
|
|
34
|
+
if (hasPass && !hasFail) return true;
|
|
35
|
+
if (exitCode !== undefined) return exitCode === 0;
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface VerificationViolation {
|
|
2
|
+
type: "commit-without-verification" | "push-without-verification" | "pr-without-verification";
|
|
3
|
+
command: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const COMMIT_RE = /\bgit\s+commit\b/;
|
|
7
|
+
const PUSH_RE = /\bgit\s+push\b/;
|
|
8
|
+
const PR_RE = /\bgh\s+pr\s+create\b/;
|
|
9
|
+
|
|
10
|
+
export class VerificationMonitor {
|
|
11
|
+
private verified = false;
|
|
12
|
+
private verificationWaived = false;
|
|
13
|
+
|
|
14
|
+
getState(): { verified: boolean; verificationWaived: boolean } {
|
|
15
|
+
return {
|
|
16
|
+
verified: this.verified,
|
|
17
|
+
verificationWaived: this.verificationWaived,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
setState(state: { verified: boolean; verificationWaived: boolean }): void {
|
|
22
|
+
this.verified = state.verified;
|
|
23
|
+
this.verificationWaived = state.verificationWaived;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
recordVerification(): void {
|
|
27
|
+
this.verified = true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
recordVerificationWaiver(): void {
|
|
31
|
+
this.verificationWaived = true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
onSourceWritten(): void {
|
|
35
|
+
this.verified = false;
|
|
36
|
+
this.verificationWaived = false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
hasRecentVerification(): boolean {
|
|
40
|
+
return this.verified;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
checkCommitGate(command: string): VerificationViolation | null {
|
|
44
|
+
const allowed = this.verified || this.verificationWaived;
|
|
45
|
+
if (COMMIT_RE.test(command)) {
|
|
46
|
+
return allowed ? null : { type: "commit-without-verification", command };
|
|
47
|
+
}
|
|
48
|
+
if (PUSH_RE.test(command)) {
|
|
49
|
+
return allowed ? null : { type: "push-without-verification", command };
|
|
50
|
+
}
|
|
51
|
+
if (PR_RE.test(command)) {
|
|
52
|
+
return allowed ? null : { type: "pr-without-verification", command };
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
reset(): void {
|
|
58
|
+
this.verified = false;
|
|
59
|
+
this.verificationWaived = false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export type TddViolationType = "source-before-test" | "source-during-red" | "existing-tests-not-run-before-change";
|
|
2
|
+
|
|
3
|
+
export function getTddViolationWarning(type: TddViolationType, file: string, _phase?: string): string {
|
|
4
|
+
if (type === "source-before-test") {
|
|
5
|
+
return `⚠️ TDD: Writing source code (${file}) without a failing test. Consider whether this change needs a test first, or if existing tests already cover it.`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (type === "source-during-red") {
|
|
9
|
+
return `⚠️ TDD: Writing source code (${file}) before running your new test. Run the test suite to verify your test fails, then implement.`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (type === "existing-tests-not-run-before-change") {
|
|
13
|
+
return `⚠️ TDD SCENARIO 2: You modified ${file} without running existing tests first.\nRun the tests, observe their current state, THEN make your change.`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return `⚠️ TDD: Unexpected violation type "${type}" for ${file}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type DebugViolationType = "fix-without-investigation" | "excessive-fix-attempts";
|
|
20
|
+
|
|
21
|
+
export type VerificationViolationType =
|
|
22
|
+
| "commit-without-verification"
|
|
23
|
+
| "push-without-verification"
|
|
24
|
+
| "pr-without-verification";
|
|
25
|
+
|
|
26
|
+
export function getDebugViolationWarning(type: DebugViolationType, file: string, fixAttempts: number): string {
|
|
27
|
+
if (type === "fix-without-investigation") {
|
|
28
|
+
return `
|
|
29
|
+
⚠️ DEBUG VIOLATION: You edited production code (${file}) without investigating first.
|
|
30
|
+
|
|
31
|
+
The Iron Law: NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST.
|
|
32
|
+
|
|
33
|
+
Before editing code, you must:
|
|
34
|
+
1. Read the error messages and stack traces carefully
|
|
35
|
+
2. Read the relevant source files to understand the code
|
|
36
|
+
3. Trace the data flow to find where the bad value originates
|
|
37
|
+
|
|
38
|
+
You're treating symptoms, not causes. Symptom fixes create new bugs.
|
|
39
|
+
|
|
40
|
+
Stop. Read. Understand. Then fix.
|
|
41
|
+
`.trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (type === "excessive-fix-attempts") {
|
|
45
|
+
return `
|
|
46
|
+
⚠️ DEBUG WARNING: ${fixAttempts} failed fix attempts on ${file}.
|
|
47
|
+
|
|
48
|
+
${fixAttempts} fix attempts haven't resolved the issue. Consider stepping back to investigate root cause.
|
|
49
|
+
|
|
50
|
+
Pattern indicating architectural problem:
|
|
51
|
+
- Each fix reveals new problems in different places
|
|
52
|
+
- Fixes require "massive refactoring" to implement
|
|
53
|
+
- Each fix creates new symptoms elsewhere
|
|
54
|
+
|
|
55
|
+
STOP and question fundamentals:
|
|
56
|
+
- Is this pattern fundamentally sound?
|
|
57
|
+
- Are we sticking with it through sheer inertia?
|
|
58
|
+
- Should we refactor architecture vs. continue fixing symptoms?
|
|
59
|
+
|
|
60
|
+
Discuss with your human partner before attempting more fixes.
|
|
61
|
+
`.trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return `⚠️ DEBUG WARNING: Unexpected violation type "${type}" for ${file}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getVerificationViolationWarning(type: VerificationViolationType, command: string): string {
|
|
68
|
+
const action =
|
|
69
|
+
type === "commit-without-verification" ? "commit" : type === "push-without-verification" ? "push" : "create a PR";
|
|
70
|
+
|
|
71
|
+
return `
|
|
72
|
+
⚠️ VERIFICATION REQUIRED: You're about to ${action} without running verification.
|
|
73
|
+
|
|
74
|
+
Command: ${command}
|
|
75
|
+
|
|
76
|
+
Run the test/build/lint command FIRST. Read the output. Confirm it passes.
|
|
77
|
+
THEN ${action}.
|
|
78
|
+
|
|
79
|
+
Evidence before claims. No shortcuts.
|
|
80
|
+
`.trim();
|
|
81
|
+
}
|