78bae4addf
Per Pravila §15.1 — инжектит cwd/branch/HEAD/worktree-root + правила поведения в каждый Task-prompt. FAIL-OPEN на любой ошибке (git не в PATH, malformed stdin, non-Task tools). Все 5 тестов из subagent-prompt-prefix.test.mjs PASS. Регистрация в .claude/settings.json — Task 6 плана. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
110 lines
4.4 KiB
JavaScript
110 lines
4.4 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* PreToolUse hook — git-safety header inject for Task tool.
|
||
*
|
||
* For каждый Task-инвокейшн контроллера: дописывает в начало tool_input.prompt
|
||
* заголовок с cwd / branch / parent SHA / worktree-root + правилами поведения
|
||
* (rule 1–5). Это компенсирует класс инцидентов «субагент путает ветку/worktree»
|
||
* (Sprint 6, Pravila §15.1).
|
||
*
|
||
* FAIL-OPEN: любая ошибка / тайм-аут / git-не-в-PATH → {continue: true} без
|
||
* модификации; хук НИКОГДА не блокирует Task.
|
||
*
|
||
* Non-Task tools — pass-through без модификации.
|
||
*
|
||
* Spec: docs/superpowers/specs/2026-05-18-parallel-sessions-coordination-design.md §4
|
||
* Rule: docs/Pravila_raboty_Claude_v1_1.md §15.1
|
||
*/
|
||
import { execFile } from 'node:child_process';
|
||
import { promisify } from 'node:util';
|
||
|
||
const execFileP = promisify(execFile);
|
||
const GIT_TIMEOUT_MS = 1500;
|
||
|
||
/** Read all stdin into a string. */
|
||
async function readStdin() {
|
||
let buf = '';
|
||
for await (const chunk of process.stdin) buf += chunk;
|
||
return buf;
|
||
}
|
||
|
||
/** Parse hook input JSON; return null on any parse error. */
|
||
function parseHookInput(raw) {
|
||
try { return JSON.parse(raw); } catch { return null; }
|
||
}
|
||
|
||
/** Run a single fixed-args git command; return trimmed stdout, or null on any error. */
|
||
async function gitCmd(args) {
|
||
try {
|
||
const { stdout } = await execFileP('git', args, { timeout: GIT_TIMEOUT_MS, encoding: 'utf8' });
|
||
return stdout.trim();
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/** Build the safety header block. Returns null if any of the 4 git values can't be resolved. */
|
||
async function buildHeader() {
|
||
const cwd = process.cwd();
|
||
const [branch, head, top] = await Promise.all([
|
||
gitCmd(['branch', '--show-current']),
|
||
gitCmd(['rev-parse', 'HEAD']),
|
||
gitCmd(['rev-parse', '--show-toplevel']),
|
||
]);
|
||
if (!branch || !head || !top) return null;
|
||
return [
|
||
'=== SUBAGENT GIT-SAFETY HEADER (Pravila §15.1, auto-injected) ===',
|
||
`Working directory (cwd): ${cwd}`,
|
||
`Branch (git branch --show-current): ${branch}`,
|
||
`Parent commit (git rev-parse HEAD): ${head}`,
|
||
`Worktree root (git rev-parse --show-toplevel): ${top}`,
|
||
'',
|
||
'ОБЯЗАТЕЛЬНЫЕ ПРАВИЛА:',
|
||
'1. Все git-операции выполняй ТОЛЬКО внутри cwd выше. Если pwd ≠ cwd — STOP, верни ошибку.',
|
||
'2. ЗАПРЕЩЕНО: `git checkout <branch>`, `git switch <branch>`, `git checkout -b`, `git branch -m`, `git worktree add/remove`, `git push --force`, `git reset --hard`.',
|
||
'3. После КАЖДОГО `git commit` — выполни `git rev-parse HEAD` и `git branch --show-current`, выпиши результат в ответ. Это обязательная часть отчёта.',
|
||
'4. Если обнаружил, что находишься не в той ветке/worktree — STOP, не правь ничего, верни ошибку с raw output `git status` и `git branch --show-current`.',
|
||
'5. Если задача не требует git-операций (только Edit/Read/Grep) — этот блок информационный, follow rule 1.',
|
||
'',
|
||
'=== END SUBAGENT GIT-SAFETY HEADER ===',
|
||
'',
|
||
].join('\n');
|
||
}
|
||
|
||
/** Emit fail-open response and exit 0. */
|
||
function failOpen() {
|
||
process.stdout.write(JSON.stringify({ continue: true }));
|
||
process.exit(0);
|
||
}
|
||
|
||
async function main() {
|
||
const raw = await readStdin();
|
||
const input = parseHookInput(raw);
|
||
if (!input) return failOpen();
|
||
|
||
// Non-Task — pass-through
|
||
if (input.tool_name !== 'Task') return failOpen();
|
||
|
||
const originalPrompt = input.tool_input?.prompt;
|
||
if (typeof originalPrompt !== 'string') return failOpen();
|
||
|
||
const header = await buildHeader();
|
||
if (!header) {
|
||
// git unavailable / not in worktree — fail-open per spec §4.5 edge-case 3
|
||
process.stderr.write('[subagent-prompt-prefix] git resolution failed — passing through\n');
|
||
return failOpen();
|
||
}
|
||
|
||
const newPrompt = header + originalPrompt;
|
||
process.stdout.write(JSON.stringify({
|
||
hookSpecificOutput: {
|
||
hookEventName: 'PreToolUse',
|
||
permissionDecision: 'allow',
|
||
permissionDecisionReason: 'git-safety header injected (Pravila §15.1)',
|
||
updatedInput: { prompt: newPrompt },
|
||
},
|
||
}));
|
||
}
|
||
|
||
main().catch(() => failOpen());
|