@webpieces/ai-hook-rules 0.4.609 → 0.4.611

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.
Files changed (74) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -2
  3. package/src/adapters/claude-code-response.js +6 -2
  4. package/src/adapters/claude-code-response.js.map +1 -1
  5. package/src/adapters/hook-core.js +13 -10
  6. package/src/adapters/hook-core.js.map +1 -1
  7. package/src/bin/dev-hook-install.js +3 -3
  8. package/src/bin/dev-hook-install.js.map +1 -1
  9. package/src/bin/guarantee-root.js +23 -10
  10. package/src/bin/guarantee-root.js.map +1 -1
  11. package/src/bin/l0-allowlist.js +1 -1
  12. package/src/bin/l0-allowlist.js.map +1 -1
  13. package/src/bin/setup.js +6 -3
  14. package/src/bin/setup.js.map +1 -1
  15. package/src/bin/shim-audit-log.d.ts +0 -3
  16. package/src/bin/shim-audit-log.js +14 -13
  17. package/src/bin/shim-audit-log.js.map +1 -1
  18. package/src/bin/shim.js +2 -2
  19. package/src/bin/shim.js.map +1 -1
  20. package/src/core/decision-log.d.ts +75 -10
  21. package/src/core/decision-log.js +91 -28
  22. package/src/core/decision-log.js.map +1 -1
  23. package/src/core/l0-fault-codes.d.ts +1 -1
  24. package/src/core/l0-fault-codes.js +1 -1
  25. package/src/core/l0-fault-codes.js.map +1 -1
  26. package/src/core/l1-doc.js +6 -0
  27. package/src/core/l1-doc.js.map +1 -1
  28. package/src/core/l1-rows.d.ts +13 -0
  29. package/src/core/l1-rows.js +24 -11
  30. package/src/core/l1-rows.js.map +1 -1
  31. package/src/core/load-rules.js +2 -0
  32. package/src/core/load-rules.js.map +1 -1
  33. package/src/core/log-stream.d.ts +32 -18
  34. package/src/core/log-stream.js +33 -19
  35. package/src/core/log-stream.js.map +1 -1
  36. package/src/core/log-streams.d.ts +38 -0
  37. package/src/core/log-streams.js +45 -0
  38. package/src/core/log-streams.js.map +1 -0
  39. package/src/core/main-sync-log.d.ts +2 -2
  40. package/src/core/main-sync-log.js +22 -15
  41. package/src/core/main-sync-log.js.map +1 -1
  42. package/src/core/main-sync-refresh.d.ts +3 -3
  43. package/src/core/main-sync-refresh.js +4 -4
  44. package/src/core/main-sync-refresh.js.map +1 -1
  45. package/src/core/rejection-log.d.ts +1 -1
  46. package/src/core/rejection-log.js +13 -12
  47. package/src/core/rejection-log.js.map +1 -1
  48. package/src/core/rules/feature-branch-guard.d.ts +11 -0
  49. package/src/core/rules/feature-branch-guard.js +20 -5
  50. package/src/core/rules/feature-branch-guard.js.map +1 -1
  51. package/src/core/rules/index.js +1 -0
  52. package/src/core/rules/index.js.map +1 -1
  53. package/src/core/rules/merged-branch-bash-guard.d.ts +12 -1
  54. package/src/core/rules/merged-branch-bash-guard.js +21 -6
  55. package/src/core/rules/merged-branch-bash-guard.js.map +1 -1
  56. package/src/core/rules/read-stale-guard.d.ts +11 -0
  57. package/src/core/rules/read-stale-guard.js +25 -10
  58. package/src/core/rules/read-stale-guard.js.map +1 -1
  59. package/src/core/rules/stale-main-bash-guard.d.ts +11 -0
  60. package/src/core/rules/stale-main-bash-guard.js +22 -7
  61. package/src/core/rules/stale-main-bash-guard.js.map +1 -1
  62. package/src/core/rules/whole-repo-build-guard.d.ts +88 -0
  63. package/src/core/rules/whole-repo-build-guard.js +176 -0
  64. package/src/core/rules/whole-repo-build-guard.js.map +1 -0
  65. package/src/core/rules/whole-repo-build-scan.d.ts +35 -0
  66. package/src/core/rules/whole-repo-build-scan.js +216 -0
  67. package/src/core/rules/whole-repo-build-scan.js.map +1 -0
  68. package/src/core/runner.js +34 -15
  69. package/src/core/runner.js.map +1 -1
  70. package/src/core/sync-main.d.ts +1 -1
  71. package/src/core/sync-main.js +2 -2
  72. package/src/core/sync-main.js.map +1 -1
  73. package/templates/ai-hook.sh +11 -8
  74. package/templates/guarantee-root.sh +10 -7
@@ -2,11 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SyncLogEvent = void 0;
4
4
  exports.logSyncEvent = logSyncEvent;
5
- exports.syncStderrLogPath = syncStderrLogPath;
5
+ exports.refresherChildStdioPath = refresherChildStdioPath;
6
6
  const tslib_1 = require("tslib");
7
7
  const fs = tslib_1.__importStar(require("fs"));
8
8
  const path = tslib_1.__importStar(require("path"));
9
9
  const rules_config_1 = require("@webpieces/rules-config");
10
+ const log_streams_1 = require("./log-streams");
10
11
  const to_error_1 = require("./to-error");
11
12
  const log_stream_1 = require("./log-stream");
12
13
  // The ASYNC log — observability for the detached background refresher (sync-main.ts) that writes
@@ -16,12 +17,14 @@ const log_stream_1 = require("./log-stream");
16
17
  // This log captures its lifecycle — SPAWN_ATTEMPT (parent side), then START / SKIP_INPROGRESS /
17
18
  // FINISH / ERROR (child side) — so we can tell whether the detached child never launched, was killed
18
19
  // mid-run (START with no FINISH), or threw. Writes to
19
- // `.webpieces/logs/<stream>guard-async-work.log`, where <stream> is LogStream's
20
- // `<sessionId>-<agentId|coordinator>-<hook>-` prefix (see
21
- // LOGS_STATE_DIR: every webpieces log lives under `logs/`, never beside the non-log state in `hooks/`).
22
- const LOG_FILE = 'guard-async-work.log';
23
- const LOG_FILE_PREV = 'guard-async-work.1.log';
24
- const STDERR_FILE = 'guard-async-work.stderr.log';
20
+ // `.webpieces/logs/async-refresh/<writer>.log`, where <writer> is LogStream's
21
+ // `<sessionId>-<agentId|coordinator>-<hook>` key (see LOGS_STATE_DIR and ASYNC_REFRESH_STREAM).
22
+ //
23
+ // THE CHILD'S RAW STDIO GOES TO THIS SAME FILE. It used to have a `.stderr.log` sibling, which was
24
+ // 0 bytes on every measured run — it is written to ONLY when the child dies before its own logging
25
+ // (a module-load failure, say), and that is precisely the moment you want the crash output sitting
26
+ // directly beneath the SPAWN_ATTEMPT that preceded it, in time order, rather than in a second file
27
+ // you have to think to open. Same cycle, same writer, one file.
25
28
  const MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded (mirrors decision-log)
26
29
  const MAX_DETAIL_LEN = 300;
27
30
  // Data-only record of one refresher lifecycle event (per CLAUDE.md: classes for data).
@@ -40,7 +43,7 @@ class SyncLogEvent {
40
43
  exports.SyncLogEvent = SyncLogEvent;
41
44
  /**
42
45
  * Append one tab-separated line per refresher event to
43
- * `.webpieces/logs/<stream>guard-async-work.log` (see LogStream for the prefix). `root` is
46
+ * `.webpieces/logs/async-refresh/<writer>.log` (see LogStream for the writer key). `root` is
44
47
  * the workspace root holding `.webpieces`. Swallows all errors — logging must never block or fail
45
48
  * the refresher (or the hook that spawns it).
46
49
  */
@@ -50,10 +53,10 @@ function logSyncEvent(root, event) {
50
53
  const timestamp = new Date().toISOString();
51
54
  // LOCAL scope: this is the refresher's own lifecycle trace for THIS worktree. One writer per
52
55
  // log, so its appends cannot interleave with another agent's.
53
- const logsDir = rules_config_1.dotWebpieces.logs(root);
56
+ const logsDir = rules_config_1.dotWebpieces.logsFile(root, log_streams_1.ASYNC_REFRESH_STREAM);
54
57
  fs.mkdirSync(logsDir, { recursive: true });
55
- const logPath = path.join(logsDir, log_stream_1.logStream.fileName(LOG_FILE));
56
- rotateLogFile(logPath, path.join(logsDir, log_stream_1.logStream.fileName(LOG_FILE_PREV)));
58
+ const logPath = path.join(logsDir, log_stream_1.logStream.writerFile('.log'));
59
+ rotateLogFile(logPath, path.join(logsDir, log_stream_1.logStream.writerFile('.1.log')));
57
60
  const line = [
58
61
  `[${timestamp}]`,
59
62
  event.phase,
@@ -68,12 +71,16 @@ function logSyncEvent(root, event) {
68
71
  void error;
69
72
  }
70
73
  }
71
- // Absolute path the detached child's stdout/stderr are redirected to (opened with fs.openSync(p,'a')
72
- // by the spawner), so even a crash BEFORE our own logging runs — e.g. a module-load failure — is
74
+ // The path the detached child's stdout/stderr are redirected to (opened with fs.openSync(p,'a') by
75
+ // the spawner), so even a crash BEFORE our own logging runs — e.g. a module-load failure — is
73
76
  // captured instead of vanishing into /dev/null. Callers must ensure the log dir exists first
74
77
  // (logSyncEvent's mkdir, called for SPAWN_ATTEMPT, does that).
75
- function syncStderrLogPath(root) {
76
- return rules_config_1.dotWebpieces.logsFile(root, log_stream_1.logStream.fileName(STDERR_FILE));
78
+ //
79
+ // This is THE SAME FILE logSyncEvent appends to — see the header. It is not a second stream and it
80
+ // no longer has a name of its own.
81
+ // webpieces-disable no-function-outside-class -- sibling of logSyncEvent in this module-scope writer; it must stay callable from a tree too broken to build a DI container
82
+ function refresherChildStdioPath(root) {
83
+ return rules_config_1.dotWebpieces.logsFile(root, log_streams_1.ASYNC_REFRESH_STREAM, log_stream_1.logStream.writerFile('.log'));
77
84
  }
78
85
  // Collapse newlines/tabs and cap length so one event is always one log line.
79
86
  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":"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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';\nimport { logStream } from './log-stream';\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\n// `.webpieces/logs/<stream>guard-async-work.log`, where <stream> is LogStream's\n// `<sessionId>-<agentId|coordinator>-<hook>-` prefix (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\n * `.webpieces/logs/<stream>guard-async-work.log` (see LogStream for the prefix). `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, logStream.fileName(LOG_FILE));\n rotateLogFile(logPath, path.join(logsDir, logStream.fileName(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, logStream.fileName(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"]}
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';\nimport { ASYNC_REFRESH_STREAM } from './log-streams';\n\nimport { toError } from './to-error';\nimport { logStream } from './log-stream';\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\n// `.webpieces/logs/async-refresh/<writer>.log`, where <writer> is LogStream's\n// `<sessionId>-<agentId|coordinator>-<hook>` key (see LOGS_STATE_DIR and ASYNC_REFRESH_STREAM).\n//\n// THE CHILD'S RAW STDIO GOES TO THIS SAME FILE. It used to have a `.stderr.log` sibling, which was\n// 0 bytes on every measured run it is written to ONLY when the child dies before its own logging\n// (a module-load failure, say), and that is precisely the moment you want the crash output sitting\n// directly beneath the SPAWN_ATTEMPT that preceded it, in time order, rather than in a second file\n// you have to think to open. Same cycle, same writer, one file.\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\n * `.webpieces/logs/async-refresh/<writer>.log` (see LogStream for the writer key). `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.logsFile(root, ASYNC_REFRESH_STREAM);\n fs.mkdirSync(logsDir, { recursive: true });\n\n const logPath = path.join(logsDir, logStream.writerFile('.log'));\n rotateLogFile(logPath, path.join(logsDir, logStream.writerFile('.1.log')));\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// The path the detached child's stdout/stderr are redirected to (opened with fs.openSync(p,'a') by\n// 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).\n//\n// This is THE SAME FILE logSyncEvent appends to — see the header. It is not a second stream and it\n// no longer has a name of its own.\n// webpieces-disable no-function-outside-class -- sibling of logSyncEvent in this module-scope writer; it must stay callable from a tree too broken to build a DI container\nexport function refresherChildStdioPath(root: string): string {\n return dotWebpieces.logsFile(root, ASYNC_REFRESH_STREAM, logStream.writerFile('.log'));\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"]}
@@ -15,14 +15,14 @@ export declare function refresherArgv(refresher: string, workspaceRoot: string,
15
15
  * the NEXT call. This is the first detached spawn in the codebase — every existing hook is synchronous.
16
16
  *
17
17
  * Observability: we log SPAWN_ATTEMPT here and the child logs START/FINISH/ERROR, all to
18
- * `.webpieces/logs/<stream>guard-async-work.log` (LogStream prefixes every name). The child's
18
+ * `.webpieces/logs/async-refresh/<writer>.log` (LogStream names every writer). The child's
19
19
  * stdout/stderr are redirected to a sibling file (not
20
- * /dev/null) so a crash before the child's own logging is still captured. If guard-async-work.log shows
20
+ * /dev/null) so a crash before the child's own logging is still captured. If the async-refresh stream shows
21
21
  * SPAWN_ATTEMPT but never START, the detached child was killed before it ran.
22
22
  *
23
23
  * THE IDENTITY GOES ON ARGV, and that is what makes the sentence above true. The child is a separate
24
24
  * node process whose `logStream` starts unidentified, so it used to write every line to the shared
25
- * `unknown-coordinator-hook-guard-async-work.log` while this parent wrote SPAWN_ATTEMPT to its own
25
+ * `unknown-coordinator-hook.log` while this parent wrote SPAWN_ATTEMPT to its own
26
26
  * prefixed stream. One cycle, two files — the SPAWN_ATTEMPT-without-START check then read as a
27
27
  * failure on every cycle even when the child ran perfectly — and every agent's child appending to
28
28
  * that one shared path is exactly the multi-writer tearing LogStream exists to remove. Passing
@@ -36,14 +36,14 @@ function refresherArgv(refresher, workspaceRoot, hangTimeoutMinutes, identity) {
36
36
  * the NEXT call. This is the first detached spawn in the codebase — every existing hook is synchronous.
37
37
  *
38
38
  * Observability: we log SPAWN_ATTEMPT here and the child logs START/FINISH/ERROR, all to
39
- * `.webpieces/logs/<stream>guard-async-work.log` (LogStream prefixes every name). The child's
39
+ * `.webpieces/logs/async-refresh/<writer>.log` (LogStream names every writer). The child's
40
40
  * stdout/stderr are redirected to a sibling file (not
41
- * /dev/null) so a crash before the child's own logging is still captured. If guard-async-work.log shows
41
+ * /dev/null) so a crash before the child's own logging is still captured. If the async-refresh stream shows
42
42
  * SPAWN_ATTEMPT but never START, the detached child was killed before it ran.
43
43
  *
44
44
  * THE IDENTITY GOES ON ARGV, and that is what makes the sentence above true. The child is a separate
45
45
  * node process whose `logStream` starts unidentified, so it used to write every line to the shared
46
- * `unknown-coordinator-hook-guard-async-work.log` while this parent wrote SPAWN_ATTEMPT to its own
46
+ * `unknown-coordinator-hook.log` while this parent wrote SPAWN_ATTEMPT to its own
47
47
  * prefixed stream. One cycle, two files — the SPAWN_ATTEMPT-without-START check then read as a
48
48
  * failure on every cycle even when the child ran perfectly — and every agent's child appending to
49
49
  * that one shared path is exactly the multi-writer tearing LogStream exists to remove. Passing
@@ -66,7 +66,7 @@ function triggerMainSyncRefresh(workspaceRoot, hangTimeoutMinutes = rules_config
66
66
  (0, main_sync_log_1.logSyncEvent)(workspaceRoot, new main_sync_log_1.SyncLogEvent('SPAWN_ATTEMPT', process.pid, '-', `refresher=${refresher}`));
67
67
  // Redirect the detached child's stdout+stderr to a file (not /dev/null) so an uncaught crash
68
68
  // before the child's own logging — e.g. a module-load failure — is still captured.
69
- const errFd = fs.openSync((0, main_sync_log_1.syncStderrLogPath)(workspaceRoot), 'a');
69
+ const errFd = fs.openSync((0, main_sync_log_1.refresherChildStdioPath)(workspaceRoot), 'a');
70
70
  const child = (0, child_process_1.spawn)(process.execPath, refresherArgv(refresher, workspaceRoot, hangTimeoutMinutes, log_stream_1.logStream.identity()), {
71
71
  detached: true,
72
72
  stdio: ['ignore', errFd, errFd],
@@ -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":";;AAeA,4EAEC;AAUD,sCAEC;AAuBD,wDAmCC;;AAvFD,iDAAsC;AACtC,+CAAyB;AACzB,mDAA6B;AAE7B,0DAAuE;AAEvE,yCAAqC;AACrC,mDAAgF;AAChF,6CAAyD;AAEzD,qGAAqG;AACrG,yEAAyE;AACzE,IAAI,gBAAgB,GAAG,KAAK,CAAC;AAE7B,8GAA8G;AAC9G,SAAgB,gCAAgC;IAC5C,gBAAgB,GAAG,KAAK,CAAC;AAC7B,CAAC;AAED;;;;;;GAMG;AACH,mHAAmH;AACnH,SAAgB,aAAa,CAAC,SAAiB,EAAE,aAAqB,EAAE,kBAA0B,EAAE,QAAwB;IACxH,OAAO,CAAC,SAAS,EAAE,aAAa,EAAE,MAAM,CAAC,kBAAkB,CAAC,EAAE,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;AACvH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;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,aAAa,CAAC,SAAS,EAAE,aAAa,EAAE,kBAAkB,EAAE,sBAAS,CAAC,QAAQ,EAAE,CAAC,EAAE;YACrH,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';\nimport { logStream, StreamIdentity } from './log-stream';\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 * The detached child's argv, spawner side. Positions here are offset by two from the child's\n * `process.argv` (node + script path), so this array's 3/4/5 are the child's 4/5/6 — which is what\n * `sync-main.ts:spawnerIdentity` reads. The two are pinned against each other by ONE test, because a\n * silent disagreement about these positions IS the bug being fixed: the child previously received no\n * identity at all and wrote every line to the shared `unknown-coordinator-hook-` stream.\n */\n// webpieces-disable no-function-outside-class -- argv builder for this module's spawn, matching its function shape\nexport function refresherArgv(refresher: string, workspaceRoot: string, hangTimeoutMinutes: number, identity: StreamIdentity): string[] {\n return [refresher, workspaceRoot, String(hangTimeoutMinutes), identity.sessionId, identity.agentId, identity.hook];\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/<stream>guard-async-work.log` (LogStream prefixes every name). The child's\n * 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 *\n * THE IDENTITY GOES ON ARGV, and that is what makes the sentence above true. The child is a separate\n * node process whose `logStream` starts unidentified, so it used to write every line to the shared\n * `unknown-coordinator-hook-guard-async-work.log` while this parent wrote SPAWN_ATTEMPT to its own\n * prefixed stream. One cycle, two files — the SPAWN_ATTEMPT-without-START check then read as a\n * failure on every cycle even when the child ran perfectly — and every agent's child appending to\n * that one shared path is exactly the multi-writer tearing LogStream exists to remove. Passing\n * session/agent/hook through (positional argv, alongside the root and timeout the child already\n * takes) puts the whole cycle back in ONE file.\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, refresherArgv(refresher, workspaceRoot, hangTimeoutMinutes, logStream.identity()), {\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
+ {"version":3,"file":"main-sync-refresh.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/main-sync-refresh.ts"],"names":[],"mappings":";;AAeA,4EAEC;AAUD,sCAEC;AAuBD,wDAmCC;;AAvFD,iDAAsC;AACtC,+CAAyB;AACzB,mDAA6B;AAE7B,0DAAuE;AAEvE,yCAAqC;AACrC,mDAAsF;AACtF,6CAAyD;AAEzD,qGAAqG;AACrG,yEAAyE;AACzE,IAAI,gBAAgB,GAAG,KAAK,CAAC;AAE7B,8GAA8G;AAC9G,SAAgB,gCAAgC;IAC5C,gBAAgB,GAAG,KAAK,CAAC;AAC7B,CAAC;AAED;;;;;;GAMG;AACH,mHAAmH;AACnH,SAAgB,aAAa,CAAC,SAAiB,EAAE,aAAqB,EAAE,kBAA0B,EAAE,QAAwB;IACxH,OAAO,CAAC,SAAS,EAAE,aAAa,EAAE,MAAM,CAAC,kBAAkB,CAAC,EAAE,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;AACvH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;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,uCAAuB,EAAC,aAAa,CAAC,EAAE,GAAG,CAAC,CAAC;QACvE,MAAM,KAAK,GAAG,IAAA,qBAAK,EAAC,OAAO,CAAC,QAAQ,EAAE,aAAa,CAAC,SAAS,EAAE,aAAa,EAAE,kBAAkB,EAAE,sBAAS,CAAC,QAAQ,EAAE,CAAC,EAAE;YACrH,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, refresherChildStdioPath } from './main-sync-log';\nimport { logStream, StreamIdentity } from './log-stream';\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 * The detached child's argv, spawner side. Positions here are offset by two from the child's\n * `process.argv` (node + script path), so this array's 3/4/5 are the child's 4/5/6 — which is what\n * `sync-main.ts:spawnerIdentity` reads. The two are pinned against each other by ONE test, because a\n * silent disagreement about these positions IS the bug being fixed: the child previously received no\n * identity at all and wrote every line to the shared `unknown-coordinator-hook-` stream.\n */\n// webpieces-disable no-function-outside-class -- argv builder for this module's spawn, matching its function shape\nexport function refresherArgv(refresher: string, workspaceRoot: string, hangTimeoutMinutes: number, identity: StreamIdentity): string[] {\n return [refresher, workspaceRoot, String(hangTimeoutMinutes), identity.sessionId, identity.agentId, identity.hook];\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/async-refresh/<writer>.log` (LogStream names every writer). The child's\n * 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 the async-refresh stream shows\n * SPAWN_ATTEMPT but never START, the detached child was killed before it ran.\n *\n * THE IDENTITY GOES ON ARGV, and that is what makes the sentence above true. The child is a separate\n * node process whose `logStream` starts unidentified, so it used to write every line to the shared\n * `unknown-coordinator-hook.log` while this parent wrote SPAWN_ATTEMPT to its own\n * prefixed stream. One cycle, two files — the SPAWN_ATTEMPT-without-START check then read as a\n * failure on every cycle even when the child ran perfectly — and every agent's child appending to\n * that one shared path is exactly the multi-writer tearing LogStream exists to remove. Passing\n * session/agent/hook through (positional argv, alongside the root and timeout the child already\n * takes) puts the whole cycle back in ONE file.\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(refresherChildStdioPath(workspaceRoot), 'a');\n const child = spawn(process.execPath, refresherArgv(refresher, workspaceRoot, hangTimeoutMinutes, logStream.identity()), {\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"]}
@@ -3,7 +3,7 @@ export declare function logRejection(toolKind: ToolKind, input: NormalizedToolIn
3
3
  /**
4
4
  * The rule names a block report cites — every `[<rule-name>] (` header it opens with. Exported because
5
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
6
+ * `rule=` field the `calls/` stream now carries (see InvocationLog.finish). Two scrapers would be
7
7
  * two answers to one question.
8
8
  */
9
9
  export declare function extractRuleNames(report: string): string[];
@@ -6,13 +6,15 @@ const tslib_1 = require("tslib");
6
6
  const fs = tslib_1.__importStar(require("fs"));
7
7
  const path = tslib_1.__importStar(require("path"));
8
8
  const rules_config_1 = require("@webpieces/rules-config");
9
+ const log_streams_1 = require("./log-streams");
9
10
  const log_stream_1 = require("./log-stream");
10
11
  const to_error_1 = require("./to-error");
11
12
  /**
12
- * The rejection index and its detail files, BOTH under `logs/`, both named by the same stream:
13
+ * The rejection index and its detail files, BOTH inside the `rejections/` stream directory, both
14
+ * named by the same writer key:
13
15
  *
14
- * logs/<sid>-<agent>-<hook>-hook-rejection.log ← the index (one line per block)
15
- * logs/<sid>-<agent>-<hook>-hook-rejection/ ← its detail dir, SAME base name
16
+ * logs/rejections/<sid>-<agent>-<hook>.log ← the index (one line per block)
17
+ * logs/rejections/<sid>-<agent>-<hook>/ ← its detail dir, SAME key
16
18
  * writeInfo-<epochMs>.md
17
19
  *
18
20
  * The detail files used to be `hooks/<YYYY-MM-DD>/writeInfo-<epochMs>.md` — a directory keyed only by
@@ -29,9 +31,6 @@ const to_error_1 = require("./to-error");
29
31
  * stream directory left empty. Every delete is `force: true` and failures are swallowed, so concurrent
30
32
  * agents racing the same week-old files is harmless.
31
33
  */
32
- const LOG_BASE = 'hook-rejection';
33
- const LOG_FILE = `${LOG_BASE}.log`;
34
- const LOG_FILE_PREV = `${LOG_BASE}.1.log`;
35
34
  const DETAIL_PREFIX = 'writeInfo-';
36
35
  const DETAIL_SUFFIX = '.md';
37
36
  const DETAIL_RE = /^writeInfo-(\d+)\.md$/;
@@ -49,20 +48,22 @@ function logRejection(toolKind, input, result, cwd) {
49
48
  const epochMs = String(now.getTime());
50
49
  // LOCAL scope — a rejection is this worktree's event. WHICH writer's event is answered by the
51
50
  // stream prefix, on the index AND on its detail directory, so neither can collide.
52
- const logsDir = rules_config_1.dotWebpieces.logs(root);
53
- const detailDirName = log_stream_1.logStream.fileName(LOG_BASE);
51
+ const logsDir = rules_config_1.dotWebpieces.logsFile(root, log_streams_1.REJECTIONS_STREAM);
52
+ // The detail directory is the writer key with NO extension, sitting beside the `.log` that
53
+ // indexes it — so index and details share one owner, one level down inside the stream dir.
54
+ const detailDirName = log_stream_1.logStream.writerFile('');
54
55
  const detailDir = path.join(logsDir, detailDirName);
55
56
  fs.mkdirSync(detailDir, { recursive: true });
56
57
  const relativePath = computeRelativePath(input.filePath, root);
57
58
  const ruleNames = extractRuleNames(result.report);
58
59
  const detailFileName = `${DETAIL_PREFIX}${epochMs}${DETAIL_SUFFIX}`;
59
- // Relative to `logs/` — the index's own directory — so the pointer resolves from exactly where
60
+ // Relative to `rejections/` — the index's own directory — so the pointer resolves from where
60
61
  // the reader found the line, and reads as the log's own name plus a file.
61
62
  const detailRelPath = `${detailDirName}/${detailFileName}`;
62
63
  const detail = buildDetailContent(timestamp, toolKind, relativePath, ruleNames, result.report, input);
63
64
  fs.writeFileSync(path.join(detailDir, detailFileName), detail);
64
- const logPath = path.join(logsDir, log_stream_1.logStream.fileName(LOG_FILE));
65
- rotateLogFile(logPath, path.join(logsDir, log_stream_1.logStream.fileName(LOG_FILE_PREV)));
65
+ const logPath = path.join(logsDir, log_stream_1.logStream.writerFile('.log'));
66
+ rotateLogFile(logPath, path.join(logsDir, log_stream_1.logStream.writerFile('.1.log')));
66
67
  // APPEND-ONLY, and `fault=` is spelled exactly as the L0 sh log spells it. A fault-S storm
67
68
  // previously landed here as a couple of lines attributed to whatever rule the report happened
68
69
  // to cite, with nothing anywhere identifying L0 as the cause. (WHO made the call is answered by
@@ -88,7 +89,7 @@ function computeRelativePath(filePath, cwd) {
88
89
  /**
89
90
  * The rule names a block report cites — every `[<rule-name>] (` header it opens with. Exported because
90
91
  * two audit streams need the same answer from the same regex: this file's rejection index, and the
91
- * `rule=` field guard-invocations.log now carries (see InvocationLog.finish). Two scrapers would be
92
+ * `rule=` field the `calls/` stream now carries (see InvocationLog.finish). Two scrapers would be
92
93
  * two answers to one question.
93
94
  */
94
95
  // 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.
@@ -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';\nimport { logStream } from './log-stream';\nimport { toError } from './to-error';\n\n/**\n * The rejection index and its detail files, BOTH under `logs/`, both named by the same stream:\n *\n * logs/<sid>-<agent>-<hook>-hook-rejection.log ← the index (one line per block)\n * logs/<sid>-<agent>-<hook>-hook-rejection/ ← its detail dir, SAME base name\n * writeInfo-<epochMs>.md\n *\n * The detail files used to be `hooks/<YYYY-MM-DD>/writeInfo-<epochMs>.md` — a directory keyed only by\n * the DATE, shared by every writer in the tree, written with a raw `writeFileSync`. Two agents blocked\n * in the same millisecond produced the same path and one silently overwrote the other's evidence. The\n * comment that licensed it claimed a per-worktree log has exactly one writer; LogStream's class\n * comment is the measurement that disproves it (parallel PreToolUse hooks, non-isolated subagents and\n * multiple Claude Code windows all share a tree). Naming the directory after the LOG THAT POINTS AT IT\n * gives it the same one-owner-by-construction property the log itself has, and makes the pointer\n * readable: the index line, the directory and the file are one name.\n *\n * RETENTION is therefore filename-based, not directory-date-based: there is no date level left, so\n * `DetailPruner` parses `<epochMs>` out of each `writeInfo-*.md` (no `stat` call) and removes any\n * stream directory left empty. Every delete is `force: true` and failures are swallowed, so concurrent\n * agents racing the same week-old files is harmless.\n */\nconst LOG_BASE = 'hook-rejection';\nconst LOG_FILE = `${LOG_BASE}.log`;\nconst LOG_FILE_PREV = `${LOG_BASE}.1.log`;\nconst DETAIL_PREFIX = 'writeInfo-';\nconst DETAIL_SUFFIX = '.md';\nconst DETAIL_RE = /^writeInfo-(\\d+)\\.md$/;\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\n // LOCAL scope — a rejection is this worktree's event. WHICH writer's event is answered by the\n // stream prefix, on the index AND on its detail directory, so neither can collide.\n const logsDir = dotWebpieces.logs(root);\n const detailDirName = logStream.fileName(LOG_BASE);\n const detailDir = path.join(logsDir, detailDirName);\n fs.mkdirSync(detailDir, { recursive: true });\n\n const relativePath = computeRelativePath(input.filePath, root);\n const ruleNames = extractRuleNames(result.report);\n const detailFileName = `${DETAIL_PREFIX}${epochMs}${DETAIL_SUFFIX}`;\n // Relative to `logs/` — the index's own directory — so the pointer resolves from exactly where\n // the reader found the line, and reads as the log's own name plus a file.\n const detailRelPath = `${detailDirName}/${detailFileName}`;\n\n const detail = buildDetailContent(timestamp, toolKind, relativePath, ruleNames, result.report, input);\n fs.writeFileSync(path.join(detailDir, detailFileName), detail);\n\n const logPath = path.join(logsDir, logStream.fileName(LOG_FILE));\n rotateLogFile(logPath, path.join(logsDir, logStream.fileName(LOG_FILE_PREV)));\n\n // APPEND-ONLY, and `fault=` is spelled exactly as the L0 sh log spells it. A fault-S storm\n // previously landed here as a couple of lines attributed to whatever rule the report happened\n // to cite, with nothing anywhere identifying L0 as the cause. (WHO made the call is answered by\n // the filename, which logStream prefixes with session/agent/hook — not by a column here.)\n const logLine = `[${timestamp}]\\t${toolKind}\\t${relativePath}\\t[${ruleNames.join(',')}]\\t${detailRelPath}\\tfault=${result.fault}\\n`;\n fs.appendFileSync(logPath, logLine);\n\n detailPruner.prune(logsDir, 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\n/**\n * The 7-day sweep over rejection DETAIL files.\n *\n * A class rather than four module-scope helpers because the four belong together: `prune` is the\n * policy and the other three are the swallow-everything filesystem calls it is built from, none of\n * which means anything on its own.\n *\n * The age comes out of the FILENAME, not out of `stat` — the epoch millis are already in the name, so\n * a sweep costs one `readdir` per stream directory and no syscall per file. (The old scheme could\n * delete a whole `hooks/<YYYY-MM-DD>/` in one recursive `rm` because the date WAS the directory; the\n * directory is now the WRITER, which never expires, so expiry moved down one level.)\n *\n * IDEMPOTENT AND RACE-TOLERANT ON PURPOSE. Several agents sweep the same directory concurrently, so\n * two processes will regularly try to unlink the same week-old file and to rmdir the same emptied\n * directory. `force: true` makes an already-gone target a no-op, an rmdir of a still-populated (or\n * already-removed) directory fails harmlessly, and every error is swallowed: the loser of the race\n * does nothing and reports nothing, which is the correct outcome — the file is gone either way.\n */\nclass DetailPruner {\n prune(logsDir: string, maxAgeDays: number): void {\n const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;\n for (const dirName of this.readdirOrEmpty(logsDir)) {\n const dir = path.join(logsDir, dirName);\n if (!this.isDirectory(dir)) continue;\n const names = this.readdirOrEmpty(dir);\n let removed = 0;\n for (const name of names) {\n const match = DETAIL_RE.exec(name);\n if (match === null) continue;\n if (Number(match[1]) >= cutoff) continue;\n this.removeFile(path.join(dir, name));\n removed += 1;\n }\n // Only a directory we just emptied is a candidate, and the removal is a NON-recursive rmdir\n // on purpose: it fails harmlessly if a concurrent writer dropped a fresh detail in between,\n // so the sweep can never take a file it did not decide was expired.\n if (removed > 0 && removed === names.length) this.removeEmptyDir(dir);\n }\n }\n\n private readdirOrEmpty(dir: string): string[] {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.readdirSync(dir);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return [];\n }\n }\n\n private isDirectory(target: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.statSync(target).isDirectory();\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return false;\n }\n }\n\n // `force: true` is what makes the concurrent sweep harmless: a file another agent already deleted\n // is a no-op rather than an ENOENT.\n private removeFile(target: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.rmSync(target, { force: true });\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n // NON-recursive by design — see prune(). This must refuse a directory that is not empty.\n private removeEmptyDir(target: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.rmdirSync(target);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n}\n\nconst detailPruner = new DetailPruner();\n"]}
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';\nimport { REJECTIONS_STREAM } from './log-streams';\n\nimport type { ToolKind, NormalizedToolInput, BlockedResult } from './types';\nimport { logStream } from './log-stream';\nimport { toError } from './to-error';\n\n/**\n * The rejection index and its detail files, BOTH inside the `rejections/` stream directory, both\n * named by the same writer key:\n *\n * logs/rejections/<sid>-<agent>-<hook>.log ← the index (one line per block)\n * logs/rejections/<sid>-<agent>-<hook>/ ← its detail dir, SAME key\n * writeInfo-<epochMs>.md\n *\n * The detail files used to be `hooks/<YYYY-MM-DD>/writeInfo-<epochMs>.md` — a directory keyed only by\n * the DATE, shared by every writer in the tree, written with a raw `writeFileSync`. Two agents blocked\n * in the same millisecond produced the same path and one silently overwrote the other's evidence. The\n * comment that licensed it claimed a per-worktree log has exactly one writer; LogStream's class\n * comment is the measurement that disproves it (parallel PreToolUse hooks, non-isolated subagents and\n * multiple Claude Code windows all share a tree). Naming the directory after the LOG THAT POINTS AT IT\n * gives it the same one-owner-by-construction property the log itself has, and makes the pointer\n * readable: the index line, the directory and the file are one name.\n *\n * RETENTION is therefore filename-based, not directory-date-based: there is no date level left, so\n * `DetailPruner` parses `<epochMs>` out of each `writeInfo-*.md` (no `stat` call) and removes any\n * stream directory left empty. Every delete is `force: true` and failures are swallowed, so concurrent\n * agents racing the same week-old files is harmless.\n */\nconst DETAIL_PREFIX = 'writeInfo-';\nconst DETAIL_SUFFIX = '.md';\nconst DETAIL_RE = /^writeInfo-(\\d+)\\.md$/;\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\n // LOCAL scope — a rejection is this worktree's event. WHICH writer's event is answered by the\n // stream prefix, on the index AND on its detail directory, so neither can collide.\n const logsDir = dotWebpieces.logsFile(root, REJECTIONS_STREAM);\n // The detail directory is the writer key with NO extension, sitting beside the `.log` that\n // indexes it — so index and details share one owner, one level down inside the stream dir.\n const detailDirName = logStream.writerFile('');\n const detailDir = path.join(logsDir, detailDirName);\n fs.mkdirSync(detailDir, { recursive: true });\n\n const relativePath = computeRelativePath(input.filePath, root);\n const ruleNames = extractRuleNames(result.report);\n const detailFileName = `${DETAIL_PREFIX}${epochMs}${DETAIL_SUFFIX}`;\n // Relative to `rejections/` — the index's own directory — so the pointer resolves from where\n // the reader found the line, and reads as the log's own name plus a file.\n const detailRelPath = `${detailDirName}/${detailFileName}`;\n\n const detail = buildDetailContent(timestamp, toolKind, relativePath, ruleNames, result.report, input);\n fs.writeFileSync(path.join(detailDir, detailFileName), detail);\n\n const logPath = path.join(logsDir, logStream.writerFile('.log'));\n rotateLogFile(logPath, path.join(logsDir, logStream.writerFile('.1.log')));\n\n // APPEND-ONLY, and `fault=` is spelled exactly as the L0 sh log spells it. A fault-S storm\n // previously landed here as a couple of lines attributed to whatever rule the report happened\n // to cite, with nothing anywhere identifying L0 as the cause. (WHO made the call is answered by\n // the filename, which logStream prefixes with session/agent/hook — not by a column here.)\n const logLine = `[${timestamp}]\\t${toolKind}\\t${relativePath}\\t[${ruleNames.join(',')}]\\t${detailRelPath}\\tfault=${result.fault}\\n`;\n fs.appendFileSync(logPath, logLine);\n\n detailPruner.prune(logsDir, 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 the `calls/` stream 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\n/**\n * The 7-day sweep over rejection DETAIL files.\n *\n * A class rather than four module-scope helpers because the four belong together: `prune` is the\n * policy and the other three are the swallow-everything filesystem calls it is built from, none of\n * which means anything on its own.\n *\n * The age comes out of the FILENAME, not out of `stat` — the epoch millis are already in the name, so\n * a sweep costs one `readdir` per stream directory and no syscall per file. (The old scheme could\n * delete a whole `hooks/<YYYY-MM-DD>/` in one recursive `rm` because the date WAS the directory; the\n * directory is now the WRITER, which never expires, so expiry moved down one level.)\n *\n * IDEMPOTENT AND RACE-TOLERANT ON PURPOSE. Several agents sweep the same directory concurrently, so\n * two processes will regularly try to unlink the same week-old file and to rmdir the same emptied\n * directory. `force: true` makes an already-gone target a no-op, an rmdir of a still-populated (or\n * already-removed) directory fails harmlessly, and every error is swallowed: the loser of the race\n * does nothing and reports nothing, which is the correct outcome — the file is gone either way.\n */\nclass DetailPruner {\n prune(logsDir: string, maxAgeDays: number): void {\n const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;\n for (const dirName of this.readdirOrEmpty(logsDir)) {\n const dir = path.join(logsDir, dirName);\n if (!this.isDirectory(dir)) continue;\n const names = this.readdirOrEmpty(dir);\n let removed = 0;\n for (const name of names) {\n const match = DETAIL_RE.exec(name);\n if (match === null) continue;\n if (Number(match[1]) >= cutoff) continue;\n this.removeFile(path.join(dir, name));\n removed += 1;\n }\n // Only a directory we just emptied is a candidate, and the removal is a NON-recursive rmdir\n // on purpose: it fails harmlessly if a concurrent writer dropped a fresh detail in between,\n // so the sweep can never take a file it did not decide was expired.\n if (removed > 0 && removed === names.length) this.removeEmptyDir(dir);\n }\n }\n\n private readdirOrEmpty(dir: string): string[] {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.readdirSync(dir);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return [];\n }\n }\n\n private isDirectory(target: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.statSync(target).isDirectory();\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return false;\n }\n }\n\n // `force: true` is what makes the concurrent sweep harmless: a file another agent already deleted\n // is a no-op rather than an ENOENT.\n private removeFile(target: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.rmSync(target, { force: true });\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n // NON-recursive by design — see prune(). This must refuse a directory that is not empty.\n private removeEmptyDir(target: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.rmdirSync(target);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n}\n\nconst detailPruner = new DetailPruner();\n"]}
@@ -26,6 +26,17 @@ export declare class FeatureBranchGuardRule extends FileRuleBase<FeatureBranchGu
26
26
  readonly fixHint: FixHint;
27
27
  check(ctx: FileContext): readonly Violation[];
28
28
  private cacheSummary;
29
+ /**
30
+ * The guard could not ESTABLISH the state it judges on, so it judged nothing.
31
+ *
32
+ * A sibling of allow() rather than a reason string passed to it, because the difference has to
33
+ * reach the LOG as a value: `ALLOW_FAIL_OPEN` vs `ALLOW`. It was previously a `' (fail-open)'`
34
+ * suffix on the free-text reason, which meant an abstention and a real approval were the same
35
+ * verdict and the abstentions could not be counted — so nobody could tell whether these guards
36
+ * were protecting anything or quietly standing down. Never block on data you could not
37
+ * establish; but say out loud, in a field, that you did not establish it.
38
+ */
39
+ private failOpen;
29
40
  private allow;
30
41
  private block;
31
42
  private logDecision;
@@ -9,6 +9,7 @@ const fix_hint_1 = require("../fix-hint");
9
9
  const to_error_1 = require("../to-error");
10
10
  const main_sync_refresh_1 = require("../main-sync-refresh");
11
11
  const decision_log_1 = require("../decision-log");
12
+ const l0_fault_codes_1 = require("../l0-fault-codes");
12
13
  const merged_branch_message_1 = require("./merged-branch-message");
13
14
  const tree_recovery_1 = require("./tree-recovery");
14
15
  /**
@@ -44,7 +45,7 @@ class FeatureBranchGuardRule extends rule_base_1.FileRuleBase {
44
45
  const branch = this.currentBranch(ctx.workspaceRoot);
45
46
  // Can't determine branch (e.g. not a git repo) → don't block. Fail-open.
46
47
  if (branch === null)
47
- return this.allow(ctx, branch, 'branch-undeterminable (fail-open)');
48
+ return this.failOpen(ctx, branch, 'branch-undeterminable');
48
49
  // State 1: on main — synchronous, no cache needed.
49
50
  if (branch === 'main') {
50
51
  return this.block(ctx, branch, 'on-main', this.onMainMessage());
@@ -55,14 +56,14 @@ class FeatureBranchGuardRule extends rule_base_1.FileRuleBase {
55
56
  // No cache yet (first edit of the session) → allow; the refresh we just spawned populates it
56
57
  // for the next call. Fail-open: never block on missing data.
57
58
  if (status === null)
58
- return this.allow(ctx, branch, 'no-sync-cache (fail-open)', 'cache=none');
59
+ return this.failOpen(ctx, branch, 'no-sync-cache', 'cache=none');
59
60
  const cache = this.cacheSummary(status);
60
61
  // BELT-AND-BRACES since the cache became branch-keyed: we looked this entry up BY `branch`, so
61
62
  // a mismatch now means the map's key and the entry's own `branch` field disagree — a shape bug,
62
63
  // not a normal state. Kept (rather than deleted) so such a bug degrades to an allow instead of
63
64
  // blocking on another branch's signals. Unreachable in normal operation.
64
65
  if (status.branch !== branch)
65
- return this.allow(ctx, branch, 'stale-cross-branch-cache (fail-open)', cache);
66
+ return this.failOpen(ctx, branch, 'stale-cross-branch-cache', cache);
66
67
  // State 2: this feature branch was already merged into main.
67
68
  if (status.branchAlreadyMerged) {
68
69
  const pr = status.mergedPr !== '' ? status.mergedPr : '?';
@@ -88,16 +89,30 @@ class FeatureBranchGuardRule extends rule_base_1.FileRuleBase {
88
89
  // Log + return for the allow path. Centralizes the decision-log call so every exit of check()
89
90
  // is recorded with its reason + the async cache it read (this is the audit trail for "why didn't
90
91
  // the guard fire?"). `cache` is the summary of the main-sync-status.json that drove the decision.
92
+ /**
93
+ * The guard could not ESTABLISH the state it judges on, so it judged nothing.
94
+ *
95
+ * A sibling of allow() rather than a reason string passed to it, because the difference has to
96
+ * reach the LOG as a value: `ALLOW_FAIL_OPEN` vs `ALLOW`. It was previously a `' (fail-open)'`
97
+ * suffix on the free-text reason, which meant an abstention and a real approval were the same
98
+ * verdict and the abstentions could not be counted — so nobody could tell whether these guards
99
+ * were protecting anything or quietly standing down. Never block on data you could not
100
+ * establish; but say out loud, in a field, that you did not establish it.
101
+ */
102
+ failOpen(ctx, branch, reason, cache = '-') {
103
+ this.logDecision(ctx, branch, 'ALLOW_FAIL_OPEN', reason, cache);
104
+ return [];
105
+ }
91
106
  allow(ctx, branch, reason, cache = '-') {
92
107
  this.logDecision(ctx, branch, 'ALLOW', reason, cache);
93
108
  return [];
94
109
  }
95
110
  block(ctx, branch, reason, message, cache = '-') {
96
- this.logDecision(ctx, branch, 'BLOCK', reason, cache);
111
+ this.logDecision(ctx, branch, 'BLOCK_AI_CURE', reason, cache);
97
112
  return [new types_1.Violation(1, ctx.relativePath, message)];
98
113
  }
99
114
  logDecision(ctx, branch, verdict, reason, cache) {
100
- (0, decision_log_1.logGuardDecision)(ctx.workspaceRoot, new decision_log_1.GuardDecision('feature-branch-guard', ctx.tool, ctx.relativePath, branch ?? 'unknown', verdict, reason, cache));
115
+ (0, decision_log_1.logGuardDecision)(ctx.workspaceRoot, new decision_log_1.GuardDecision('feature-branch-guard', ctx.tool, ctx.relativePath, branch ?? 'unknown', verdict, reason, cache, l0_fault_codes_1.L0_FAULT_NONE, decision_log_1.MATRIX_L2));
101
116
  }
102
117
  currentBranch(workspaceRoot) {
103
118
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
@@ -1 +1 @@
1
- {"version":3,"file":"feature-branch-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/feature-branch-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from 'child_process';\n\nimport {\n FeatureBranchGuardConfig,\n DEFAULT_HANG_TIMEOUT_MINUTES,\n readMainSyncStatus,\n squashRecoverySteps,\n MainSyncStatus,\n SyncFlowGuidance,\n} from '@webpieces/rules-config';\n\nimport type { FileContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { FileRuleBase } from '../rule-base';\nimport { FixHint, Option } from '../fix-hint';\nimport { toError } from '../to-error';\nimport { triggerMainSyncRefresh } from '../main-sync-refresh';\nimport { logGuardDecision, GuardDecision } from '../decision-log';\nimport { MergedBranchMessage } from './merged-branch-message';\nimport { TreeRecovery } from './tree-recovery';\n\n/**\n * Comprehensive \"are you on a proper feature branch?\" guard — the single rule that blocks edits when\n * the branch isn't a healthy place to work. Four states, in priority order:\n * 1. On main (checked SYNCHRONOUSLY here) → block: create a feature branch.\n * 2. Branch already merged into main (merged PR) → block: your work is in main, branch off fresh.\n * 3. No fork point with origin/main → block: squash onto a new branch.\n * 4. origin/main moved & touches your files → block: merge main first.\n * States 2–4 are PRECOMPUTED into `.webpieces/main-sync-status.json` by the detached refresher, so\n * this check does NO network git (only a fast local `git rev-parse` for state 1). On every call it\n * fire-and-forget spawns the refresher so the NEXT call is fresh. Runs in the GUARDS hook (it's a\n * hookGuard); file-scoped, so only Write/Edit/MultiEdit are guarded — Bash passes through so the AI\n * can still run `pnpm wp-start-upsert-pr` and the rest of the recovery flow.\n */\nexport class FeatureBranchGuardRule extends FileRuleBase<FeatureBranchGuardConfig> {\n constructor(config: FeatureBranchGuardConfig) { super(config, 'feature-branch-guard'); }\n\n readonly description = 'Block edits unless you are on a proper feature branch (not main, not already-merged, forked, in sync with main).';\n override readonly files = ['**/*'];\n override readonly defaultOptions = {\n branchNamingConvention: '{whoami}/{featurename}',\n hangTimeoutMinutes: DEFAULT_HANG_TIMEOUT_MINUTES,\n };\n readonly fixHint = new FixHint(\n 'You are not on a clean, up-to-date feature branch.',\n 'You must be on a clean, up-to-date feature branch to edit code. Pick one:',\n [\n new Option('On main → create a feature branch. Already merged → branch off fresh main.', true),\n new Option('main moved/conflicts, NO PR yet → `pnpm wp-start-update` (merge), `/wp-merge` (resolve), `pnpm wp-finish-update`. An OPEN PR? then you MUST use `pnpm wp-start-upsert-pr` → `/wp-merge` → `pnpm wp-finish-upsert-pr` (the merge rewrites the branch, so the PR must be re-pointed in the same run). Never mix a start from one pair with a finish from the other.'),\n new Option('Disable in webpieces.config.json under feature-branch-guard (mode OFF) if intentional.'),\n ],\n );\n\n check(ctx: FileContext): readonly Violation[] {\n // Only files inside the workspace root — guard has no jurisdiction, nothing worth logging.\n if (ctx.relativePath.startsWith('..')) return [];\n\n const branch = this.currentBranch(ctx.workspaceRoot);\n // Can't determine branch (e.g. not a git repo) → don't block. Fail-open.\n if (branch === null) return this.allow(ctx, branch, 'branch-undeterminable (fail-open)');\n\n // State 1: on main — synchronous, no cache needed.\n if (branch === 'main') {\n return this.block(ctx, branch, 'on-main', this.onMainMessage());\n }\n\n // Keep the cache warm for the next call. Detached; never blocks this edit.\n triggerMainSyncRefresh(ctx.workspaceRoot, this.config.hangTimeoutMinutes ?? DEFAULT_HANG_TIMEOUT_MINUTES);\n\n const status = readMainSyncStatus(ctx.workspaceRoot, branch);\n // No cache yet (first edit of the session) → allow; the refresh we just spawned populates it\n // for the next call. Fail-open: never block on missing data.\n if (status === null) return this.allow(ctx, branch, 'no-sync-cache (fail-open)', 'cache=none');\n\n const cache = this.cacheSummary(status);\n // BELT-AND-BRACES since the cache became branch-keyed: we looked this entry up BY `branch`, so\n // a mismatch now means the map's key and the entry's own `branch` field disagree — a shape bug,\n // not a normal state. Kept (rather than deleted) so such a bug degrades to an allow instead of\n // blocking on another branch's signals. Unreachable in normal operation.\n if (status.branch !== branch) return this.allow(ctx, branch, 'stale-cross-branch-cache (fail-open)', cache);\n\n // State 2: this feature branch was already merged into main.\n if (status.branchAlreadyMerged) {\n const pr = status.mergedPr !== '' ? status.mergedPr : '?';\n const merged = this.alreadyMergedMessage(ctx.workspaceRoot, branch, status.mergedPr);\n return this.block(ctx, branch, `already-merged PR#${pr}`, merged, cache);\n }\n // State 3: no fork point — main was merged into the branch.\n if (!status.hasForkPoint) {\n return this.block(ctx, branch, 'no-fork-point', this.noForkPointMessage(branch), cache);\n }\n // State 4: origin/main moved and touches files you changed.\n if (status.conflict) {\n return this.block(ctx, branch, 'main-moved-conflict', this.conflictMessage(status.conflictFiles, status.openPr), cache);\n }\n return this.allow(ctx, branch, 'clean-feature-branch', cache);\n }\n\n // One-line summary of the async-written cache that drove this decision, for the SYNC log — so a\n // wrong allow/block is traceable to the exact (possibly stale) main-sync-status.json read.\n private cacheSummary(status: MainSyncStatus): string {\n const merged = status.branchAlreadyMerged ? `PR#${status.mergedPr !== '' ? status.mergedPr : '?'}` : 'no';\n return `cache=${status.branch} merged=${merged} fork=${String(status.hasForkPoint)} conflict=${String(status.conflict)} ts=${status.timestamp}`;\n }\n\n // Log + return for the allow path. Centralizes the decision-log call so every exit of check()\n // is recorded with its reason + the async cache it read (this is the audit trail for \"why didn't\n // the guard fire?\"). `cache` is the summary of the main-sync-status.json that drove the decision.\n private allow(ctx: FileContext, branch: string | null, reason: string, cache: string = '-'): readonly Violation[] {\n this.logDecision(ctx, branch, 'ALLOW', reason, cache);\n return [];\n }\n\n private block(ctx: FileContext, branch: string, reason: string, message: string, cache: string = '-'): readonly Violation[] {\n this.logDecision(ctx, branch, 'BLOCK', reason, cache);\n return [new V(1, ctx.relativePath, message)];\n }\n\n private logDecision(ctx: FileContext, branch: string | null, verdict: 'ALLOW' | 'BLOCK', reason: string, cache: string): void {\n logGuardDecision(\n ctx.workspaceRoot,\n new GuardDecision('feature-branch-guard', ctx.tool, ctx.relativePath, branch ?? 'unknown', verdict, reason, cache),\n );\n }\n\n private currentBranch(workspaceRoot: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: workspaceRoot,\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n private onMainMessage(): string {\n const convention = this.config.branchNamingConvention ?? '{whoami}/{featurename}';\n return [\n 'You should not be working on main.',\n 'Do a `git pull origin main` to get latest, then create a feature branch based on the naming convention.',\n `Branch naming convention (from webpieces.config.json): ${convention}`,\n 'Example: git checkout -b ' + convention.replace(/<[^>]+>/g, 'value'),\n ].join('\\n');\n }\n\n // Shared with read-stale-guard, which blocks READS in this same state — see MergedBranchMessage.\n // The tree kind picks the flavour of the cure: a dead LINKED WORKTREE is told to open a new\n // worktree off origin/main and reap this one; the primary clone is told to branch off origin/main.\n private alreadyMergedMessage(workspaceRoot: string, branch: string, mergedPr: string): string {\n return new MergedBranchMessage(workspaceRoot).forEdits(\n branch, mergedPr, new TreeRecovery().kindOf(workspaceRoot), workspaceRoot,\n );\n }\n\n private conflictMessage(conflictFiles: readonly string[], openPr: string): string {\n const files = conflictFiles.length > 0\n ? conflictFiles.map((f: string): string => ` - ${f}`).join('\\n')\n : ' (see git diff)';\n const header = [\n 'origin/main moved and touched files you also changed since your fork point:',\n files,\n '',\n ];\n // Steer EARLY: if a PR already tracks this branch, the update-only flow would just fail-fast\n // (a 3-point update strands the PR on the old branch generation), so recommend ONLY the PR\n // flow and don't waste the AI's tokens on wp-start-update.\n const guidance = new SyncFlowGuidance();\n // An OPEN PR removes the choice, so print ONLY the PR flow here — showing the update-only flow\n // as if it were an option just burns tokens on a command that fail-fasts.\n if (openPr !== '') {\n return header\n .concat([\n `An OPEN PR (#${openPr}) already tracks this branch, so the PR flow is the ONLY option`,\n 'here — it re-merges main AND re-points the PR in the same run:',\n '',\n ])\n .concat(guidance.prFlow())\n .concat(['', ...guidance.whyPrForcesFlowB()])\n .join('\\n');\n }\n return header\n .concat([\n 'You must merge main in before editing further. No PR is open for this branch, so flow A',\n 'below is the one to use — but use flow B the moment a PR exists:',\n '',\n ])\n .concat(guidance.flows())\n .join('\\n');\n }\n\n private noForkPointMessage(branch: string): string {\n return [\n 'No fork point with origin/main — main appears to have been merged into this branch,',\n 'so a clean squash-merge is impossible. A human must redo the work on a fresh branch:',\n '',\n ...squashRecoverySteps(branch),\n ].join('\\n');\n }\n}\n"]}
1
+ {"version":3,"file":"feature-branch-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/feature-branch-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from 'child_process';\n\nimport {\n FeatureBranchGuardConfig,\n DEFAULT_HANG_TIMEOUT_MINUTES,\n readMainSyncStatus,\n squashRecoverySteps,\n MainSyncStatus,\n SyncFlowGuidance,\n} from '@webpieces/rules-config';\n\nimport type { FileContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { FileRuleBase } from '../rule-base';\nimport { FixHint, Option } from '../fix-hint';\nimport { toError } from '../to-error';\nimport { triggerMainSyncRefresh } from '../main-sync-refresh';\nimport { logGuardDecision, GuardDecision, Verdict, MATRIX_L2 } from '../decision-log';\nimport { L0_FAULT_NONE } from '../l0-fault-codes';\nimport { MergedBranchMessage } from './merged-branch-message';\nimport { TreeRecovery } from './tree-recovery';\n\n/**\n * Comprehensive \"are you on a proper feature branch?\" guard — the single rule that blocks edits when\n * the branch isn't a healthy place to work. Four states, in priority order:\n * 1. On main (checked SYNCHRONOUSLY here) → block: create a feature branch.\n * 2. Branch already merged into main (merged PR) → block: your work is in main, branch off fresh.\n * 3. No fork point with origin/main → block: squash onto a new branch.\n * 4. origin/main moved & touches your files → block: merge main first.\n * States 2–4 are PRECOMPUTED into `.webpieces/main-sync-status.json` by the detached refresher, so\n * this check does NO network git (only a fast local `git rev-parse` for state 1). On every call it\n * fire-and-forget spawns the refresher so the NEXT call is fresh. Runs in the GUARDS hook (it's a\n * hookGuard); file-scoped, so only Write/Edit/MultiEdit are guarded — Bash passes through so the AI\n * can still run `pnpm wp-start-upsert-pr` and the rest of the recovery flow.\n */\nexport class FeatureBranchGuardRule extends FileRuleBase<FeatureBranchGuardConfig> {\n constructor(config: FeatureBranchGuardConfig) { super(config, 'feature-branch-guard'); }\n\n readonly description = 'Block edits unless you are on a proper feature branch (not main, not already-merged, forked, in sync with main).';\n override readonly files = ['**/*'];\n override readonly defaultOptions = {\n branchNamingConvention: '{whoami}/{featurename}',\n hangTimeoutMinutes: DEFAULT_HANG_TIMEOUT_MINUTES,\n };\n readonly fixHint = new FixHint(\n 'You are not on a clean, up-to-date feature branch.',\n 'You must be on a clean, up-to-date feature branch to edit code. Pick one:',\n [\n new Option('On main → create a feature branch. Already merged → branch off fresh main.', true),\n new Option('main moved/conflicts, NO PR yet → `pnpm wp-start-update` (merge), `/wp-merge` (resolve), `pnpm wp-finish-update`. An OPEN PR? then you MUST use `pnpm wp-start-upsert-pr` → `/wp-merge` → `pnpm wp-finish-upsert-pr` (the merge rewrites the branch, so the PR must be re-pointed in the same run). Never mix a start from one pair with a finish from the other.'),\n new Option('Disable in webpieces.config.json under feature-branch-guard (mode OFF) if intentional.'),\n ],\n );\n\n check(ctx: FileContext): readonly Violation[] {\n // Only files inside the workspace root — guard has no jurisdiction, nothing worth logging.\n if (ctx.relativePath.startsWith('..')) return [];\n\n const branch = this.currentBranch(ctx.workspaceRoot);\n // Can't determine branch (e.g. not a git repo) → don't block. Fail-open.\n if (branch === null) return this.failOpen(ctx, branch, 'branch-undeterminable');\n\n // State 1: on main — synchronous, no cache needed.\n if (branch === 'main') {\n return this.block(ctx, branch, 'on-main', this.onMainMessage());\n }\n\n // Keep the cache warm for the next call. Detached; never blocks this edit.\n triggerMainSyncRefresh(ctx.workspaceRoot, this.config.hangTimeoutMinutes ?? DEFAULT_HANG_TIMEOUT_MINUTES);\n\n const status = readMainSyncStatus(ctx.workspaceRoot, branch);\n // No cache yet (first edit of the session) → allow; the refresh we just spawned populates it\n // for the next call. Fail-open: never block on missing data.\n if (status === null) return this.failOpen(ctx, branch, 'no-sync-cache', 'cache=none');\n\n const cache = this.cacheSummary(status);\n // BELT-AND-BRACES since the cache became branch-keyed: we looked this entry up BY `branch`, so\n // a mismatch now means the map's key and the entry's own `branch` field disagree — a shape bug,\n // not a normal state. Kept (rather than deleted) so such a bug degrades to an allow instead of\n // blocking on another branch's signals. Unreachable in normal operation.\n if (status.branch !== branch) return this.failOpen(ctx, branch, 'stale-cross-branch-cache', cache);\n\n // State 2: this feature branch was already merged into main.\n if (status.branchAlreadyMerged) {\n const pr = status.mergedPr !== '' ? status.mergedPr : '?';\n const merged = this.alreadyMergedMessage(ctx.workspaceRoot, branch, status.mergedPr);\n return this.block(ctx, branch, `already-merged PR#${pr}`, merged, cache);\n }\n // State 3: no fork point — main was merged into the branch.\n if (!status.hasForkPoint) {\n return this.block(ctx, branch, 'no-fork-point', this.noForkPointMessage(branch), cache);\n }\n // State 4: origin/main moved and touches files you changed.\n if (status.conflict) {\n return this.block(ctx, branch, 'main-moved-conflict', this.conflictMessage(status.conflictFiles, status.openPr), cache);\n }\n return this.allow(ctx, branch, 'clean-feature-branch', cache);\n }\n\n // One-line summary of the async-written cache that drove this decision, for the SYNC log — so a\n // wrong allow/block is traceable to the exact (possibly stale) main-sync-status.json read.\n private cacheSummary(status: MainSyncStatus): string {\n const merged = status.branchAlreadyMerged ? `PR#${status.mergedPr !== '' ? status.mergedPr : '?'}` : 'no';\n return `cache=${status.branch} merged=${merged} fork=${String(status.hasForkPoint)} conflict=${String(status.conflict)} ts=${status.timestamp}`;\n }\n\n // Log + return for the allow path. Centralizes the decision-log call so every exit of check()\n // is recorded with its reason + the async cache it read (this is the audit trail for \"why didn't\n // the guard fire?\"). `cache` is the summary of the main-sync-status.json that drove the decision.\n /**\n * The guard could not ESTABLISH the state it judges on, so it judged nothing.\n *\n * A sibling of allow() rather than a reason string passed to it, because the difference has to\n * reach the LOG as a value: `ALLOW_FAIL_OPEN` vs `ALLOW`. It was previously a `' (fail-open)'`\n * suffix on the free-text reason, which meant an abstention and a real approval were the same\n * verdict and the abstentions could not be counted — so nobody could tell whether these guards\n * were protecting anything or quietly standing down. Never block on data you could not\n * establish; but say out loud, in a field, that you did not establish it.\n */\n private failOpen(ctx: FileContext, branch: string | null, reason: string, cache: string = '-'): readonly Violation[] {\n this.logDecision(ctx, branch, 'ALLOW_FAIL_OPEN', reason, cache);\n return [];\n }\n\n private allow(ctx: FileContext, branch: string | null, reason: string, cache: string = '-'): readonly Violation[] {\n this.logDecision(ctx, branch, 'ALLOW', reason, cache);\n return [];\n }\n\n private block(ctx: FileContext, branch: string, reason: string, message: string, cache: string = '-'): readonly Violation[] {\n this.logDecision(ctx, branch, 'BLOCK_AI_CURE', reason, cache);\n return [new V(1, ctx.relativePath, message)];\n }\n\n private logDecision(ctx: FileContext, branch: string | null, verdict: Verdict, reason: string, cache: string): void {\n logGuardDecision(\n ctx.workspaceRoot,\n new GuardDecision('feature-branch-guard', ctx.tool, ctx.relativePath, branch ?? 'unknown', verdict, reason, cache, L0_FAULT_NONE, MATRIX_L2),\n );\n }\n\n private currentBranch(workspaceRoot: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: workspaceRoot,\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n private onMainMessage(): string {\n const convention = this.config.branchNamingConvention ?? '{whoami}/{featurename}';\n return [\n 'You should not be working on main.',\n 'Do a `git pull origin main` to get latest, then create a feature branch based on the naming convention.',\n `Branch naming convention (from webpieces.config.json): ${convention}`,\n 'Example: git checkout -b ' + convention.replace(/<[^>]+>/g, 'value'),\n ].join('\\n');\n }\n\n // Shared with read-stale-guard, which blocks READS in this same state — see MergedBranchMessage.\n // The tree kind picks the flavour of the cure: a dead LINKED WORKTREE is told to open a new\n // worktree off origin/main and reap this one; the primary clone is told to branch off origin/main.\n private alreadyMergedMessage(workspaceRoot: string, branch: string, mergedPr: string): string {\n return new MergedBranchMessage(workspaceRoot).forEdits(\n branch, mergedPr, new TreeRecovery().kindOf(workspaceRoot), workspaceRoot,\n );\n }\n\n private conflictMessage(conflictFiles: readonly string[], openPr: string): string {\n const files = conflictFiles.length > 0\n ? conflictFiles.map((f: string): string => ` - ${f}`).join('\\n')\n : ' (see git diff)';\n const header = [\n 'origin/main moved and touched files you also changed since your fork point:',\n files,\n '',\n ];\n // Steer EARLY: if a PR already tracks this branch, the update-only flow would just fail-fast\n // (a 3-point update strands the PR on the old branch generation), so recommend ONLY the PR\n // flow and don't waste the AI's tokens on wp-start-update.\n const guidance = new SyncFlowGuidance();\n // An OPEN PR removes the choice, so print ONLY the PR flow here — showing the update-only flow\n // as if it were an option just burns tokens on a command that fail-fasts.\n if (openPr !== '') {\n return header\n .concat([\n `An OPEN PR (#${openPr}) already tracks this branch, so the PR flow is the ONLY option`,\n 'here — it re-merges main AND re-points the PR in the same run:',\n '',\n ])\n .concat(guidance.prFlow())\n .concat(['', ...guidance.whyPrForcesFlowB()])\n .join('\\n');\n }\n return header\n .concat([\n 'You must merge main in before editing further. No PR is open for this branch, so flow A',\n 'below is the one to use — but use flow B the moment a PR exists:',\n '',\n ])\n .concat(guidance.flows())\n .join('\\n');\n }\n\n private noForkPointMessage(branch: string): string {\n return [\n 'No fork point with origin/main — main appears to have been merged into this branch,',\n 'so a clean squash-merge is impossible. A human must redo the work on a fresh branch:',\n '',\n ...squashRecoverySteps(branch),\n ].join('\\n');\n }\n}\n"]}
@@ -24,5 +24,6 @@ exports.builtInRuleNames = [
24
24
  'read-stale-guard',
25
25
  'merged-branch-bash-guard',
26
26
  'stale-main-bash-guard',
27
+ 'whole-repo-build-guard',
27
28
  ];
28
29
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/index.ts"],"names":[],"mappings":";;;AAAa,QAAA,gBAAgB,GAAsB;IAC/C,gBAAgB;IAChB,iBAAiB;IACjB,gBAAgB;IAChB,oBAAoB;IACpB,aAAa;IACb,gBAAgB;IAChB,qBAAqB;IACrB,yBAAyB;IACzB,qBAAqB;IACrB,sBAAsB;IACtB,qBAAqB;IACrB,eAAe;IACf,8BAA8B;IAC9B,uBAAuB;IACvB,2BAA2B;IAC3B,yBAAyB;IACzB,gBAAgB;IAChB,4BAA4B;IAC5B,sBAAsB;IACtB,kBAAkB;IAClB,0BAA0B;IAC1B,uBAAuB;CAC1B,CAAC","sourcesContent":["export const builtInRuleNames: readonly string[] = [\n 'no-any-unknown',\n 'no-implicit-any',\n 'max-file-lines',\n 'validate-ts-in-src',\n 'no-js-files',\n 'no-destructure',\n 'require-return-type',\n 'no-unmanaged-exceptions',\n 'catch-error-pattern',\n 'throw-cause-required',\n 'no-symbol-di-tokens',\n 'no-custom-css',\n 'no-process-exit-outside-main',\n 'branch-creation-guard',\n 'pr-creation-or-push-guard',\n 'merge-in-progress-guard',\n 'pr-merge-guard',\n 'redirect-how-to-merge-main',\n 'feature-branch-guard',\n 'read-stale-guard',\n 'merged-branch-bash-guard',\n 'stale-main-bash-guard',\n];\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/index.ts"],"names":[],"mappings":";;;AAAa,QAAA,gBAAgB,GAAsB;IAC/C,gBAAgB;IAChB,iBAAiB;IACjB,gBAAgB;IAChB,oBAAoB;IACpB,aAAa;IACb,gBAAgB;IAChB,qBAAqB;IACrB,yBAAyB;IACzB,qBAAqB;IACrB,sBAAsB;IACtB,qBAAqB;IACrB,eAAe;IACf,8BAA8B;IAC9B,uBAAuB;IACvB,2BAA2B;IAC3B,yBAAyB;IACzB,gBAAgB;IAChB,4BAA4B;IAC5B,sBAAsB;IACtB,kBAAkB;IAClB,0BAA0B;IAC1B,uBAAuB;IACvB,wBAAwB;CAC3B,CAAC","sourcesContent":["export const builtInRuleNames: readonly string[] = [\n 'no-any-unknown',\n 'no-implicit-any',\n 'max-file-lines',\n 'validate-ts-in-src',\n 'no-js-files',\n 'no-destructure',\n 'require-return-type',\n 'no-unmanaged-exceptions',\n 'catch-error-pattern',\n 'throw-cause-required',\n 'no-symbol-di-tokens',\n 'no-custom-css',\n 'no-process-exit-outside-main',\n 'branch-creation-guard',\n 'pr-creation-or-push-guard',\n 'merge-in-progress-guard',\n 'pr-merge-guard',\n 'redirect-how-to-merge-main',\n 'feature-branch-guard',\n 'read-stale-guard',\n 'merged-branch-bash-guard',\n 'stale-main-bash-guard',\n 'whole-repo-build-guard',\n];\n"]}