Files
AGENTS/.pi/gsd/hooks/gsd-workflow-guard.js

99 lines
3.3 KiB
JavaScript
Raw Permalink Normal View History

2026-04-24 20:00:33 +02:00
#!/usr/bin/env node
// gsd-hook-version: 1.30.0
// SHARED CANONICAL FILE - hardlinked into all harness hooks/ directories.
// Harness config dir is auto-detected from __dirname at runtime.
// Do NOT hardcode harness-specific paths here. See HOOKS_ARCHITECTURE.md.
// GSD Workflow Guard - PreToolUse hook
// Detects when Claude attempts file edits outside a GSD workflow context
// (no active /gsd- command or Task subagent) and injects an advisory warning.
//
// This is a SOFT guard - it advises, not blocks. The edit still proceeds.
// The warning nudges the agent to use /gsd-quick or /gsd-fast instead of
// making direct edits that bypass state tracking.
//
// Enable via config: hooks.workflow_guard: true (default: false)
// Only triggers on Write/Edit tool calls to non-.planning/ files.
const fs = require("fs");
const path = require("path");
let input = "";
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => (input += chunk));
process.stdin.on("end", () => {
clearTimeout(stdinTimeout);
try {
const data = JSON.parse(input);
const toolName = data.tool_name;
// Only guard Write and Edit tool calls
if (toolName !== "Write" && toolName !== "Edit") {
process.exit(0);
}
// Check if we're inside a GSD workflow (Task subagent or /gsd- command)
// Subagents have a session_id that differs from the parent
// and typically have a description field set by the orchestrator
if (data.tool_input?.is_subagent || data.session_type === "task") {
process.exit(0);
}
// Check the file being edited
const filePath = data.tool_input?.file_path || data.tool_input?.path || "";
// Allow edits to .planning/ files (GSD state management)
if (filePath.includes(".planning/") || filePath.includes(".planning\\")) {
process.exit(0);
}
// Allow edits to common config/docs files that don't need GSD tracking
const allowedPatterns = [
/\.gitignore$/,
/\.env/,
/CLAUDE\.md$/,
/AGENTS\.md$/,
/GEMINI\.md$/,
/settings\.json$/,
];
if (allowedPatterns.some((p) => p.test(filePath))) {
process.exit(0);
}
// Check if workflow guard is enabled
const cwd = data.cwd || process.cwd();
const configPath = path.join(cwd, ".planning", "config.json");
if (fs.existsSync(configPath)) {
try {
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
if (!config.hooks?.workflow_guard) {
process.exit(0); // Guard disabled (default)
}
} catch (e) {
process.exit(0);
}
} else {
process.exit(0); // No GSD project - don't guard
}
// If we get here: GSD project, guard enabled, file edit outside .planning/,
// not in a subagent context. Inject advisory warning.
const output = {
hookSpecificOutput: {
hookEventName: "PreToolUse",
additionalContext:
`⚠️ WORKFLOW ADVISORY: You're editing ${path.basename(filePath)} directly without a GSD command. ` +
"This edit will not be tracked in STATE.md or produce a SUMMARY.md. " +
"Consider using /gsd-fast for trivial fixes or /gsd-quick for larger changes " +
"to maintain project state tracking. " +
"If this is intentional (e.g., user explicitly asked for a direct edit), proceed normally.",
},
};
process.stdout.write(JSON.stringify(output));
} catch (e) {
// Silent fail - never block tool execution
process.exit(0);
}
});