@webpieces/ai-hook-rules 0.4.559 → 0.4.561
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +2 -2
- package/src/adapters/claude-code-response.d.ts +1 -1
- package/src/adapters/claude-code-response.js +9 -1
- package/src/adapters/claude-code-response.js.map +1 -1
- package/src/adapters/hook-core.js +19 -9
- package/src/adapters/hook-core.js.map +1 -1
- package/src/bin/shim-audit-log.d.ts +73 -0
- package/src/bin/shim-audit-log.js +144 -0
- package/src/bin/shim-audit-log.js.map +1 -0
- package/src/bin/shim.d.ts +1 -0
- package/src/bin/shim.js +45 -18
- package/src/bin/shim.js.map +1 -1
- package/src/core/decision-log.d.ts +50 -9
- package/src/core/decision-log.js +118 -34
- package/src/core/decision-log.js.map +1 -1
- package/src/core/main-sync-log.d.ts +1 -1
- package/src/core/main-sync-log.js +9 -9
- package/src/core/main-sync-log.js.map +1 -1
- package/src/core/main-sync-refresh.d.ts +1 -1
- package/src/core/main-sync-refresh.js +2 -2
- package/src/core/main-sync-refresh.js.map +1 -1
- package/src/core/rejection-log.d.ts +7 -0
- package/src/core/rejection-log.js +21 -6
- package/src/core/rejection-log.js.map +1 -1
- package/templates/ai-hook.sh +69 -16
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decision-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/decision-log.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nimport { dotWebpieces, readMainSyncStatus, MainSyncStatus, RepoRootFinder } from '@webpieces/rules-config';\n\nimport { toError } from './to-error';\n\n// The SYNC decision log — what the synchronous hook DID on each invocation and WHY. Its companion is\n// the ASYNC log (guard-async-work.log, written by the detached refresher in main-sync-log.ts). This\n// one records EVERY guard decision — allow, block, config-bypass, and the fail-open cases — and CITES\n// the async-written cache snapshot (`cache` field) that drove the decision, so a wrong allow/block is\n// traceable to a stale or missing async write. Writes to `.webpieces/hooks/guard-sync-decisions.log`.\nconst HOOKS_DIR = 'hooks';\nconst LOG_FILE = 'guard-sync-decisions.log';\nconst LOG_FILE_PREV = 'guard-sync-decisions.1.log';\n// The per-INVOCATION stream (companion to the per-DECISION log above): one line for every guards-hook\n// call, so cleanup automation can mine tool + branch + sync-status over time. See logGuardInvocation.\nconst INVOCATION_LOG_FILE = 'guard-invocations.log';\nconst INVOCATION_LOG_FILE_PREV = 'guard-invocations.1.log';\nconst MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded (mirrors rejection-log)\nconst MAX_TARGET_LEN = 160;\n\nexport type Verdict = 'ALLOW' | 'BLOCK';\n\n// Data-only record of one guard decision (per CLAUDE.md: classes for data, not object literals).\n// `cache` summarizes the async-written main-sync-status.json that drove a feature-branch-guard\n// decision (branch/merged/conflict/fork + the cache timestamp), or '-' when no cache was consulted\n// (bash guards, on-main, config-bypass).\nexport class GuardDecision {\n rule: string;\n tool: string;\n target: string; // file path (file guards) or the bash command (bash guards)\n branch: string;\n verdict: Verdict;\n reason: string;\n cache: string;\n\n constructor(rule: string, tool: string, target: string, branch: string, verdict: Verdict, reason: string, cache: string = '-') {\n this.rule = rule;\n this.tool = tool;\n this.target = target;\n this.branch = branch;\n this.verdict = verdict;\n this.reason = reason;\n this.cache = cache;\n }\n}\n\n/**\n * Append one tab-separated line per decision to `.webpieces/hooks/guard-sync-decisions.log`. `root` is\n * the repo/workspace root that holds `.webpieces` (callers pass workspaceRoot, or a\n * RepoRootFinder-resolved root at the pre-load config-bypass site — never a raw cwd, so a bypass\n * logged from a subdir never scatters a stray `.webpieces`). Swallows all errors — logging must never\n * block or fail a hook.\n */\nexport function logGuardDecision(root: string, decision: GuardDecision): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const timestamp = new Date().toISOString();\n // LOCAL scope: a guard decision belongs to the tree it judged, and a per-worktree log has one\n // writer, so appends cannot interleave with another agent's.\n const hooksDir = dotWebpieces.localFile(root, HOOKS_DIR);\n fs.mkdirSync(hooksDir, { recursive: true });\n\n const logPath = path.join(hooksDir, LOG_FILE);\n rotateLogFile(logPath, path.join(hooksDir, LOG_FILE_PREV));\n\n const line = [\n `[${timestamp}]`,\n decision.verdict,\n decision.tool,\n oneLine(decision.target),\n decision.branch,\n decision.rule,\n oneLine(decision.reason),\n oneLine(decision.cache),\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n}\n\n/**\n * Append one line per GUARDS-hook invocation to `.webpieces/hooks/guard-invocations.log` — the raw\n * \"what did the guard see\" stream, written on EVERY call (allow or block, bash or file), unlike\n * guard-sync-decisions.log which only records actual decisions. Captures the tool, the command/file,\n * the live git branch, and the async-written main-sync-status.json (branch / merged / fork-point /\n * conflict) so cleanup automation can later mine it — e.g. \"on an already-merged branch, a\n * `git checkout main` should also delete the branch\". `cwd` is the AI's working dir; the repo root\n * that owns `.webpieces` is resolved from it. Swallows all errors — logging never blocks a hook.\n */\nexport function logGuardInvocation(cwd: string, tool: string, target: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n const branch = branchForLog(root);\n // The cache is branch-keyed, so the entry to log is the one for the branch we are standing on.\n // 'unknown' (branchForLog's failure value) simply misses and logs 'sync=none'.\n const sync = summarizeSyncStatus(readMainSyncStatus(root, branch));\n\n const hooksDir = dotWebpieces.localFile(root, HOOKS_DIR);\n fs.mkdirSync(hooksDir, { recursive: true });\n const logPath = path.join(hooksDir, INVOCATION_LOG_FILE);\n rotateLogFile(logPath, path.join(hooksDir, INVOCATION_LOG_FILE_PREV));\n\n const line = [`[${new Date().toISOString()}]`, tool, oneLine(target), `branch=${branch}`, sync].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n}\n\n// One-field summary of main-sync-status.json for the invocation log: the branch the cache is FOR,\n// whether it is already merged (and its PR), fork-point presence, and conflict state — the signals a\n// cleanup step keys off. 'sync=none' when the cache has not been written yet (first call of a session).\nfunction summarizeSyncStatus(status: MainSyncStatus | null): string {\n if (status === null) return 'sync=none';\n const merged = status.branchAlreadyMerged ? `PR#${status.mergedPr !== '' ? status.mergedPr : '?'}` : 'no';\n return `sync=${status.branch} merged=${merged} fork=${String(status.hasForkPoint)} conflict=${String(status.conflict)} ts=${status.timestamp}`;\n}\n\n// Best-effort current branch for the log line. Returns 'unknown' on any failure (e.g. not a git\n// repo) — this is for display only, never for a control decision.\nexport function branchForLog(root: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: root,\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim() || 'unknown';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return 'unknown';\n }\n}\n\n// Collapse newlines/tabs and cap length so one decision is always one log line.\nfunction oneLine(value: string): string {\n const flat = value.replace(/[\\t\\r\\n]+/g, ' ').trim();\n return flat.length <= MAX_TARGET_LEN ? flat : flat.slice(0, MAX_TARGET_LEN) + '…';\n}\n\nfunction rotateLogFile(logPath: string, prevPath: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const stat = fs.statSync(logPath);\n if (stat.size > MAX_LOG_BYTES) {\n if (fs.existsSync(prevPath)) fs.unlinkSync(prevPath);\n fs.renameSync(logPath, prevPath);\n }\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"decision-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/decision-log.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nimport { dotWebpieces, readMainSyncStatus, MainSyncStatus, RepoRootFinder, claudeEnv } from '@webpieces/rules-config';\n\nimport { toError } from './to-error';\n\n// The SYNC decision log — what the synchronous hook DID on each invocation and WHY. Its companion is\n// the ASYNC log (guard-async-work.log, written by the detached refresher in main-sync-log.ts). This\n// one records EVERY guard decision — allow, block, config-bypass, and the fail-open cases — and CITES\n// the async-written cache snapshot (`cache` field) that drove the decision, so a wrong allow/block is\n// traceable to a stale or missing async write. Writes to `.webpieces/logs/guard-sync-decisions.log`\n// (see LOGS_STATE_DIR: every webpieces log lives under `logs/`, never beside `hooks/`'s non-log state).\nconst LOG_FILE = 'guard-sync-decisions.log';\nconst LOG_FILE_PREV = 'guard-sync-decisions.1.log';\n// The per-INVOCATION stream (companion to the per-DECISION log above): one line for every guards-hook\n// call, so cleanup automation can mine tool + branch + sync-status + OUTCOME over time. See InvocationLog.\nconst INVOCATION_LOG_FILE = 'guard-invocations.log';\nconst INVOCATION_LOG_FILE_PREV = 'guard-invocations.1.log';\nconst MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded (mirrors rejection-log)\nconst MAX_TARGET_LEN = 160;\n\nexport type Verdict = 'ALLOW' | 'BLOCK';\n\n// Data-only record of one guard decision (per CLAUDE.md: classes for data, not object literals).\n// `cache` summarizes the async-written main-sync-status.json that drove a feature-branch-guard\n// decision (branch/merged/conflict/fork + the cache timestamp), or '-' when no cache was consulted\n// (bash guards, on-main, config-bypass).\nexport class GuardDecision {\n rule: string;\n tool: string;\n target: string; // file path (file guards) or the bash command (bash guards)\n branch: string;\n verdict: Verdict;\n reason: string;\n cache: string;\n\n constructor(rule: string, tool: string, target: string, branch: string, verdict: Verdict, reason: string, cache: string = '-') {\n this.rule = rule;\n this.tool = tool;\n this.target = target;\n this.branch = branch;\n this.verdict = verdict;\n this.reason = reason;\n this.cache = cache;\n }\n}\n\n/**\n * Append one tab-separated line per decision to `.webpieces/logs/guard-sync-decisions.log`. `root` is\n * the repo/workspace root that holds `.webpieces` (callers pass workspaceRoot, or a\n * RepoRootFinder-resolved root at the pre-load config-bypass site — never a raw cwd, so a bypass\n * logged from a subdir never scatters a stray `.webpieces`). Swallows all errors — logging must never\n * block or fail a hook.\n */\nexport function logGuardDecision(root: string, decision: GuardDecision): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const timestamp = new Date().toISOString();\n // LOCAL scope: a guard decision belongs to the tree it judged, and a per-worktree log has one\n // writer, so appends cannot interleave with another agent's.\n const logsDir = dotWebpieces.logs(root);\n fs.mkdirSync(logsDir, { recursive: true });\n\n const logPath = path.join(logsDir, LOG_FILE);\n rotateLogFile(logPath, path.join(logsDir, LOG_FILE_PREV));\n\n const line = [\n `[${timestamp}]`,\n decision.verdict,\n decision.tool,\n oneLine(decision.target),\n decision.branch,\n decision.rule,\n oneLine(decision.reason),\n oneLine(decision.cache),\n // The tree this decision was actually made against, and what Claude Code told the hook the\n // project was. Appended (never reordered) for the same reason as on the invocation line —\n // see ClaudeEnv: when these two disagree, that disagreement is the bug.\n `root=${root}`,\n `projectDir=${claudeEnv.projectDirForLog()}`,\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n}\n\n/**\n * What the guard SAW on one invocation, captured up front and held until the outcome is known.\n * Data-only (per CLAUDE.md: classes for data, explicit construction).\n */\nexport class GuardInvocation {\n constructor(\n public readonly root: string,\n public readonly timestamp: string,\n public readonly tool: string,\n public readonly target: string,\n public readonly branch: string,\n public readonly sync: string,\n public readonly projectDir: string,\n ) {}\n}\n\n/**\n * The per-INVOCATION stream — `.webpieces/logs/guard-invocations.log`, one line for EVERY guards-hook\n * call (allow or block, bash or file), unlike guard-sync-decisions.log which records only the calls a\n * rule actually judged. It captures the tool, the command/file, the live git branch, the async-written\n * main-sync-status.json snapshot (branch / merged / fork-point / conflict), and — since this class\n * replaced a bare log-and-forget function — HOW THE CALL ENDED.\n *\n * WHY IT IS TWO CALLS. The line used to be written the moment the hook started, so it could not carry\n * a verdict: the decision had not been made yet. Answering \"what happened to this call?\" therefore\n * meant joining this file against guard-sync-decisions.log BY TIMESTAMP, which is exactly the kind of\n * reconstruction a log exists to make unnecessary. So {@link begin} now only CAPTURES (including the\n * git/cache reads, which must still happen while the hook is running), and {@link finish} — called\n * from the hook's single terminal boundary, emitAllow/emitDeny — writes the whole line once the\n * outcome is known. The two streams stay distinct in purpose: this one is \"every call and how it\n * ended\", the decision log remains \"every judgement and why\".\n *\n * Every error is swallowed: logging must never block or fail a hook.\n */\nexport class InvocationLog {\n private pending: GuardInvocation | null = null;\n\n /**\n * Capture the context of one invocation. `cwd` is the AI's working dir; the repo root that owns\n * `.webpieces` is resolved from it. Writes NOTHING — {@link finish} does that.\n */\n begin(cwd: string, tool: string, target: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n const branch = branchForLog(root);\n // The cache is branch-keyed, so the entry to log is the one for the branch we are standing\n // on. 'unknown' (branchForLog's failure value) simply misses and logs 'sync=none'.\n const sync = summarizeSyncStatus(readMainSyncStatus(root, branch));\n this.pending = new GuardInvocation(root, new Date().toISOString(), tool, oneLine(target), branch, sync, claudeEnv.projectDirForLog());\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n /**\n * Write the captured line, now stamped with the outcome. A no-op when nothing was captured (the\n * 'rules' hook, or a terminal boundary reached before begin()), and it clears the pending entry so\n * a second emit cannot double-log.\n *\n * `rule` is the rule that blocked, or '-' when there is none. FIELD ORDER IS APPEND-ONLY: the five\n * original fields keep their positions (cleanup automation mines this file), and `verdict=` /\n * `rule=` are added at the end.\n */\n finish(verdict: Verdict, rule: string): void {\n const invocation = this.pending;\n this.pending = null;\n if (invocation === null) return;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const logsDir = dotWebpieces.logs(invocation.root);\n fs.mkdirSync(logsDir, { recursive: true });\n const logPath = path.join(logsDir, INVOCATION_LOG_FILE);\n rotateLogFile(logPath, path.join(logsDir, INVOCATION_LOG_FILE_PREV));\n\n const line = [\n `[${invocation.timestamp}]`,\n invocation.tool,\n invocation.target,\n `branch=${invocation.branch}`,\n invocation.sync,\n `verdict=${verdict}`,\n `rule=${oneLine(rule) || '-'}`,\n // The tree the guard ACTED in, next to what Claude Code said the project was. Both, on\n // every line, because the diagnostic value is entirely in comparing them — see\n // ClaudeEnv for the open question this field exists to settle empirically.\n `root=${invocation.root}`,\n `projectDir=${invocation.projectDir}`,\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n}\n\n// Process-wide instance: one hook process handles exactly one tool call, so a single pending entry is\n// the whole state there is. Module-scope (rather than DI) because the terminal boundary that flushes\n// it — emitAllow/emitDeny — is itself module-scope protocol code with no container in reach.\nexport const invocationLog = new InvocationLog();\n\n// One-field summary of main-sync-status.json for the invocation log: the branch the cache is FOR,\n// whether it is already merged (and its PR), fork-point presence, and conflict state — the signals a\n// cleanup step keys off. 'sync=none' when the cache has not been written yet (first call of a session).\nfunction summarizeSyncStatus(status: MainSyncStatus | null): string {\n if (status === null) return 'sync=none';\n const merged = status.branchAlreadyMerged ? `PR#${status.mergedPr !== '' ? status.mergedPr : '?'}` : 'no';\n return `sync=${status.branch} merged=${merged} fork=${String(status.hasForkPoint)} conflict=${String(status.conflict)} ts=${status.timestamp}`;\n}\n\n// Best-effort current branch for the log line. Returns 'unknown' on any failure (e.g. not a git\n// repo) — this is for display only, never for a control decision.\nexport function branchForLog(root: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: root,\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim() || 'unknown';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return 'unknown';\n }\n}\n\n// Collapse newlines/tabs and cap length so one decision is always one log line.\nfunction oneLine(value: string): string {\n const flat = value.replace(/[\\t\\r\\n]+/g, ' ').trim();\n return flat.length <= MAX_TARGET_LEN ? flat : flat.slice(0, MAX_TARGET_LEN) + '…';\n}\n\nfunction rotateLogFile(logPath: string, prevPath: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const stat = fs.statSync(logPath);\n if (stat.size > MAX_LOG_BYTES) {\n if (fs.existsSync(prevPath)) fs.unlinkSync(prevPath);\n fs.renameSync(logPath, prevPath);\n }\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n}\n"]}
|
|
@@ -7,7 +7,7 @@ export declare class SyncLogEvent {
|
|
|
7
7
|
constructor(phase: SyncPhase, pid: number, branchArg: string, detail: string);
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Append one tab-separated line per refresher event to `.webpieces/
|
|
10
|
+
* Append one tab-separated line per refresher event to `.webpieces/logs/guard-async-work.log`. `root` is
|
|
11
11
|
* the workspace root holding `.webpieces`. Swallows all errors — logging must never block or fail
|
|
12
12
|
* the refresher (or the hook that spawns it).
|
|
13
13
|
*/
|
|
@@ -14,8 +14,8 @@ const to_error_1 = require("./to-error");
|
|
|
14
14
|
// exited, with stdio discarded, so when it fails to update the cache there is normally no trace.
|
|
15
15
|
// This log captures its lifecycle — SPAWN_ATTEMPT (parent side), then START / SKIP_INPROGRESS /
|
|
16
16
|
// FINISH / ERROR (child side) — so we can tell whether the detached child never launched, was killed
|
|
17
|
-
// mid-run (START with no FINISH), or threw. Writes to `.webpieces/
|
|
18
|
-
|
|
17
|
+
// mid-run (START with no FINISH), or threw. Writes to `.webpieces/logs/guard-async-work.log` (see
|
|
18
|
+
// LOGS_STATE_DIR: every webpieces log lives under `logs/`, never beside the non-log state in `hooks/`).
|
|
19
19
|
const LOG_FILE = 'guard-async-work.log';
|
|
20
20
|
const LOG_FILE_PREV = 'guard-async-work.1.log';
|
|
21
21
|
const STDERR_FILE = 'guard-async-work.stderr.log';
|
|
@@ -36,7 +36,7 @@ class SyncLogEvent {
|
|
|
36
36
|
}
|
|
37
37
|
exports.SyncLogEvent = SyncLogEvent;
|
|
38
38
|
/**
|
|
39
|
-
* Append one tab-separated line per refresher event to `.webpieces/
|
|
39
|
+
* Append one tab-separated line per refresher event to `.webpieces/logs/guard-async-work.log`. `root` is
|
|
40
40
|
* the workspace root holding `.webpieces`. Swallows all errors — logging must never block or fail
|
|
41
41
|
* the refresher (or the hook that spawns it).
|
|
42
42
|
*/
|
|
@@ -46,10 +46,10 @@ function logSyncEvent(root, event) {
|
|
|
46
46
|
const timestamp = new Date().toISOString();
|
|
47
47
|
// LOCAL scope: this is the refresher's own lifecycle trace for THIS worktree. One writer per
|
|
48
48
|
// log, so its appends cannot interleave with another agent's.
|
|
49
|
-
const
|
|
50
|
-
fs.mkdirSync(
|
|
51
|
-
const logPath = path.join(
|
|
52
|
-
rotateLogFile(logPath, path.join(
|
|
49
|
+
const logsDir = rules_config_1.dotWebpieces.logs(root);
|
|
50
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
51
|
+
const logPath = path.join(logsDir, LOG_FILE);
|
|
52
|
+
rotateLogFile(logPath, path.join(logsDir, LOG_FILE_PREV));
|
|
53
53
|
const line = [
|
|
54
54
|
`[${timestamp}]`,
|
|
55
55
|
event.phase,
|
|
@@ -66,10 +66,10 @@ function logSyncEvent(root, event) {
|
|
|
66
66
|
}
|
|
67
67
|
// Absolute path the detached child's stdout/stderr are redirected to (opened with fs.openSync(p,'a')
|
|
68
68
|
// by the spawner), so even a crash BEFORE our own logging runs — e.g. a module-load failure — is
|
|
69
|
-
// captured instead of vanishing into /dev/null. Callers must ensure the
|
|
69
|
+
// captured instead of vanishing into /dev/null. Callers must ensure the log dir exists first
|
|
70
70
|
// (logSyncEvent's mkdir, called for SPAWN_ATTEMPT, does that).
|
|
71
71
|
function syncStderrLogPath(root) {
|
|
72
|
-
return rules_config_1.dotWebpieces.
|
|
72
|
+
return rules_config_1.dotWebpieces.logsFile(root, STDERR_FILE);
|
|
73
73
|
}
|
|
74
74
|
// Collapse newlines/tabs and cap length so one event is always one log line.
|
|
75
75
|
function oneLine(value) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main-sync-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/main-sync-log.ts"],"names":[],"mappings":";;;AA2CA,oCAwBC;AAMD,8CAEC;;AA3ED,+CAAyB;AACzB,mDAA6B;AAE7B,0DAAuD;AAEvD,yCAAqC;AAErC,iGAAiG;AACjG,mGAAmG;AACnG,iGAAiG;AACjG,iGAAiG;AACjG,gGAAgG;AAChG,qGAAqG;AACrG
|
|
1
|
+
{"version":3,"file":"main-sync-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/main-sync-log.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { dotWebpieces } from '@webpieces/rules-config';\n\nimport { toError } from './to-error';\n\n// The ASYNC log — observability for the detached background refresher (sync-main.ts) that writes\n// main-sync-status.json. Its companion is the SYNC log (sync-decisions.log, decision-log.ts) which\n// records what the hook DECIDED using that cache. The refresher runs AFTER the spawning hook has\n// exited, with stdio discarded, so when it fails to update the cache there is normally no trace.\n// This log captures its lifecycle — SPAWN_ATTEMPT (parent side), then START / SKIP_INPROGRESS /\n// FINISH / ERROR (child side) — so we can tell whether the detached child never launched, was killed\n// mid-run (START with no FINISH), or threw. Writes to `.webpieces/logs/guard-async-work.log` (see\n// LOGS_STATE_DIR: every webpieces log lives under `logs/`, never beside the non-log state in `hooks/`).\nconst LOG_FILE = 'guard-async-work.log';\nconst LOG_FILE_PREV = 'guard-async-work.1.log';\nconst STDERR_FILE = 'guard-async-work.stderr.log';\nconst MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded (mirrors decision-log)\nconst MAX_DETAIL_LEN = 300;\n\nexport type SyncPhase = 'SPAWN_ATTEMPT' | 'START' | 'SKIP_INPROGRESS' | 'FINISH' | 'ERROR';\n\n// Data-only record of one refresher lifecycle event (per CLAUDE.md: classes for data).\nexport class SyncLogEvent {\n phase: SyncPhase;\n pid: number;\n branchArg: string;\n detail: string;\n\n constructor(phase: SyncPhase, pid: number, branchArg: string, detail: string) {\n this.phase = phase;\n this.pid = pid;\n this.branchArg = branchArg;\n this.detail = detail;\n }\n}\n\n/**\n * Append one tab-separated line per refresher event to `.webpieces/logs/guard-async-work.log`. `root` is\n * the workspace root holding `.webpieces`. Swallows all errors — logging must never block or fail\n * the refresher (or the hook that spawns it).\n */\nexport function logSyncEvent(root: string, event: SyncLogEvent): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const timestamp = new Date().toISOString();\n // LOCAL scope: this is the refresher's own lifecycle trace for THIS worktree. One writer per\n // log, so its appends cannot interleave with another agent's.\n const logsDir = dotWebpieces.logs(root);\n fs.mkdirSync(logsDir, { recursive: true });\n\n const logPath = path.join(logsDir, LOG_FILE);\n rotateLogFile(logPath, path.join(logsDir, LOG_FILE_PREV));\n\n const line = [\n `[${timestamp}]`,\n event.phase,\n `pid=${String(event.pid)}`,\n event.branchArg,\n oneLine(event.detail),\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n}\n\n// Absolute path the detached child's stdout/stderr are redirected to (opened with fs.openSync(p,'a')\n// by the spawner), so even a crash BEFORE our own logging runs — e.g. a module-load failure — is\n// captured instead of vanishing into /dev/null. Callers must ensure the log dir exists first\n// (logSyncEvent's mkdir, called for SPAWN_ATTEMPT, does that).\nexport function syncStderrLogPath(root: string): string {\n return dotWebpieces.logsFile(root, STDERR_FILE);\n}\n\n// Collapse newlines/tabs and cap length so one event is always one log line.\nfunction oneLine(value: string): string {\n const flat = value.replace(/[\\t\\r\\n]+/g, ' ').trim();\n return flat.length <= MAX_DETAIL_LEN ? flat : flat.slice(0, MAX_DETAIL_LEN) + '…';\n}\n\nfunction rotateLogFile(logPath: string, prevPath: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const stat = fs.statSync(logPath);\n if (stat.size > MAX_LOG_BYTES) {\n if (fs.existsSync(prevPath)) fs.unlinkSync(prevPath);\n fs.renameSync(logPath, prevPath);\n }\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n}\n"]}
|
|
@@ -6,7 +6,7 @@ export declare function resetMainSyncRefreshLatchForTest(): void;
|
|
|
6
6
|
* the NEXT call. This is the first detached spawn in the codebase — every existing hook is synchronous.
|
|
7
7
|
*
|
|
8
8
|
* Observability: we log SPAWN_ATTEMPT here and the child logs START/FINISH/ERROR, all to
|
|
9
|
-
* `.webpieces/
|
|
9
|
+
* `.webpieces/logs/guard-async-work.log`. The child's stdout/stderr are redirected to a sibling file (not
|
|
10
10
|
* /dev/null) so a crash before the child's own logging is still captured. If guard-async-work.log shows
|
|
11
11
|
* SPAWN_ATTEMPT but never START, the detached child was killed before it ran.
|
|
12
12
|
*/
|
|
@@ -23,7 +23,7 @@ function resetMainSyncRefreshLatchForTest() {
|
|
|
23
23
|
* the NEXT call. This is the first detached spawn in the codebase — every existing hook is synchronous.
|
|
24
24
|
*
|
|
25
25
|
* Observability: we log SPAWN_ATTEMPT here and the child logs START/FINISH/ERROR, all to
|
|
26
|
-
* `.webpieces/
|
|
26
|
+
* `.webpieces/logs/guard-async-work.log`. The child's stdout/stderr are redirected to a sibling file (not
|
|
27
27
|
* /dev/null) so a crash before the child's own logging is still captured. If guard-async-work.log shows
|
|
28
28
|
* SPAWN_ATTEMPT but never START, the detached child was killed before it ran.
|
|
29
29
|
*/
|
|
@@ -39,7 +39,7 @@ function triggerMainSyncRefresh(workspaceRoot, hangTimeoutMinutes = rules_config
|
|
|
39
39
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
40
40
|
try {
|
|
41
41
|
const refresher = path.join(__dirname, 'sync-main.js');
|
|
42
|
-
// SPAWN_ATTEMPT first — this also creates .webpieces/
|
|
42
|
+
// SPAWN_ATTEMPT first — this also creates .webpieces/logs so the stderr fd below can open.
|
|
43
43
|
(0, main_sync_log_1.logSyncEvent)(workspaceRoot, new main_sync_log_1.SyncLogEvent('SPAWN_ATTEMPT', process.pid, '-', `refresher=${refresher}`));
|
|
44
44
|
// Redirect the detached child's stdout+stderr to a file (not /dev/null) so an uncaught crash
|
|
45
45
|
// before the child's own logging — e.g. a module-load failure — is still captured.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main-sync-refresh.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/main-sync-refresh.ts"],"names":[],"mappings":";;AAcA,4EAEC;AAaD,wDAmCC;;AAhED,iDAAsC;AACtC,+CAAyB;AACzB,mDAA6B;AAE7B,0DAAuE;AAEvE,yCAAqC;AACrC,mDAAgF;AAEhF,qGAAqG;AACrG,yEAAyE;AACzE,IAAI,gBAAgB,GAAG,KAAK,CAAC;AAE7B,8GAA8G;AAC9G,SAAgB,gCAAgC;IAC5C,gBAAgB,GAAG,KAAK,CAAC;AAC7B,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,sBAAsB,CAAC,aAAqB,EAAE,qBAA6B,2CAA4B;IACnH,gGAAgG;IAChG,mGAAmG;IACnG,mGAAmG;IACnG,2FAA2F;IAC3F,wFAAwF;IACxF,IAAI,gBAAgB;QAAE,OAAO;IAC7B,gBAAgB,GAAG,IAAI,CAAC;IAExB,8DAA8D;IAC9D,IAAI,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;QACvD,
|
|
1
|
+
{"version":3,"file":"main-sync-refresh.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/main-sync-refresh.ts"],"names":[],"mappings":";;AAcA,4EAEC;AAaD,wDAmCC;;AAhED,iDAAsC;AACtC,+CAAyB;AACzB,mDAA6B;AAE7B,0DAAuE;AAEvE,yCAAqC;AACrC,mDAAgF;AAEhF,qGAAqG;AACrG,yEAAyE;AACzE,IAAI,gBAAgB,GAAG,KAAK,CAAC;AAE7B,8GAA8G;AAC9G,SAAgB,gCAAgC;IAC5C,gBAAgB,GAAG,KAAK,CAAC;AAC7B,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,sBAAsB,CAAC,aAAqB,EAAE,qBAA6B,2CAA4B;IACnH,gGAAgG;IAChG,mGAAmG;IACnG,mGAAmG;IACnG,2FAA2F;IAC3F,wFAAwF;IACxF,IAAI,gBAAgB;QAAE,OAAO;IAC7B,gBAAgB,GAAG,IAAI,CAAC;IAExB,8DAA8D;IAC9D,IAAI,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;QACvD,2FAA2F;QAC3F,IAAA,4BAAY,EAAC,aAAa,EAAE,IAAI,4BAAY,CAAC,eAAe,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,aAAa,SAAS,EAAE,CAAC,CAAC,CAAC;QAE3G,6FAA6F;QAC7F,mFAAmF;QACnF,MAAM,KAAK,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAA,iCAAiB,EAAC,aAAa,CAAC,EAAE,GAAG,CAAC,CAAC;QACjE,MAAM,KAAK,GAAG,IAAA,qBAAK,EAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,aAAa,EAAE,MAAM,CAAC,kBAAkB,CAAC,CAAC,EAAE;YAC1F,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,CAAC,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC;SAClC,CAAC,CAAC;QACH,8FAA8F;QAC9F,wFAAwF;QACxF,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,GAAU,EAAQ,EAAE;YACrC,IAAA,4BAAY,EAAC,aAAa,EAAE,IAAI,4BAAY,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,iBAAiB,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QACjH,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,KAAK,EAAE,CAAC;QACd,mFAAmF;QACnF,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACxB,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,KAAK,KAAK,CAAC;QACX,0EAA0E;IAC9E,CAAC;AACL,CAAC","sourcesContent":["import { spawn } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nimport { DEFAULT_HANG_TIMEOUT_MINUTES } from '@webpieces/rules-config';\n\nimport { toError } from './to-error';\nimport { logSyncEvent, SyncLogEvent, syncStderrLogPath } from './main-sync-log';\n\n// Per-process latch for the spawn below. A hook process handles exactly one tool call, so this makes\n// the refresher at-most-once per tool call. Exported reset is test-only.\nlet alreadyTriggered = false;\n\n// webpieces-disable no-function-outside-class -- test-only latch reset, matching this module's function shape\nexport function resetMainSyncRefreshLatchForTest(): void {\n alreadyTriggered = false;\n}\n\n/**\n * Fire-and-forget spawn of the detached refresher (sync-main.js in this same dir — spawned by path,\n * not a bin). The child outlives this hook process (`detached` + `unref`), does the slow\n * merged-PR/fetch/merge-base/overlap work, and writes the cache the feature-branch-guard reads on\n * the NEXT call. This is the first detached spawn in the codebase — every existing hook is synchronous.\n *\n * Observability: we log SPAWN_ATTEMPT here and the child logs START/FINISH/ERROR, all to\n * `.webpieces/logs/guard-async-work.log`. The child's stdout/stderr are redirected to a sibling file (not\n * /dev/null) so a crash before the child's own logging is still captured. If guard-async-work.log shows\n * SPAWN_ATTEMPT but never START, the detached child was killed before it ran.\n */\nexport function triggerMainSyncRefresh(workspaceRoot: string, hangTimeoutMinutes: number = DEFAULT_HANG_TIMEOUT_MINUTES): void {\n // ONE refresher per hook process. Several call sites fire this on a single tool call — the Read\n // fast path in hook-core AND read-stale-guard's own check(), for one — which is why the log showed\n // two SPAWN_ATTEMPTs ~20ms apart from the same pid on every cycle. The loser only ever reached the\n // lock and exited, so the second child was pure waste (and one more `git fetch` racing the\n // agent's). The child's lock still guards against refreshers from OTHER hook processes.\n if (alreadyTriggered) return;\n alreadyTriggered = true;\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const refresher = path.join(__dirname, 'sync-main.js');\n // SPAWN_ATTEMPT first — this also creates .webpieces/logs so the stderr fd below can open.\n logSyncEvent(workspaceRoot, new SyncLogEvent('SPAWN_ATTEMPT', process.pid, '-', `refresher=${refresher}`));\n\n // Redirect the detached child's stdout+stderr to a file (not /dev/null) so an uncaught crash\n // before the child's own logging — e.g. a module-load failure — is still captured.\n const errFd = fs.openSync(syncStderrLogPath(workspaceRoot), 'a');\n const child = spawn(process.execPath, [refresher, workspaceRoot, String(hangTimeoutMinutes)], {\n detached: true,\n stdio: ['ignore', errFd, errFd],\n });\n // spawn errors (e.g. ENOENT) arrive asynchronously; record one if it fires. The hook may exit\n // before this handler runs, but on POSIX a successful exec has already happened by now.\n child.once('error', (err: Error): void => {\n logSyncEvent(workspaceRoot, new SyncLogEvent('ERROR', child.pid ?? -1, '-', `spawn failed: ${err.message}`));\n });\n child.unref();\n // The child has its own dup'd copy of the fd after spawn; close the parent's copy.\n fs.closeSync(errFd);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n // Spawning the background refresh must never block or fail the tool call.\n }\n}\n"]}
|
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
import type { ToolKind, NormalizedToolInput, BlockedResult } from './types';
|
|
2
2
|
export declare function logRejection(toolKind: ToolKind, input: NormalizedToolInput, result: BlockedResult, cwd: string): void;
|
|
3
|
+
/**
|
|
4
|
+
* The rule names a block report cites — every `[<rule-name>] (` header it opens with. Exported because
|
|
5
|
+
* two audit streams need the same answer from the same regex: this file's rejection index, and the
|
|
6
|
+
* `rule=` field guard-invocations.log now carries (see InvocationLog.finish). Two scrapers would be
|
|
7
|
+
* two answers to one question.
|
|
8
|
+
*/
|
|
9
|
+
export declare function extractRuleNames(report: string): string[];
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.logRejection = logRejection;
|
|
4
|
+
exports.extractRuleNames = extractRuleNames;
|
|
4
5
|
const tslib_1 = require("tslib");
|
|
5
6
|
const fs = tslib_1.__importStar(require("fs"));
|
|
6
7
|
const path = tslib_1.__importStar(require("path"));
|
|
7
8
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
8
|
-
|
|
9
|
+
// The rejection log SPLITS across the two state dirs on purpose: the `.log` index goes to `logs/`
|
|
10
|
+
// with every other webpieces log, while the dated `hooks/<YYYY-MM-DD>/writeInfo-*.md` DETAIL files
|
|
11
|
+
// are not logs and stay in `hooks/` (see LOGS_STATE_DIR).
|
|
9
12
|
const LOG_FILE = 'hook-rejection.log';
|
|
10
13
|
const LOG_FILE_PREV = 'hook-rejection.1.log';
|
|
11
14
|
const MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded
|
|
@@ -15,7 +18,7 @@ function logRejection(toolKind, input, result, cwd) {
|
|
|
15
18
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
16
19
|
try {
|
|
17
20
|
// `.webpieces/` lives at the repo root, NOT the AI's cwd — resolve it so a hook fired while
|
|
18
|
-
// the AI is in a subdirectory never scatters a stray `<subdir>/.webpieces
|
|
21
|
+
// the AI is in a subdirectory never scatters a stray `<subdir>/.webpieces` tree.
|
|
19
22
|
const root = new rules_config_1.RepoRootFinder().resolveRepoRoot(cwd);
|
|
20
23
|
const now = new Date();
|
|
21
24
|
const timestamp = now.toISOString();
|
|
@@ -23,17 +26,22 @@ function logRejection(toolKind, input, result, cwd) {
|
|
|
23
26
|
const dateStr = timestamp.slice(0, 10);
|
|
24
27
|
// LOCAL scope — a rejection is this worktree's event, and a per-worktree log has exactly one
|
|
25
28
|
// writer, so its appends and its daily detail files cannot collide with another agent's.
|
|
26
|
-
const hooksDir = rules_config_1.dotWebpieces.localFile(root,
|
|
29
|
+
const hooksDir = rules_config_1.dotWebpieces.localFile(root, rules_config_1.HOOKS_STATE_DIR);
|
|
30
|
+
const logsDir = rules_config_1.dotWebpieces.logs(root);
|
|
27
31
|
const dayDir = path.join(hooksDir, dateStr);
|
|
28
32
|
fs.mkdirSync(dayDir, { recursive: true });
|
|
33
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
29
34
|
const relativePath = computeRelativePath(input.filePath, root);
|
|
30
35
|
const ruleNames = extractRuleNames(result.report);
|
|
31
36
|
const detailFileName = `writeInfo-${epochMs}.md`;
|
|
32
|
-
|
|
37
|
+
// Relative to the STATE DIR, not to the log file's own directory: the index now lives in
|
|
38
|
+
// `logs/` while the detail lives in `hooks/<date>/`, so a bare `<date>/<file>` would no
|
|
39
|
+
// longer resolve from where the reader found the line.
|
|
40
|
+
const detailRelPath = `${rules_config_1.HOOKS_STATE_DIR}/${dateStr}/${detailFileName}`;
|
|
33
41
|
const detail = buildDetailContent(timestamp, toolKind, relativePath, ruleNames, result.report, input);
|
|
34
42
|
fs.writeFileSync(path.join(dayDir, detailFileName), detail);
|
|
35
|
-
const logPath = path.join(
|
|
36
|
-
rotateLogFile(logPath, path.join(
|
|
43
|
+
const logPath = path.join(logsDir, LOG_FILE);
|
|
44
|
+
rotateLogFile(logPath, path.join(logsDir, LOG_FILE_PREV));
|
|
37
45
|
const logLine = `[${timestamp}]\t${toolKind}\t${relativePath}\t[${ruleNames.join(',')}]\t${detailRelPath}\n`;
|
|
38
46
|
fs.appendFileSync(logPath, logLine);
|
|
39
47
|
rotateOldDays(hooksDir, MAX_AGE_DAYS);
|
|
@@ -52,6 +60,13 @@ function computeRelativePath(filePath, cwd) {
|
|
|
52
60
|
}
|
|
53
61
|
return filePath;
|
|
54
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* The rule names a block report cites — every `[<rule-name>] (` header it opens with. Exported because
|
|
65
|
+
* two audit streams need the same answer from the same regex: this file's rejection index, and the
|
|
66
|
+
* `rule=` field guard-invocations.log now carries (see InvocationLog.finish). Two scrapers would be
|
|
67
|
+
* two answers to one question.
|
|
68
|
+
*/
|
|
69
|
+
// webpieces-disable no-function-outside-class -- pure regex scraper beside this module's other module-scope helpers; exported so the invocation log and the rejection index scrape rule names with the SAME code.
|
|
55
70
|
function extractRuleNames(report) {
|
|
56
71
|
const names = [];
|
|
57
72
|
let match = RULE_NAME_RE.exec(report);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rejection-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/rejection-log.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { dotWebpieces, RepoRootFinder } from '@webpieces/rules-config';\n\nimport type { ToolKind, NormalizedToolInput, BlockedResult } from './types';\n\nconst HOOKS_DIR = 'hooks';\nconst LOG_FILE = 'hook-rejection.log';\nconst LOG_FILE_PREV = 'hook-rejection.1.log';\nconst MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded\nconst MAX_AGE_DAYS = 7;\n\nconst RULE_NAME_RE = /^\\[([^\\]]+)\\] \\(/gm;\n\nexport function logRejection(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n result: BlockedResult,\n cwd: string,\n): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // `.webpieces/` lives at the repo root, NOT the AI's cwd — resolve it so a hook fired while\n // the AI is in a subdirectory never scatters a stray `<subdir>/.webpieces/hooks` tree.\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n const now = new Date();\n const timestamp = now.toISOString();\n const epochMs = String(now.getTime());\n const dateStr = timestamp.slice(0, 10);\n\n // LOCAL scope — a rejection is this worktree's event, and a per-worktree log has exactly one\n // writer, so its appends and its daily detail files cannot collide with another agent's.\n const hooksDir = dotWebpieces.localFile(root, HOOKS_DIR);\n const dayDir = path.join(hooksDir, dateStr);\n fs.mkdirSync(dayDir, { recursive: true });\n\n const relativePath = computeRelativePath(input.filePath, root);\n const ruleNames = extractRuleNames(result.report);\n const detailFileName = `writeInfo-${epochMs}.md`;\n const detailRelPath = `${dateStr}/${detailFileName}`;\n\n const detail = buildDetailContent(timestamp, toolKind, relativePath, ruleNames, result.report, input);\n fs.writeFileSync(path.join(dayDir, detailFileName), detail);\n\n const logPath = path.join(hooksDir, LOG_FILE);\n rotateLogFile(logPath, path.join(hooksDir, LOG_FILE_PREV));\n\n const logLine = `[${timestamp}]\\t${toolKind}\\t${relativePath}\\t[${ruleNames.join(',')}]\\t${detailRelPath}\\n`;\n fs.appendFileSync(logPath, logLine);\n\n rotateOldDays(hooksDir, MAX_AGE_DAYS);\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n}\n\nfunction computeRelativePath(filePath: string, cwd: string): string {\n if (filePath.startsWith(cwd)) {\n const rel = filePath.slice(cwd.length);\n if (rel.startsWith('/')) return rel.slice(1);\n return rel;\n }\n return filePath;\n}\n\nfunction extractRuleNames(report: string): string[] {\n const names: string[] = [];\n let match = RULE_NAME_RE.exec(report);\n while (match !== null) {\n names.push(match[1]);\n match = RULE_NAME_RE.exec(report);\n }\n RULE_NAME_RE.lastIndex = 0;\n return names;\n}\n\nfunction buildDetailContent(\n timestamp: string,\n toolKind: ToolKind,\n relativePath: string,\n ruleNames: string[],\n report: string,\n input: NormalizedToolInput,\n): string {\n const lines: string[] = [];\n lines.push('# Hook Rejection Detail');\n lines.push('');\n lines.push(`- **Timestamp:** ${timestamp}`);\n lines.push(`- **Tool:** ${toolKind}`);\n lines.push(`- **File:** ${relativePath}`);\n lines.push(`- **Rules violated:** ${ruleNames.join(', ')}`);\n lines.push('');\n lines.push('## Report');\n lines.push('');\n lines.push('```');\n lines.push(report.trimEnd());\n lines.push('```');\n lines.push('');\n lines.push('## Content Being Written');\n lines.push('');\n\n if (toolKind === 'Write') {\n const content = input.edits.length > 0 ? input.edits[0].newString : '';\n lines.push('```typescript');\n lines.push(content.trimEnd());\n lines.push('```');\n } else {\n for (let i = 0; i < input.edits.length; i += 1) {\n const edit = input.edits[i];\n lines.push(`### Edit ${String(i + 1)} of ${String(input.edits.length)}`);\n lines.push('');\n lines.push('**old_string:**');\n lines.push('```typescript');\n lines.push(edit.oldString.trimEnd());\n lines.push('```');\n lines.push('');\n lines.push('**new_string:**');\n lines.push('```typescript');\n lines.push(edit.newString.trimEnd());\n lines.push('```');\n lines.push('');\n }\n }\n\n return lines.join('\\n') + '\\n';\n}\n\nfunction rotateLogFile(logPath: string, prevPath: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const stat = fs.statSync(logPath);\n if (stat.size > MAX_LOG_BYTES) {\n if (fs.existsSync(prevPath)) fs.unlinkSync(prevPath);\n fs.renameSync(logPath, prevPath);\n }\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n}\n\nfunction rotateOldDays(hooksDir: string, maxAgeDays: number): void {\n const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;\n let entries: string[];\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n entries = fs.readdirSync(hooksDir);\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n return;\n }\n\n for (const entry of entries) {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(entry)) continue;\n const dirDate = new Date(entry + 'T00:00:00Z');\n if (isNaN(dirDate.getTime())) continue;\n if (dirDate.getTime() < cutoff) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.rmSync(path.join(hooksDir, entry), { recursive: true, force: true });\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"rejection-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/rejection-log.ts"],"names":[],"mappings":";;AAiBA,oCA8CC;AAkBD,4CASC;;AA1FD,+CAAyB;AACzB,mDAA6B;AAE7B,0DAAwF;AAIxF,kGAAkG;AAClG,mGAAmG;AACnG,0DAA0D;AAC1D,MAAM,QAAQ,GAAG,oBAAoB,CAAC;AACtC,MAAM,aAAa,GAAG,sBAAsB,CAAC;AAC7C,MAAM,aAAa,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC,gCAAgC;AAClE,MAAM,YAAY,GAAG,CAAC,CAAC;AAEvB,MAAM,YAAY,GAAG,oBAAoB,CAAC;AAE1C,SAAgB,YAAY,CACxB,QAAkB,EAClB,KAA0B,EAC1B,MAAqB,EACrB,GAAW;IAEX,8DAA8D;IAC9D,IAAI,CAAC;QACD,4FAA4F;QAC5F,iFAAiF;QACjF,MAAM,IAAI,GAAG,IAAI,6BAAc,EAAE,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;QACvD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,SAAS,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;QACpC,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QACtC,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAEvC,6FAA6F;QAC7F,yFAAyF;QACzF,MAAM,QAAQ,GAAG,2BAAY,CAAC,SAAS,CAAC,IAAI,EAAE,8BAAe,CAAC,CAAC;QAC/D,MAAM,OAAO,GAAG,2BAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxC,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC5C,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1C,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAE3C,MAAM,YAAY,GAAG,mBAAmB,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QAC/D,MAAM,SAAS,GAAG,gBAAgB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAClD,MAAM,cAAc,GAAG,aAAa,OAAO,KAAK,CAAC;QACjD,yFAAyF;QACzF,wFAAwF;QACxF,uDAAuD;QACvD,MAAM,aAAa,GAAG,GAAG,8BAAe,IAAI,OAAO,IAAI,cAAc,EAAE,CAAC;QAExE,MAAM,MAAM,GAAG,kBAAkB,CAAC,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QACtG,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,CAAC;QAE5D,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC7C,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC;QAE1D,MAAM,OAAO,GAAG,IAAI,SAAS,MAAM,QAAQ,KAAK,YAAY,MAAM,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,aAAa,IAAI,CAAC;QAC7G,EAAE,CAAC,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAEpC,aAAa,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IAC1C,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,6BAA6B;QAC7B,KAAK,GAAG,CAAC;IACb,CAAC;AACL,CAAC;AAED,SAAS,mBAAmB,CAAC,QAAgB,EAAE,GAAW;IACtD,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC7C,OAAO,GAAG,CAAC;IACf,CAAC;IACD,OAAO,QAAQ,CAAC;AACpB,CAAC;AAED;;;;;GAKG;AACH,kNAAkN;AAClN,SAAgB,gBAAgB,CAAC,MAAc;IAC3C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACtC,OAAO,KAAK,KAAK,IAAI,EAAE,CAAC;QACpB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACrB,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IACD,YAAY,CAAC,SAAS,GAAG,CAAC,CAAC;IAC3B,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,SAAS,kBAAkB,CACvB,SAAiB,EACjB,QAAkB,EAClB,YAAoB,EACpB,SAAmB,EACnB,MAAc,EACd,KAA0B;IAE1B,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;IACtC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,oBAAoB,SAAS,EAAE,CAAC,CAAC;IAC5C,KAAK,CAAC,IAAI,CAAC,eAAe,QAAQ,EAAE,CAAC,CAAC;IACtC,KAAK,CAAC,IAAI,CAAC,eAAe,YAAY,EAAE,CAAC,CAAC;IAC1C,KAAK,CAAC,IAAI,CAAC,yBAAyB,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5D,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACxB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClB,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IAC7B,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;IACvC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACvB,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;QACvE,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAC5B,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;QAC9B,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;SAAM,CAAC;QACJ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7C,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC5B,KAAK,CAAC,IAAI,CAAC,YAAY,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACzE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACf,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;YAC9B,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;YAC5B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC;YACrC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAClB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACf,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;YAC9B,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;YAC5B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC;YACrC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAClB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACnB,CAAC;IACL,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AACnC,CAAC;AAED,SAAS,aAAa,CAAC,OAAe,EAAE,QAAgB;IACpD,8DAA8D;IAC9D,IAAI,CAAC;QACD,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAClC,IAAI,IAAI,CAAC,IAAI,GAAG,aAAa,EAAE,CAAC;YAC5B,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;gBAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;YACrD,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QACrC,CAAC;IACL,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,6BAA6B;QAC7B,KAAK,GAAG,CAAC;IACb,CAAC;AACL,CAAC;AAED,SAAS,aAAa,CAAC,QAAgB,EAAE,UAAkB;IACvD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC7D,IAAI,OAAiB,CAAC;IACtB,8DAA8D;IAC9D,IAAI,CAAC;QACD,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,6BAA6B;QAC7B,KAAK,GAAG,CAAC;QACT,OAAO;IACX,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC1B,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,SAAS;QACjD,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,KAAK,GAAG,YAAY,CAAC,CAAC;QAC/C,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YAAE,SAAS;QACvC,IAAI,OAAO,CAAC,OAAO,EAAE,GAAG,MAAM,EAAE,CAAC;YAC7B,8DAA8D;YAC9D,IAAI,CAAC;gBACD,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAC5E,CAAC;YAAC,OAAO,GAAY,EAAE,CAAC;gBACpB,6BAA6B;gBAC7B,KAAK,GAAG,CAAC;YACb,CAAC;QACL,CAAC;IACL,CAAC;AACL,CAAC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { dotWebpieces, RepoRootFinder, HOOKS_STATE_DIR } from '@webpieces/rules-config';\n\nimport type { ToolKind, NormalizedToolInput, BlockedResult } from './types';\n\n// The rejection log SPLITS across the two state dirs on purpose: the `.log` index goes to `logs/`\n// with every other webpieces log, while the dated `hooks/<YYYY-MM-DD>/writeInfo-*.md` DETAIL files\n// are not logs and stay in `hooks/` (see LOGS_STATE_DIR).\nconst LOG_FILE = 'hook-rejection.log';\nconst LOG_FILE_PREV = 'hook-rejection.1.log';\nconst MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded\nconst MAX_AGE_DAYS = 7;\n\nconst RULE_NAME_RE = /^\\[([^\\]]+)\\] \\(/gm;\n\nexport function logRejection(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n result: BlockedResult,\n cwd: string,\n): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // `.webpieces/` lives at the repo root, NOT the AI's cwd — resolve it so a hook fired while\n // the AI is in a subdirectory never scatters a stray `<subdir>/.webpieces` tree.\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n const now = new Date();\n const timestamp = now.toISOString();\n const epochMs = String(now.getTime());\n const dateStr = timestamp.slice(0, 10);\n\n // LOCAL scope — a rejection is this worktree's event, and a per-worktree log has exactly one\n // writer, so its appends and its daily detail files cannot collide with another agent's.\n const hooksDir = dotWebpieces.localFile(root, HOOKS_STATE_DIR);\n const logsDir = dotWebpieces.logs(root);\n const dayDir = path.join(hooksDir, dateStr);\n fs.mkdirSync(dayDir, { recursive: true });\n fs.mkdirSync(logsDir, { recursive: true });\n\n const relativePath = computeRelativePath(input.filePath, root);\n const ruleNames = extractRuleNames(result.report);\n const detailFileName = `writeInfo-${epochMs}.md`;\n // Relative to the STATE DIR, not to the log file's own directory: the index now lives in\n // `logs/` while the detail lives in `hooks/<date>/`, so a bare `<date>/<file>` would no\n // longer resolve from where the reader found the line.\n const detailRelPath = `${HOOKS_STATE_DIR}/${dateStr}/${detailFileName}`;\n\n const detail = buildDetailContent(timestamp, toolKind, relativePath, ruleNames, result.report, input);\n fs.writeFileSync(path.join(dayDir, detailFileName), detail);\n\n const logPath = path.join(logsDir, LOG_FILE);\n rotateLogFile(logPath, path.join(logsDir, LOG_FILE_PREV));\n\n const logLine = `[${timestamp}]\\t${toolKind}\\t${relativePath}\\t[${ruleNames.join(',')}]\\t${detailRelPath}\\n`;\n fs.appendFileSync(logPath, logLine);\n\n rotateOldDays(hooksDir, MAX_AGE_DAYS);\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n}\n\nfunction computeRelativePath(filePath: string, cwd: string): string {\n if (filePath.startsWith(cwd)) {\n const rel = filePath.slice(cwd.length);\n if (rel.startsWith('/')) return rel.slice(1);\n return rel;\n }\n return filePath;\n}\n\n/**\n * The rule names a block report cites — every `[<rule-name>] (` header it opens with. Exported because\n * two audit streams need the same answer from the same regex: this file's rejection index, and the\n * `rule=` field guard-invocations.log now carries (see InvocationLog.finish). Two scrapers would be\n * two answers to one question.\n */\n// webpieces-disable no-function-outside-class -- pure regex scraper beside this module's other module-scope helpers; exported so the invocation log and the rejection index scrape rule names with the SAME code.\nexport function extractRuleNames(report: string): string[] {\n const names: string[] = [];\n let match = RULE_NAME_RE.exec(report);\n while (match !== null) {\n names.push(match[1]);\n match = RULE_NAME_RE.exec(report);\n }\n RULE_NAME_RE.lastIndex = 0;\n return names;\n}\n\nfunction buildDetailContent(\n timestamp: string,\n toolKind: ToolKind,\n relativePath: string,\n ruleNames: string[],\n report: string,\n input: NormalizedToolInput,\n): string {\n const lines: string[] = [];\n lines.push('# Hook Rejection Detail');\n lines.push('');\n lines.push(`- **Timestamp:** ${timestamp}`);\n lines.push(`- **Tool:** ${toolKind}`);\n lines.push(`- **File:** ${relativePath}`);\n lines.push(`- **Rules violated:** ${ruleNames.join(', ')}`);\n lines.push('');\n lines.push('## Report');\n lines.push('');\n lines.push('```');\n lines.push(report.trimEnd());\n lines.push('```');\n lines.push('');\n lines.push('## Content Being Written');\n lines.push('');\n\n if (toolKind === 'Write') {\n const content = input.edits.length > 0 ? input.edits[0].newString : '';\n lines.push('```typescript');\n lines.push(content.trimEnd());\n lines.push('```');\n } else {\n for (let i = 0; i < input.edits.length; i += 1) {\n const edit = input.edits[i];\n lines.push(`### Edit ${String(i + 1)} of ${String(input.edits.length)}`);\n lines.push('');\n lines.push('**old_string:**');\n lines.push('```typescript');\n lines.push(edit.oldString.trimEnd());\n lines.push('```');\n lines.push('');\n lines.push('**new_string:**');\n lines.push('```typescript');\n lines.push(edit.newString.trimEnd());\n lines.push('```');\n lines.push('');\n }\n }\n\n return lines.join('\\n') + '\\n';\n}\n\nfunction rotateLogFile(logPath: string, prevPath: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const stat = fs.statSync(logPath);\n if (stat.size > MAX_LOG_BYTES) {\n if (fs.existsSync(prevPath)) fs.unlinkSync(prevPath);\n fs.renameSync(logPath, prevPath);\n }\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n}\n\nfunction rotateOldDays(hooksDir: string, maxAgeDays: number): void {\n const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;\n let entries: string[];\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n entries = fs.readdirSync(hooksDir);\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n return;\n }\n\n for (const entry of entries) {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(entry)) continue;\n const dirDate = new Date(entry + 'T00:00:00Z');\n if (isNaN(dirDate.getTime())) continue;\n if (dirDate.getTime() < cutoff) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.rmSync(path.join(hooksDir, entry), { recursive: true, force: true });\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n }\n }\n}\n"]}
|
package/templates/ai-hook.sh
CHANGED
|
@@ -80,6 +80,58 @@ fi
|
|
|
80
80
|
# Read the tool payload ONCE, up front. The shim no longer exec's the bin (see RUN_BIN_SH), so it must
|
|
81
81
|
# forward stdin to the bin itself — and it needs the payload again on the fail-closed path below.
|
|
82
82
|
PAYLOAD="$(cat)"
|
|
83
|
+
CMD="$(printf '%s' "$PAYLOAD" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"\\]*\)".*/\1/p')"
|
|
84
|
+
TOOL="$(printf '%s' "$PAYLOAD" | sed -n 's/.*"tool_name"[[:space:]]*:[[:space:]]*"\([^"\\]*\)".*/\1/p')"
|
|
85
|
+
FILE="$(printf '%s' "$PAYLOAD" | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"\\]*\)".*/\1/p')"
|
|
86
|
+
WP_CWD="$(printf '%s' "$PAYLOAD" | sed -n 's/.*"cwd"[[:space:]]*:[[:space:]]*"\([^"\\]*\)".*/\1/p')"
|
|
87
|
+
[ -n "$WP_CWD" ] || WP_CWD="$ROOT" # no cwd in the payload (older client, or a hand-run) → the shim's own tree
|
|
88
|
+
# Best-effort AUDIT TRAIL of what L0 did with this call — every call, not just the broken ones. One
|
|
89
|
+
# tab-separated line per invocation into this TREE's own logs/ai-hook-shim.log (gitignored), so the
|
|
90
|
+
# observed behaviour can be diffed against the matrix in guards/L0-tooling.md. NEVER breaks or blocks the
|
|
91
|
+
# hook: every write is swallowed, and nothing ever goes to stdout (stdout is the PreToolUse decision
|
|
92
|
+
# channel — a stray byte there would corrupt allow/deny).
|
|
93
|
+
WP_TREE=""
|
|
94
|
+
WP_LOG_DIR=""
|
|
95
|
+
wp_resolve_log_dir() {
|
|
96
|
+
_wp_rp="$(git -C "$WP_CWD" rev-parse --git-dir --git-common-dir 2>/dev/null)"
|
|
97
|
+
_wp_gd="$(printf '%s\n' "$_wp_rp" | sed -n 1p)"
|
|
98
|
+
_wp_cd="$(printf '%s\n' "$_wp_rp" | sed -n 2p)"
|
|
99
|
+
if [ -z "$_wp_gd" ] || [ -z "$_wp_cd" ]; then
|
|
100
|
+
WP_TREE=primary; WP_LOG_DIR="$WP_CWD/.webpieces/logs"; return 0
|
|
101
|
+
fi
|
|
102
|
+
# git prints a BARE .git from the primary clone and an absolute path from a linked worktree; the TS
|
|
103
|
+
# twin runs path.resolve(cwd, printed), so do the same before comparing or taking a basename.
|
|
104
|
+
case "$_wp_gd" in /*) : ;; *) _wp_gd="$WP_CWD/$_wp_gd" ;; esac
|
|
105
|
+
case "$_wp_cd" in /*) : ;; *) _wp_cd="$WP_CWD/$_wp_cd" ;; esac
|
|
106
|
+
# The primary clone's root is the parent of the SHARED git dir — declining any layout whose shared
|
|
107
|
+
# dir is not named .git (a bare repo, --separate-git-dir), same test as primaryRoot().
|
|
108
|
+
_wp_primary="$WP_CWD"
|
|
109
|
+
case "$_wp_cd" in
|
|
110
|
+
*/.git) [ -d "${_wp_cd%/*}" ] && _wp_primary="${_wp_cd%/*}" ;;
|
|
111
|
+
esac
|
|
112
|
+
if [ "$_wp_gd" = "$_wp_cd" ]; then
|
|
113
|
+
WP_TREE=primary
|
|
114
|
+
WP_LOG_DIR="$_wp_primary/.webpieces/logs"
|
|
115
|
+
else
|
|
116
|
+
# git's OWN name for the worktree (the basename of <primary>/.git/worktrees/<name>), not the
|
|
117
|
+
# directory's basename — two worktrees under different parents may share a directory name.
|
|
118
|
+
WP_TREE="${_wp_gd##*/}"
|
|
119
|
+
WP_LOG_DIR="$_wp_primary/.webpieces/worktrees/$WP_TREE/logs"
|
|
120
|
+
fi
|
|
121
|
+
}
|
|
122
|
+
wp_log() { # $1 = L0 fault code (D|X|K|-), $2 = verdict label
|
|
123
|
+
{
|
|
124
|
+
[ -n "$WP_LOG_DIR" ] || wp_resolve_log_dir
|
|
125
|
+
mkdir -p "$WP_LOG_DIR" 2>/dev/null || return 0
|
|
126
|
+
_wp_f="$WP_LOG_DIR/ai-hook-shim.log"
|
|
127
|
+
# Rotate at the SAME 512 KB into the SAME .1.log sibling as every JS-side webpieces log. This runs
|
|
128
|
+
# on every tool call, so it is one wc and no more; a size we cannot read counts as 0 (no rotation).
|
|
129
|
+
_wp_sz="$(wc -c < "$_wp_f" 2>/dev/null | tr -d ' ')"
|
|
130
|
+
case "$_wp_sz" in ''|*[!0-9]*) _wp_sz=0 ;; esac
|
|
131
|
+
[ "$_wp_sz" -gt 524288 ] && mv -f "$_wp_f" "$WP_LOG_DIR/ai-hook-shim.1.log" 2>/dev/null
|
|
132
|
+
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null)" "$BIN_NAME" "$TOOL" "tree=$WP_TREE" "fault=$1" "$2" "$CMD" >> "$_wp_f"
|
|
133
|
+
} 2>/dev/null || true
|
|
134
|
+
}
|
|
83
135
|
BROKEN_BIN=""
|
|
84
136
|
CRASH_MSG=""
|
|
85
137
|
if [ -x "$BIN" ] && [ -z "$DRIFT_PKG" ]; then
|
|
@@ -91,6 +143,14 @@ if [ -x "$BIN" ] && [ -z "$DRIFT_PKG" ]; then
|
|
|
91
143
|
cat "$OUT_FILE" # the guard's real decision — verbatim
|
|
92
144
|
cat "$ERR_FILE" >&2
|
|
93
145
|
rm -f "$OUT_FILE" "$ERR_FILE" 2>/dev/null
|
|
146
|
+
# THE HEALTHY CALL, which this log used to be silent about (see WP_LOG_SH's header). No sh-side
|
|
147
|
+
# fault, the bin ran, and its own exit code says how it ended — 0 allow, 2 block. Logged AFTER the
|
|
148
|
+
# decision bytes are already on stdout, so the audit trail never sits between the guard and Claude
|
|
149
|
+
# Code. Without this line, "no entry" meant either healthy or never-ran, and those are the two
|
|
150
|
+
# answers a reader most needs to tell apart.
|
|
151
|
+
WP_VERDICT=PASS-BIN-ALLOW
|
|
152
|
+
[ "$RC" = 2 ] && WP_VERDICT=PASS-BIN-BLOCK
|
|
153
|
+
wp_log - "$WP_VERDICT"
|
|
94
154
|
exit "$RC"
|
|
95
155
|
fi
|
|
96
156
|
# Crashed. Keep the most useful stderr line for the human. Strip " and backslash so the text stays a
|
|
@@ -107,18 +167,11 @@ fi
|
|
|
107
167
|
# through: the assistant's own Bash tool routes through this hook too, so blocking everything would
|
|
108
168
|
# deadlock the very commands (pnpm install / rm -rf node_modules && pnpm install) that re-enable the
|
|
109
169
|
# guards. A silent exit 0 = "allow" in the PreToolUse protocol; the guards resume once the tree is sane.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
# tab-separated line per call → <root>/.webpieces/logs/ai-hook-shim.log (gitignored). NEVER breaks or
|
|
116
|
-
# blocks the hook: all writes are best-effort (|| true) and go to a file, never to stdout (stdout is
|
|
117
|
-
# the PreToolUse decision channel — a stray byte there would corrupt allow/deny).
|
|
118
|
-
LOG_DIR="$ROOT/.webpieces/logs"
|
|
119
|
-
wp_log() { # $1 = label (ALLOW-CURE|ALLOW-READ|ALLOW-CONFIG|DENY|DENY-STALE|DENY-BROKEN)
|
|
120
|
-
{ mkdir -p "$LOG_DIR" 2>/dev/null && printf '%s\t%s\t%s\t%s\t%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null)" "$BIN_NAME" "$TOOL" "$1" "$CMD" >> "$LOG_DIR/ai-hook-shim.log"; } 2>/dev/null || true
|
|
121
|
-
}
|
|
170
|
+
# WHICH of the six guards/L0-tooling.md faults fired, in the doc's own letters. Only the three sh-side
|
|
171
|
+
# codes can be decided here; S/C/Y live in the binary, which never got to run on this path.
|
|
172
|
+
WP_FAULT=X # X — bin missing (fresh clone, new worktree)
|
|
173
|
+
[ -n "$DRIFT_PKG" ] && WP_FAULT=D # D — version drift; D and K are mutually exclusive
|
|
174
|
+
[ -n "$BROKEN_BIN" ] && WP_FAULT=K # K — bin present but CRASHED (corrupt node_modules)
|
|
122
175
|
DENY_LABEL="DENY"
|
|
123
176
|
[ -n "$DRIFT_PKG" ] && DENY_LABEL="DENY-STALE" # version drift, not a missing bin
|
|
124
177
|
[ -n "$BROKEN_BIN" ] && DENY_LABEL="DENY-BROKEN" # bin present but CRASHED (corrupt node_modules)
|
|
@@ -126,19 +179,19 @@ DENY_LABEL="DENY"
|
|
|
126
179
|
# help a given fault also cannot hurt it, and gating each entry on a fault is what produced the four
|
|
127
180
|
# defects recorded above L0_ALLOW_ERE.
|
|
128
181
|
if [ "$TOOL" = "Read" ]; then
|
|
129
|
-
wp_log ALLOW-READ
|
|
182
|
+
wp_log "$WP_FAULT" ALLOW-READ # you must be able to read to work out how to fix this
|
|
130
183
|
exit 0
|
|
131
184
|
fi
|
|
132
185
|
case "$FILE" in
|
|
133
186
|
*/webpieces.config.json|webpieces.config.json)
|
|
134
|
-
wp_log ALLOW-CONFIG
|
|
187
|
+
wp_log "$WP_FAULT" ALLOW-CONFIG # the always-allowed recovery target — every guard is configured from it
|
|
135
188
|
exit 0 ;;
|
|
136
189
|
esac
|
|
137
190
|
if printf '%s' "$CMD" | grep -Eq '^(cd[[:space:]]+([A-Za-z0-9._/@~+-]+|'\''[^'\'']+'\'')[[:space:]]*&&[[:space:]]*)?((pnpm|npm)[[:space:]]+(install|i)([[:space:]]+--[A-Za-z][A-Za-z0-9=._/@:-]*)*|rm[[:space:]]+-rf[[:space:]]+(\./)?node_modules/?([[:space:]]*&&[[:space:]]*(pnpm|npm)[[:space:]]+(install|i)([[:space:]]+--[A-Za-z][A-Za-z0-9=._/@:-]*)*)?|git[[:space:]]+(pull|fetch)([[:space:]]+(--)?[A-Za-z0-9][A-Za-z0-9=._/@:-]*)*|(pnpm|npm|npx)([[:space:]]+(exec|run))?[[:space:]]+wp-upgrade-shim|cp[[:space:]]+(\./)?node_modules/@webpieces/ai-hook-rules/templates/ai-hook\.sh[[:space:]]+(\./)?\.claude/webpieces/ai-hook\.sh|(pnpm|npm|npx)([[:space:]]+(exec|run))?[[:space:]]+wp-install-ai-hooks([[:space:]]+--[A-Za-z][A-Za-z0-9=._/@:-]*)*|(pwd|git[[:space:]]+(status|log|diff|show|branch|rev-parse)|git[[:space:]]+worktree[[:space:]]+list)([[:space:]]+(--)?[A-Za-z0-9][A-Za-z0-9=._/@:-]*)*)([[:space:]]+2>(&1|/dev/null))?([[:space:]]*\|[[:space:]]*(tail|head)([[:space:]]+-(n[[:space:]]+)?[0-9]+)?)?[[:space:]]*$'; then
|
|
138
|
-
wp_log ALLOW-CURE
|
|
191
|
+
wp_log "$WP_FAULT" ALLOW-CURE # record the self-heal we let through (re-enables the guards)
|
|
139
192
|
exit 0 # allow the cure so the assistant can break the deadlock
|
|
140
193
|
fi
|
|
141
|
-
wp_log "$DENY_LABEL"
|
|
194
|
+
wp_log "$WP_FAULT" "$DENY_LABEL" # every fail-closed block, with the fault that caused it
|
|
142
195
|
if [ -n "$BROKEN_BIN" ]; then
|
|
143
196
|
# Report (do NOT auto-clean) the orphaned pnpm staging dirs — a package pnpm was mid-way through
|
|
144
197
|
# writing is left behind as <name>_<pid>_<hash>. Their presence is the fingerprint of an install that
|