fix: honor workflow guard opt-in for Bash blocks
This commit is contained in:
5
.changeset/quick-rams-wave.md
Normal file
5
.changeset/quick-rams-wave.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 265
|
||||
---
|
||||
Workflow guard now respects its opt-in config before blocking forced git-add commands on worktree-agent branches.
|
||||
@@ -65,6 +65,17 @@ function currentBranch(cwd) {
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
function workflowGuardEnabled(cwd) {
|
||||
const configPath = path.join(cwd, '.planning', 'config.json');
|
||||
if (!fs.existsSync(configPath)) return false;
|
||||
try {
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
return Boolean(config.hooks?.workflow_guard);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let input = '';
|
||||
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
|
||||
process.stdin.setEncoding('utf8');
|
||||
@@ -75,8 +86,12 @@ process.stdin.on('end', () => {
|
||||
const data = JSON.parse(input);
|
||||
const toolName = data.tool_name;
|
||||
const cwd = data.cwd || process.cwd();
|
||||
const isWorkflowGuardEnabled = workflowGuardEnabled(cwd);
|
||||
|
||||
if (toolName === 'Bash') {
|
||||
if (!isWorkflowGuardEnabled) {
|
||||
process.exit(0);
|
||||
}
|
||||
const command = data.tool_input?.command || '';
|
||||
for (const gitCwd of forceGitAddCwds(command, cwd)) {
|
||||
const branch = currentBranch(gitCwd);
|
||||
@@ -92,8 +107,8 @@ process.stdin.on('end', () => {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Only guard Write and Edit tool calls
|
||||
if (toolName !== 'Write' && toolName !== 'Edit') {
|
||||
// Only guard Write, Edit, and MultiEdit tool calls
|
||||
if (!['Write', 'Edit', 'MultiEdit'].includes(toolName)) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -125,19 +140,8 @@ process.stdin.on('end', () => {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Check if workflow guard is enabled
|
||||
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 (!isWorkflowGuardEnabled) {
|
||||
process.exit(0); // Guard disabled (default) or no GSD project
|
||||
}
|
||||
|
||||
// If we get here: GSD project, guard enabled, file edit outside .planning/,
|
||||
|
||||
@@ -26,23 +26,36 @@ function makeRepo(branch) {
|
||||
return dir;
|
||||
}
|
||||
|
||||
function runHook(cwd, command) {
|
||||
function setWorkflowGuard(dir, enabled) {
|
||||
const planningDir = path.join(dir, '.planning');
|
||||
fs.mkdirSync(planningDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(planningDir, 'config.json'),
|
||||
JSON.stringify({ hooks: { workflow_guard: enabled } }, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
function runHookInput(cwd, input) {
|
||||
return spawnSync(process.execPath, [HOOK_PATH], {
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
input: JSON.stringify({
|
||||
cwd,
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command },
|
||||
}),
|
||||
input: JSON.stringify({ cwd, ...input }),
|
||||
});
|
||||
}
|
||||
|
||||
function runBashHook(cwd, command) {
|
||||
return runHookInput(cwd, {
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command },
|
||||
});
|
||||
}
|
||||
|
||||
describe('bug #261: workflow guard blocks forced git add on worktree-agent branches', () => {
|
||||
test('blocks git add -f on worktree-agent branch before it can stage gitignored files', () => {
|
||||
test('blocks git add -f on worktree-agent branch when workflow guard is enabled', () => {
|
||||
const dir = makeRepo('worktree-agent-a1');
|
||||
try {
|
||||
const result = runHook(dir, 'git add -f .planning/phases/01/01-01-SUMMARY.md');
|
||||
setWorkflowGuard(dir, true);
|
||||
const result = runBashHook(dir, 'git add -f .planning/phases/01/01-01-SUMMARY.md');
|
||||
assert.strictEqual(result.status, 2);
|
||||
const envelope = JSON.parse(result.stdout);
|
||||
assert.strictEqual(envelope.decision, 'block');
|
||||
@@ -55,7 +68,8 @@ describe('bug #261: workflow guard blocks forced git add on worktree-agent branc
|
||||
test('blocks git add --force with git global options on worktree-agent branch', () => {
|
||||
const dir = makeRepo('worktree-agent-b2');
|
||||
try {
|
||||
const result = runHook(path.dirname(dir), `git -C "${dir}" add --force .planning/SUMMARY.md`);
|
||||
setWorkflowGuard(dir, true);
|
||||
const result = runBashHook(dir, `git -C "${dir}" add --force .planning/SUMMARY.md`);
|
||||
assert.strictEqual(result.status, 2);
|
||||
assert.strictEqual(JSON.parse(result.stdout).code, 'WORKTREE_AGENT_FORCE_ADD_FORBIDDEN');
|
||||
} finally {
|
||||
@@ -66,7 +80,8 @@ describe('bug #261: workflow guard blocks forced git add on worktree-agent branc
|
||||
test('allows ordinary git add on worktree-agent branch', () => {
|
||||
const dir = makeRepo('worktree-agent-c3');
|
||||
try {
|
||||
const result = runHook(dir, 'git add .planning/SUMMARY.md');
|
||||
setWorkflowGuard(dir, true);
|
||||
const result = runBashHook(dir, 'git add .planning/SUMMARY.md');
|
||||
assert.strictEqual(result.status, 0);
|
||||
assert.strictEqual(result.stdout, '');
|
||||
} finally {
|
||||
@@ -77,11 +92,57 @@ describe('bug #261: workflow guard blocks forced git add on worktree-agent branc
|
||||
test('allows git add -f outside worktree-agent branches', () => {
|
||||
const dir = makeRepo('feature-docs');
|
||||
try {
|
||||
const result = runHook(dir, 'git add -f .planning/SUMMARY.md');
|
||||
setWorkflowGuard(dir, true);
|
||||
const result = runBashHook(dir, 'git add -f .planning/SUMMARY.md');
|
||||
assert.strictEqual(result.status, 0);
|
||||
assert.strictEqual(result.stdout, '');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('allows git add -f on worktree-agent branch when workflow guard is disabled', () => {
|
||||
const dir = makeRepo('worktree-agent-d4');
|
||||
try {
|
||||
setWorkflowGuard(dir, false);
|
||||
const result = runBashHook(dir, 'git add -f .planning/SUMMARY.md');
|
||||
assert.strictEqual(result.status, 0);
|
||||
assert.strictEqual(result.stdout, '');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('allows git add -f on worktree-agent branch when no GSD config exists', () => {
|
||||
const dir = makeRepo('worktree-agent-e5');
|
||||
try {
|
||||
const result = runBashHook(dir, 'git add -f .planning/SUMMARY.md');
|
||||
assert.strictEqual(result.status, 0);
|
||||
assert.strictEqual(result.stdout, '');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('applies the advisory path to MultiEdit when workflow guard is enabled', () => {
|
||||
const dir = makeRepo('feature-multiedit');
|
||||
try {
|
||||
setWorkflowGuard(dir, true);
|
||||
const result = runHookInput(dir, {
|
||||
tool_name: 'MultiEdit',
|
||||
tool_input: {
|
||||
file_path: path.join(dir, 'src.js'),
|
||||
edits: [],
|
||||
},
|
||||
});
|
||||
assert.strictEqual(result.status, 0);
|
||||
const envelope = JSON.parse(result.stdout);
|
||||
assert.match(
|
||||
envelope.hookSpecificOutput.additionalContext,
|
||||
/WORKFLOW ADVISORY/
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user