mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
* feat(cli): interactive hook consent + unified .impeccable/config.json Make the design-hook install a conscious choice and unify scattered config into one file. Interactive consent - On an interactive `skills install`/`update`, the CLI explains what the hook does and offers to install it (default yes), then records the per-developer decision in the gitignored `.impeccable/config.local.json`, so it never re-asks. A recorded decision or an already-installed hook short-circuits; `-y`/non-TTY keeps the historical install-by-default behavior; `--no-hooks` is a one-off skip that records nothing. The trigger keys on "is the hook installed?" + "is there a recorded decision?", not a brittle version check. Unified config - `.impeccable/config.json` (shared) and `.impeccable/config.local.json` (gitignored) now hold all Impeccable settings: hook settings under a `hook` key, plus top-level `updateCheck`. `/impeccable hooks` writes the `hook` subtree, preserving siblings. The hook runtime reads `hook.quiet` and `hook.auditLog`; context boot reads `updateCheck`. The legacy `IMPECCABLE_HOOK_DISABLED|QUIET|LOG` and `IMPECCABLE_NO_UPDATE_CHECK` env vars still work and override config; docs now lead with config and treat env vars as a legacy note. - No backward compat for the pre-unification `hook.json`/`hook.local.json` (the hook shipped an hour ago; nothing in the wild uses it). This repo's own hook config is migrated to `.impeccable/config.json`. The CLI and skill scripts are separate trees, so a small CLI-side config module (cli/lib/impeccable-config.mjs) duplicates the config-path and .git/info/exclude handling; comments flag the duplication. Tests: new cli config unit test; skills-cli consent tests (declined skips, accepted installs, --no-hooks records nothing); hook.test.mjs back-compat removed and quiet/auditLog-from-config + gitexclude coverage added. Full suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hooks): preserve sibling config fields + resolve audit log from event cwd (Bugbot) Two Bugbot findings: - High: `/impeccable hooks` edits replaced the whole `hook` object with the merge-helper output, dropping fields those helpers don't manage — so an `ignore-value --local` could wipe the recorded install consent and make the CLI re-prompt. writeConfig now merges over the existing hook object, keeping consent/quiet/auditLog. - Medium: config-based audit logging resolved hook.auditLog from process.cwd(), which can differ from the hook event's project root (and Cursor's pre-edit hook passed no cwd). The hook now stamps the resolved project root on the audit entry, and writeAuditLog reads config from entry.cwd when present. Tests: a /impeccable hooks edit preserves consent + quiet; writeAuditLog resolves config auditLog from entry.cwd, not the fallback cwd. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hooks): resolve a relative auditLog path against the project root (Bugbot) A relative hook.auditLog was read from the project root but written relative to the hook process cwd, so when those differ the log went to the wrong place. writeAuditLog now resolves a relative target (from env or config) against the same project root it reads config from. Absolute and ~/ paths are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix hook consent recovery and smoke config * Fix hook consent explainer for Cursor * Fix empty hook target consent --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62 lines
2.0 KiB
JavaScript
62 lines
2.0 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Impeccable design hook — PostToolUse entry point.
|
|
*
|
|
* Reads the Claude Code / Codex / Cursor hook event from stdin, runs the design
|
|
* detector against the touched file, and emits a system reminder via
|
|
* `hookSpecificOutput.additionalContext` when findings exist.
|
|
*
|
|
* Contract: never break a turn. Always exit 0. Clean files emit a small ack
|
|
* unless quiet mode is enabled.
|
|
*
|
|
* Most logic lives in `hook-lib.mjs` so it is unit-testable without a
|
|
* subprocess. This file is the thin stdin/stdout adapter.
|
|
*/
|
|
|
|
import { runHook, writeAuditLog } from './hook-lib.mjs';
|
|
|
|
async function readStdin() {
|
|
if (process.stdin.isTTY) return '';
|
|
const chunks = [];
|
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
return Buffer.concat(chunks).toString('utf-8');
|
|
}
|
|
|
|
async function main() {
|
|
// Snapshot the inherited env FIRST so the re-entrancy guard checks the
|
|
// parent's value, not the value we are about to export for any child
|
|
// processes the hook might ever spawn.
|
|
const inheritedEnv = { ...process.env };
|
|
process.env.IMPECCABLE_HOOK_DEPTH = process.env.IMPECCABLE_HOOK_DEPTH || '1';
|
|
|
|
let stdinJson = '';
|
|
try { stdinJson = await readStdin(); } catch { /* fall through */ }
|
|
|
|
const result = await runHook({
|
|
stdinJson,
|
|
env: inheritedEnv,
|
|
cwd: process.cwd(),
|
|
});
|
|
|
|
writeAuditLog(process.env, result.audit, process.cwd());
|
|
|
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
process.exit(result.exitCode || 0);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
// Last-ditch: never break the agent's turn even if something we did not
|
|
// anticipate goes wrong. Audit-log the failure if logging is enabled.
|
|
try {
|
|
writeAuditLog(process.env, {
|
|
ts: new Date().toISOString(),
|
|
event: 'PostToolUse',
|
|
error: String(err && err.message ? err.message : err),
|
|
});
|
|
} catch { /* swallow */ }
|
|
if (process.env.IMPECCABLE_HOOK_DEBUG) {
|
|
process.stderr.write(`[impeccable-hook] ${err}\n`);
|
|
}
|
|
process.exit(0);
|
|
});
|