Files
portal/tools/enforce-askuser-answer-parser.mjs
T
Дмитрий c14fb72e84 feat(router-gate-v4): Stream H Task 6 — askuser-answer-parser wrapper + toApprovalRecord schema sync
Closes Stream H Task 6 (H4). Retires the manual approval-write workaround
the controller used throughout Stream H Tasks 1-5.

Two changes:

1. Pure module tools/askuser-answer-parser.mjs gains toApprovalRecord(answer, opts)
   exporter that detects a git verb in the user's free-form answer and returns
   a Stream B-compatible {type:'approve_git_operation', command, ts} record
   (matches loadApprovedGitOps reader format in shell-content-rules.mjs:125).
   Returns null for non-git answers and for stop/abort/cancel keywords.

2. New PostToolUse(AskUserQuestion) wrapper tools/enforce-askuser-answer-parser.mjs
   reads each question/answer pair, calls toApprovalRecord, appends matching
   records to ~/.claude/runtime/askuser-decisions-<sess>.jsonl. Fail-open
   observability — never blocks AskUserQuestion.

Regression: vitest tools 1742/1742 GREEN (was 1731; +5 toApprovalRecord tests
under "toApprovalRecord (Stream H Task 6 — schema sync)" including non-string
guard, +6 wrapper-hook tests under "enforce-askuser-answer-parser wrapper
(Stream H Task 6)" including missing session_id fail-open guard).

DEFERRED: settings.json registration (matcher "AskUserQuestion", PostToolUse,
fail-open, timeout 2000ms) — batched with H5/H6/H7/H8 hook activations at end
of Phase H-α/H-β. Hook code is fully implemented and unit-tested; activation
pending settings.json update.

Stream H Task 6 of 11. Plan: docs/superpowers/plans/2026-05-30-router-gate-v4-stream-H.md
2026-05-30 11:28:13 +03:00

71 lines
2.6 KiB
JavaScript

#!/usr/bin/env node
/**
* PostToolUse(AskUserQuestion) wrapper — schema bridge between Stream E
* pure parser (askuser-answer-parser.mjs::toApprovalRecord) and Stream B
* approval reader (shell-content-rules.mjs::loadApprovedGitOps).
*
* For each question/answer pair: if the answer matches a git pattern,
* append an approve_git_operation record to
* ~/.claude/runtime/askuser-decisions-<sess>.jsonl.
*
* Fail-open observability (never blocks AskUserQuestion).
*
* Stream H Task 6 — retires the manual approval-write workaround used by
* the controller throughout Stream H Tasks 1-5.
*/
import { appendFileSync, mkdirSync } from 'node:fs';
import { homedir } from 'node:os';
import { join, dirname } from 'node:path';
import { toApprovalRecord } from './askuser-answer-parser.mjs';
/**
* Pure event processor for test-injection of runtimeDir + nowMs.
*
* @param {object} event - PostToolUse payload {session_id, tool_input, tool_response}
* @param {object} [opts]
* @param {string} [opts.runtimeDir] - override default ~/.claude/runtime
* @param {number} [opts.nowMs] - override timestamp for test determinism
*/
export function processEvent(event, { runtimeDir, nowMs } = {}) {
try {
const sessionId = event && event.session_id;
const toolInput = event && event.tool_input;
const toolResponse = event && event.tool_response;
if (!sessionId || !toolInput || !toolResponse) return;
const questions = toolInput.questions || [];
const answers = toolResponse.answers || {};
const dir = runtimeDir || join(homedir(), '.claude', 'runtime');
const path = join(dir, `askuser-decisions-${sessionId}.jsonl`);
let wroteAny = false;
for (const q of questions) {
if (!q || !q.question) continue;
const ans = answers[q.question];
if (!ans) continue;
const rec = toApprovalRecord(ans, { question: q.question, nowMs });
if (!rec) continue;
if (!wroteAny) {
try { mkdirSync(dirname(path), { recursive: true }); } catch { /* ignore */ }
wroteAny = true;
}
try { appendFileSync(path, JSON.stringify(rec) + '\n'); } catch { /* fail-open */ }
}
} catch {
// fail-open observability — never throw from PostToolUse handler
}
}
async function main() {
let input = '';
for await (const chunk of process.stdin) input += chunk;
let payload;
try { payload = JSON.parse(input); } catch { return; }
processEvent(payload);
}
if (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}` || (process.argv[1] || '').endsWith('enforce-askuser-answer-parser.mjs')) {
main().catch(() => process.exit(0)); // fail-open observability
}