397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
82 lines
2.7 KiB
JavaScript
82 lines
2.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Rule #5 — Memory write requires memory-sync coverage.
|
|
*
|
|
* PreToolUse hook on Edit / Write / MultiEdit. If the file_path looks like a
|
|
* memory store .md (memory/*.md or MEMORY.md), require the last assistant
|
|
* message to declare `coverage: direct:memory-sync` OR `coverage: skill:*` for
|
|
* a memory-related skill. Otherwise block with a re-announce instruction.
|
|
*
|
|
* Override phrase: `memory dump` in user's last prompt suppresses this rule.
|
|
*
|
|
* Spec: docs/superpowers/specs/2026-05-25-enforce-hard-rules-design.md
|
|
*/
|
|
|
|
import {
|
|
readStdin,
|
|
parseEventJson,
|
|
readTranscript,
|
|
lastUserPromptText,
|
|
lastAssistantText,
|
|
parseCoverageLine,
|
|
findOverride,
|
|
logOverride,
|
|
exitDecision,
|
|
isMemoryPath,
|
|
} from './enforce-hook-helpers.mjs';
|
|
|
|
const RULE_KEY = 'memory-sync-coverage';
|
|
|
|
function isMemorySyncCoverage(cov) {
|
|
if (!cov) return false;
|
|
if (cov.channel === 'direct' && /memory-sync/i.test(cov.id)) return true;
|
|
if (cov.channel === 'skill' && /memory/i.test(cov.id)) return true;
|
|
return false;
|
|
}
|
|
|
|
export function decide({ toolName, filePath, transcriptEntries, override }) {
|
|
if (!['Edit', 'Write', 'MultiEdit'].includes(toolName)) {
|
|
return { block: false };
|
|
}
|
|
if (!isMemoryPath(filePath)) return { block: false };
|
|
if (override) return { block: false };
|
|
|
|
const assistantText = lastAssistantText(transcriptEntries);
|
|
const cov = parseCoverageLine(assistantText);
|
|
if (isMemorySyncCoverage(cov)) return { block: false };
|
|
|
|
return {
|
|
block: true,
|
|
message: [
|
|
`[enforce-memory-coverage] Write to memory path requires memory-sync coverage tag.`,
|
|
`Detected coverage: ${cov ? cov.channel + ':' + cov.id : 'NONE'} (stale or absent).`,
|
|
``,
|
|
`Re-announce on a fresh assistant turn first:`,
|
|
` coverage: direct:memory-sync`,
|
|
`Then retry the Edit/Write.`,
|
|
].join('\n'),
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
try {
|
|
const raw = await readStdin();
|
|
const event = parseEventJson(raw);
|
|
const toolName = event.tool_name || '';
|
|
const filePath = (event.tool_input && (event.tool_input.file_path || event.tool_input.notebook_path)) || '';
|
|
const transcript = readTranscript(event.transcript_path);
|
|
const userPrompt = lastUserPromptText(transcript);
|
|
const override = findOverride(userPrompt, RULE_KEY);
|
|
if (override) logOverride(RULE_KEY, override, event.session_id);
|
|
|
|
const result = decide({ toolName, filePath, transcriptEntries: transcript, override });
|
|
exitDecision(result);
|
|
} catch {
|
|
// Fail-quiet on any internal error.
|
|
exitDecision({ block: false });
|
|
}
|
|
}
|
|
|
|
const isCli = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/enforce-memory-coverage.mjs');
|
|
if (isCli) main();
|