Files
portal/tools/subagent-prompt-prefix.mjs
T
Дмитрий 78bae4addf feat(hooks): subagent-prompt-prefix — PreToolUse git-safety inject (TDD green)
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>
2026-05-18 10:17:04 +03:00

110 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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());