failproofai 0.0.2-beta.7 → 0.0.2-beta.8

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 (97) hide show
  1. package/.next/standalone/.claude/settings.json +316 -0
  2. package/.next/standalone/.failproofai/policies/workflow-policies.mjs +62 -0
  3. package/.next/standalone/.failproofai/policies-config.json +39 -0
  4. package/.next/standalone/.next/BUILD_ID +1 -1
  5. package/.next/standalone/.next/build-manifest.json +3 -3
  6. package/.next/standalone/.next/prerender-manifest.json +3 -3
  7. package/.next/standalone/.next/required-server-files.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  9. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  10. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  14. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  15. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  16. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  17. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  22. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  23. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  24. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  25. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  26. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  27. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  28. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  29. package/.next/standalone/.next/server/app/index.html +1 -1
  30. package/.next/standalone/.next/server/app/index.rsc +15 -15
  31. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  32. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  33. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  34. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  35. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  36. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  37. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  39. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  40. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  43. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  46. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/chunks/[root-of-the-server]__02nt~6d._.js +1 -1
  53. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  54. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__05zi2mt._.js → [root-of-the-server]__0.t2266._.js} +2 -2
  55. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
  56. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
  57. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
  58. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
  59. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0okos0k._.js +2 -2
  60. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0kkt_9z._.js → [root-of-the-server]__0pjorff._.js} +2 -2
  61. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +1 -1
  62. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
  63. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
  64. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
  65. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
  67. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  68. package/.next/standalone/.next/server/pages/404.html +2 -2
  69. package/.next/standalone/.next/server/pages/500.html +1 -1
  70. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  72. package/.next/standalone/.next/static/chunks/{0q7atesxo-36k.js → 04xfyqyhdxbxz.js} +1 -1
  73. package/.next/standalone/.next/static/chunks/{0e76l4~hq_sei.js → 07g0rbtaux_1t.js} +1 -1
  74. package/.next/standalone/.next/static/chunks/{02u4v.k5amfah.js → 0a_xh94bt.y0j.js} +1 -1
  75. package/.next/standalone/.next/static/chunks/{0suauczjqzn07.js → 0j752uotyfvjh.js} +1 -1
  76. package/.next/standalone/.next/static/chunks/{0ltx5i0xv85_s.js → 0qi0ubup__3pj.js} +1 -1
  77. package/.next/standalone/.next/static/chunks/{0w.rtg9.m8dk-.js → 0xyvis4r_y.8o.js} +2 -2
  78. package/.next/standalone/.next/static/chunks/{0bkizbynk9via.js → 0zfyfi1suoteq.js} +1 -1
  79. package/.next/standalone/.next/static/chunks/{13jdpvk~s2da8.js → 121a-0zn-knuy.js} +1 -1
  80. package/.next/standalone/CHANGELOG.md +14 -0
  81. package/.next/standalone/dist/cli.mjs +34 -20
  82. package/.next/standalone/docs/built-in-policies.mdx +17 -1
  83. package/.next/standalone/docs/configuration.mdx +1 -1
  84. package/.next/standalone/docs/custom-policies.mdx +3 -3
  85. package/.next/standalone/package.json +1 -1
  86. package/.next/standalone/server.js +1 -1
  87. package/.next/standalone/src/hooks/custom-hooks-loader.ts +12 -5
  88. package/.next/standalone/src/hooks/handler.ts +9 -3
  89. package/.next/standalone/src/hooks/policy-evaluator.ts +20 -16
  90. package/dist/cli.mjs +34 -20
  91. package/package.json +1 -1
  92. package/src/hooks/custom-hooks-loader.ts +12 -5
  93. package/src/hooks/handler.ts +9 -3
  94. package/src/hooks/policy-evaluator.ts +20 -16
  95. /package/.next/standalone/.next/static/{Opbai6exOQP2W488FWmr6 → itedhTSyIDln6TUf41j5X}/_buildManifest.js +0 -0
  96. /package/.next/standalone/.next/static/{Opbai6exOQP2W488FWmr6 → itedhTSyIDln6TUf41j5X}/_clientMiddlewareManifest.js +0 -0
  97. /package/.next/standalone/.next/static/{Opbai6exOQP2W488FWmr6 → itedhTSyIDln6TUf41j5X}/_ssgManifest.js +0 -0
@@ -116,7 +116,7 @@ export interface LoadAllResult {
116
116
  * 3. User convention: ~/.failproofai/policies/*policies.{js,mjs,ts} (alphabetical)
117
117
  *
118
118
  * Each file is loaded independently (fail-open per file).
119
- * Convention hooks are tagged with __conventionSource so the handler can distinguish them.
119
+ * Convention hooks are tagged with __conventionScope so the handler can build scoped prefixes.
120
120
  */
121
121
  export async function loadAllCustomHooks(
122
122
  customPoliciesPath: string | undefined,
@@ -181,16 +181,23 @@ export async function loadAllCustomHooks(
181
181
  );
182
182
  }
183
183
 
184
- // Tag convention hooks so the handler can register them with a "convention/" prefix.
185
- // Track by object reference (not name) to avoid mis-tagging an explicit custom hook
186
- // that happens to share the same name as a convention hook.
184
+ // Tag convention hooks with their scope so the handler can build scoped prefixes.
185
+ // Build a name→scope map from conventionSources, then tag by object reference
186
+ // to avoid mis-tagging an explicit custom hook that shares the same name.
187
+ const hookNameToScope = new Map<string, string>();
188
+ for (const source of conventionSources) {
189
+ for (const name of source.hookNames) {
190
+ hookNameToScope.set(name, source.scope);
191
+ }
192
+ }
187
193
  const conventionHookRefs = new Set<CustomHook>();
188
194
  for (const hook of allHooks.slice(hooksBeforeConvention)) {
189
195
  conventionHookRefs.add(hook);
190
196
  }
191
197
  for (const hook of allHooks) {
192
198
  if (conventionHookRefs.has(hook)) {
193
- (hook as CustomHook & { __conventionSource?: boolean }).__conventionSource = true;
199
+ (hook as CustomHook & { __conventionScope?: string }).__conventionScope =
200
+ hookNameToScope.get(hook.name) ?? "project";
194
201
  }
195
202
  }
196
203
 
@@ -78,8 +78,9 @@ export async function handleHookEvent(eventType: string): Promise<number> {
78
78
 
79
79
  for (const hook of customHooksList) {
80
80
  const hookName = hook.name;
81
- const isConvention = (hook as CustomHook & { __conventionSource?: boolean }).__conventionSource === true;
82
- const prefix = isConvention ? "convention" : "custom";
81
+ const conventionScope = (hook as CustomHook & { __conventionScope?: string }).__conventionScope;
82
+ const isConvention = !!conventionScope;
83
+ const prefix = isConvention ? `.failproofai-${conventionScope}` : "custom";
83
84
  const fn: PolicyFunction = async (ctx): Promise<PolicyResult> => {
84
85
  try {
85
86
  const result = await Promise.race([
@@ -98,6 +99,7 @@ export async function handleHookEvent(eventType: string): Promise<number> {
98
99
  error_type: isTimeout ? "timeout" : "exception",
99
100
  event_type: eventType,
100
101
  is_convention_policy: isConvention,
102
+ convention_scope: conventionScope ?? null,
101
103
  });
102
104
  return { decision: "allow" };
103
105
  }
@@ -170,7 +172,10 @@ export async function handleHookEvent(eventType: string): Promise<number> {
170
172
  if (result.decision === "deny" || result.decision === "instruct") {
171
173
  try {
172
174
  const isCustomHook = result.policyName?.startsWith("custom/") ?? false;
173
- const isConventionPolicy = result.policyName?.startsWith("convention/") ?? false;
175
+ const isConventionPolicy = result.policyName?.startsWith(".failproofai-") ?? false;
176
+ const conventionScope = isConventionPolicy
177
+ ? result.policyName!.match(/^\.failproofai-(project|user)\//)?.[1] ?? null
178
+ : null;
174
179
  const hasCustomParams =
175
180
  !isCustomHook && !isConventionPolicy && !!(result.policyName && config.policyParams?.[result.policyName]);
176
181
  const paramKeysOverridden = hasCustomParams
@@ -184,6 +189,7 @@ export async function handleHookEvent(eventType: string): Promise<number> {
184
189
  decision: result.decision,
185
190
  is_custom_hook: isCustomHook,
186
191
  is_convention_policy: isConventionPolicy,
192
+ convention_scope: conventionScope,
187
193
  has_custom_params: hasCustomParams,
188
194
  param_keys_overridden: paramKeysOverridden,
189
195
  });
@@ -56,9 +56,8 @@ export async function evaluatePolicies(
56
56
  session,
57
57
  };
58
58
 
59
- // Track the first instruct result (accumulated, does not short-circuit)
60
- let instructPolicyName: string | null = null;
61
- let instructReason: string | null = null;
59
+ // Track all instruct results (accumulated, does not short-circuit)
60
+ const instructEntries: Array<{ policyName: string; reason: string }> = [];
62
61
 
63
62
  // Track informational messages from allow decisions (with policy attribution)
64
63
  const allowEntries: Array<{ policyName: string; reason: string }> = [];
@@ -142,14 +141,14 @@ export async function evaluatePolicies(
142
141
  };
143
142
  }
144
143
 
145
- // Accumulate first instruct (does not short-circuit — later policies can still deny)
146
- if (result.decision === "instruct" && !instructPolicyName) {
147
- instructPolicyName = policy.name;
148
- instructReason = appendHint(
144
+ // Accumulate all instruct results (does not short-circuit — later policies can still deny)
145
+ if (result.decision === "instruct") {
146
+ const reason = appendHint(
149
147
  result.reason ?? `Instruction from policy: ${policy.name}`,
150
148
  config?.policyParams?.[policy.name]?.hint,
151
149
  );
152
- hookLogInfo(`instruct by "${policy.name}": ${instructReason}`);
150
+ instructEntries.push({ policyName: policy.name, reason });
151
+ hookLogInfo(`instruct by "${policy.name}": ${reason}`);
153
152
  }
154
153
 
155
154
  // Accumulate informational messages from allow decisions
@@ -158,17 +157,21 @@ export async function evaluatePolicies(
158
157
  }
159
158
  }
160
159
 
161
- // No deny — check if we accumulated an instruct
162
- if (instructPolicyName && instructReason) {
160
+ // No deny — check if we accumulated any instructs
161
+ if (instructEntries.length > 0) {
162
+ const combined = instructEntries.map((e) => e.reason).join("\n");
163
+ const policyNames = instructEntries.map((e) => e.policyName);
164
+
163
165
  if (eventType === "Stop") {
164
166
  // Stop hook: exitCode 2 blocks Claude from stopping.
165
167
  // Reason goes to stderr so Claude Code receives it as context.
166
168
  return {
167
169
  exitCode: 2,
168
170
  stdout: "",
169
- stderr: instructReason,
170
- policyName: instructPolicyName,
171
- reason: instructReason,
171
+ stderr: combined,
172
+ policyName: policyNames[0],
173
+ policyNames,
174
+ reason: combined,
172
175
  decision: "instruct",
173
176
  };
174
177
  }
@@ -176,15 +179,16 @@ export async function evaluatePolicies(
176
179
  const response = {
177
180
  hookSpecificOutput: {
178
181
  hookEventName: eventType,
179
- additionalContext: `Instruction from failproofai: ${instructReason}`,
182
+ additionalContext: `Instruction from failproofai: ${combined}`,
180
183
  },
181
184
  };
182
185
  return {
183
186
  exitCode: 0,
184
187
  stdout: JSON.stringify(response),
185
188
  stderr: "",
186
- policyName: instructPolicyName,
187
- reason: instructReason,
189
+ policyName: policyNames[0],
190
+ policyNames,
191
+ reason: combined,
188
192
  decision: "instruct",
189
193
  };
190
194
  }
package/dist/cli.mjs CHANGED
@@ -1402,8 +1402,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
1402
1402
  toolInput,
1403
1403
  session
1404
1404
  };
1405
- let instructPolicyName = null;
1406
- let instructReason = null;
1405
+ const instructEntries = [];
1407
1406
  const allowEntries = [];
1408
1407
  for (const policy of policies) {
1409
1408
  const schema = POLICY_PARAMS_MAP.get(policy.name);
@@ -1471,38 +1470,43 @@ async function evaluatePolicies(eventType, payload, session, config) {
1471
1470
  decision: "deny"
1472
1471
  };
1473
1472
  }
1474
- if (result.decision === "instruct" && !instructPolicyName) {
1475
- instructPolicyName = policy.name;
1476
- instructReason = appendHint(result.reason ?? `Instruction from policy: ${policy.name}`, config?.policyParams?.[policy.name]?.hint);
1477
- hookLogInfo(`instruct by "${policy.name}": ${instructReason}`);
1473
+ if (result.decision === "instruct") {
1474
+ const reason = appendHint(result.reason ?? `Instruction from policy: ${policy.name}`, config?.policyParams?.[policy.name]?.hint);
1475
+ instructEntries.push({ policyName: policy.name, reason });
1476
+ hookLogInfo(`instruct by "${policy.name}": ${reason}`);
1478
1477
  }
1479
1478
  if (result.decision === "allow" && result.reason) {
1480
1479
  allowEntries.push({ policyName: policy.name, reason: result.reason });
1481
1480
  }
1482
1481
  }
1483
- if (instructPolicyName && instructReason) {
1482
+ if (instructEntries.length > 0) {
1483
+ const combined = instructEntries.map((e) => e.reason).join(`
1484
+ `);
1485
+ const policyNames = instructEntries.map((e) => e.policyName);
1484
1486
  if (eventType === "Stop") {
1485
1487
  return {
1486
1488
  exitCode: 2,
1487
1489
  stdout: "",
1488
- stderr: instructReason,
1489
- policyName: instructPolicyName,
1490
- reason: instructReason,
1490
+ stderr: combined,
1491
+ policyName: policyNames[0],
1492
+ policyNames,
1493
+ reason: combined,
1491
1494
  decision: "instruct"
1492
1495
  };
1493
1496
  }
1494
1497
  const response = {
1495
1498
  hookSpecificOutput: {
1496
1499
  hookEventName: eventType,
1497
- additionalContext: `Instruction from failproofai: ${instructReason}`
1500
+ additionalContext: `Instruction from failproofai: ${combined}`
1498
1501
  }
1499
1502
  };
1500
1503
  return {
1501
1504
  exitCode: 0,
1502
1505
  stdout: JSON.stringify(response),
1503
1506
  stderr: "",
1504
- policyName: instructPolicyName,
1505
- reason: instructReason,
1507
+ policyName: policyNames[0],
1508
+ policyNames,
1509
+ reason: combined,
1506
1510
  decision: "instruct"
1507
1511
  };
1508
1512
  }
@@ -1762,13 +1766,19 @@ async function loadAllCustomHooks(customPoliciesPath, opts) {
1762
1766
  if (projectFiles.length > 0 || userFiles.length > 0) {
1763
1767
  hookLogInfo(`convention policies: ${projectFiles.length} project file(s), ${userFiles.length} user file(s), ${conventionCount} hook(s)`);
1764
1768
  }
1769
+ const hookNameToScope = new Map;
1770
+ for (const source of conventionSources) {
1771
+ for (const name of source.hookNames) {
1772
+ hookNameToScope.set(name, source.scope);
1773
+ }
1774
+ }
1765
1775
  const conventionHookRefs = new Set;
1766
1776
  for (const hook of allHooks.slice(hooksBeforeConvention)) {
1767
1777
  conventionHookRefs.add(hook);
1768
1778
  }
1769
1779
  for (const hook of allHooks) {
1770
1780
  if (conventionHookRefs.has(hook)) {
1771
- hook.__conventionSource = true;
1781
+ hook.__conventionScope = hookNameToScope.get(hook.name) ?? "project";
1772
1782
  }
1773
1783
  }
1774
1784
  return { hooks: allHooks, conventionSources };
@@ -1904,7 +1914,7 @@ var init_hook_activity_store = __esm(() => {
1904
1914
  });
1905
1915
 
1906
1916
  // package.json
1907
- var version2 = "0.0.2-beta.7";
1917
+ var version2 = "0.0.2-beta.8";
1908
1918
  var init_package = () => {};
1909
1919
 
1910
1920
  // src/posthog-key.ts
@@ -2071,8 +2081,9 @@ async function handleHookEvent(eventType) {
2071
2081
  const conventionHookNames = new Set(loadResult.conventionSources.flatMap((s) => s.hookNames));
2072
2082
  for (const hook of customHooksList) {
2073
2083
  const hookName = hook.name;
2074
- const isConvention = hook.__conventionSource === true;
2075
- const prefix = isConvention ? "convention" : "custom";
2084
+ const conventionScope = hook.__conventionScope;
2085
+ const isConvention = !!conventionScope;
2086
+ const prefix = isConvention ? `.failproofai-${conventionScope}` : "custom";
2076
2087
  const fn = async (ctx) => {
2077
2088
  try {
2078
2089
  const result2 = await Promise.race([
@@ -2088,7 +2099,8 @@ async function handleHookEvent(eventType) {
2088
2099
  hook_name: hookName,
2089
2100
  error_type: isTimeout ? "timeout" : "exception",
2090
2101
  event_type: eventType,
2091
- is_convention_policy: isConvention
2102
+ is_convention_policy: isConvention,
2103
+ convention_scope: conventionScope ?? null
2092
2104
  });
2093
2105
  return { decision: "allow" };
2094
2106
  }
@@ -2143,7 +2155,8 @@ async function handleHookEvent(eventType) {
2143
2155
  if (result.decision === "deny" || result.decision === "instruct") {
2144
2156
  try {
2145
2157
  const isCustomHook = result.policyName?.startsWith("custom/") ?? false;
2146
- const isConventionPolicy = result.policyName?.startsWith("convention/") ?? false;
2158
+ const isConventionPolicy = result.policyName?.startsWith(".failproofai-") ?? false;
2159
+ const conventionScope = isConventionPolicy ? result.policyName.match(/^\.failproofai-(project|user)\//)?.[1] ?? null : null;
2147
2160
  const hasCustomParams = !isCustomHook && !isConventionPolicy && !!(result.policyName && config.policyParams?.[result.policyName]);
2148
2161
  const paramKeysOverridden = hasCustomParams ? Object.keys(config.policyParams[result.policyName]) : [];
2149
2162
  const distinctId = getInstanceId();
@@ -2154,6 +2167,7 @@ async function handleHookEvent(eventType) {
2154
2167
  decision: result.decision,
2155
2168
  is_custom_hook: isCustomHook,
2156
2169
  is_convention_policy: isConventionPolicy,
2170
+ convention_scope: conventionScope,
2157
2171
  has_custom_params: hasCustomParams,
2158
2172
  param_keys_overridden: paramKeysOverridden
2159
2173
  });
@@ -3185,7 +3199,7 @@ import { realpathSync as realpathSync2 } from "fs";
3185
3199
  import { dirname as dirname5, resolve as resolve8 } from "path";
3186
3200
  import { fileURLToPath as fileURLToPath2 } from "url";
3187
3201
  // package.json
3188
- var version = "0.0.2-beta.7";
3202
+ var version = "0.0.2-beta.8";
3189
3203
 
3190
3204
  // bin/failproofai.mjs
3191
3205
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "0.0.2-beta.7",
3
+ "version": "0.0.2-beta.8",
4
4
  "description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs"
@@ -116,7 +116,7 @@ export interface LoadAllResult {
116
116
  * 3. User convention: ~/.failproofai/policies/*policies.{js,mjs,ts} (alphabetical)
117
117
  *
118
118
  * Each file is loaded independently (fail-open per file).
119
- * Convention hooks are tagged with __conventionSource so the handler can distinguish them.
119
+ * Convention hooks are tagged with __conventionScope so the handler can build scoped prefixes.
120
120
  */
121
121
  export async function loadAllCustomHooks(
122
122
  customPoliciesPath: string | undefined,
@@ -181,16 +181,23 @@ export async function loadAllCustomHooks(
181
181
  );
182
182
  }
183
183
 
184
- // Tag convention hooks so the handler can register them with a "convention/" prefix.
185
- // Track by object reference (not name) to avoid mis-tagging an explicit custom hook
186
- // that happens to share the same name as a convention hook.
184
+ // Tag convention hooks with their scope so the handler can build scoped prefixes.
185
+ // Build a name→scope map from conventionSources, then tag by object reference
186
+ // to avoid mis-tagging an explicit custom hook that shares the same name.
187
+ const hookNameToScope = new Map<string, string>();
188
+ for (const source of conventionSources) {
189
+ for (const name of source.hookNames) {
190
+ hookNameToScope.set(name, source.scope);
191
+ }
192
+ }
187
193
  const conventionHookRefs = new Set<CustomHook>();
188
194
  for (const hook of allHooks.slice(hooksBeforeConvention)) {
189
195
  conventionHookRefs.add(hook);
190
196
  }
191
197
  for (const hook of allHooks) {
192
198
  if (conventionHookRefs.has(hook)) {
193
- (hook as CustomHook & { __conventionSource?: boolean }).__conventionSource = true;
199
+ (hook as CustomHook & { __conventionScope?: string }).__conventionScope =
200
+ hookNameToScope.get(hook.name) ?? "project";
194
201
  }
195
202
  }
196
203
 
@@ -78,8 +78,9 @@ export async function handleHookEvent(eventType: string): Promise<number> {
78
78
 
79
79
  for (const hook of customHooksList) {
80
80
  const hookName = hook.name;
81
- const isConvention = (hook as CustomHook & { __conventionSource?: boolean }).__conventionSource === true;
82
- const prefix = isConvention ? "convention" : "custom";
81
+ const conventionScope = (hook as CustomHook & { __conventionScope?: string }).__conventionScope;
82
+ const isConvention = !!conventionScope;
83
+ const prefix = isConvention ? `.failproofai-${conventionScope}` : "custom";
83
84
  const fn: PolicyFunction = async (ctx): Promise<PolicyResult> => {
84
85
  try {
85
86
  const result = await Promise.race([
@@ -98,6 +99,7 @@ export async function handleHookEvent(eventType: string): Promise<number> {
98
99
  error_type: isTimeout ? "timeout" : "exception",
99
100
  event_type: eventType,
100
101
  is_convention_policy: isConvention,
102
+ convention_scope: conventionScope ?? null,
101
103
  });
102
104
  return { decision: "allow" };
103
105
  }
@@ -170,7 +172,10 @@ export async function handleHookEvent(eventType: string): Promise<number> {
170
172
  if (result.decision === "deny" || result.decision === "instruct") {
171
173
  try {
172
174
  const isCustomHook = result.policyName?.startsWith("custom/") ?? false;
173
- const isConventionPolicy = result.policyName?.startsWith("convention/") ?? false;
175
+ const isConventionPolicy = result.policyName?.startsWith(".failproofai-") ?? false;
176
+ const conventionScope = isConventionPolicy
177
+ ? result.policyName!.match(/^\.failproofai-(project|user)\//)?.[1] ?? null
178
+ : null;
174
179
  const hasCustomParams =
175
180
  !isCustomHook && !isConventionPolicy && !!(result.policyName && config.policyParams?.[result.policyName]);
176
181
  const paramKeysOverridden = hasCustomParams
@@ -184,6 +189,7 @@ export async function handleHookEvent(eventType: string): Promise<number> {
184
189
  decision: result.decision,
185
190
  is_custom_hook: isCustomHook,
186
191
  is_convention_policy: isConventionPolicy,
192
+ convention_scope: conventionScope,
187
193
  has_custom_params: hasCustomParams,
188
194
  param_keys_overridden: paramKeysOverridden,
189
195
  });
@@ -56,9 +56,8 @@ export async function evaluatePolicies(
56
56
  session,
57
57
  };
58
58
 
59
- // Track the first instruct result (accumulated, does not short-circuit)
60
- let instructPolicyName: string | null = null;
61
- let instructReason: string | null = null;
59
+ // Track all instruct results (accumulated, does not short-circuit)
60
+ const instructEntries: Array<{ policyName: string; reason: string }> = [];
62
61
 
63
62
  // Track informational messages from allow decisions (with policy attribution)
64
63
  const allowEntries: Array<{ policyName: string; reason: string }> = [];
@@ -142,14 +141,14 @@ export async function evaluatePolicies(
142
141
  };
143
142
  }
144
143
 
145
- // Accumulate first instruct (does not short-circuit — later policies can still deny)
146
- if (result.decision === "instruct" && !instructPolicyName) {
147
- instructPolicyName = policy.name;
148
- instructReason = appendHint(
144
+ // Accumulate all instruct results (does not short-circuit — later policies can still deny)
145
+ if (result.decision === "instruct") {
146
+ const reason = appendHint(
149
147
  result.reason ?? `Instruction from policy: ${policy.name}`,
150
148
  config?.policyParams?.[policy.name]?.hint,
151
149
  );
152
- hookLogInfo(`instruct by "${policy.name}": ${instructReason}`);
150
+ instructEntries.push({ policyName: policy.name, reason });
151
+ hookLogInfo(`instruct by "${policy.name}": ${reason}`);
153
152
  }
154
153
 
155
154
  // Accumulate informational messages from allow decisions
@@ -158,17 +157,21 @@ export async function evaluatePolicies(
158
157
  }
159
158
  }
160
159
 
161
- // No deny — check if we accumulated an instruct
162
- if (instructPolicyName && instructReason) {
160
+ // No deny — check if we accumulated any instructs
161
+ if (instructEntries.length > 0) {
162
+ const combined = instructEntries.map((e) => e.reason).join("\n");
163
+ const policyNames = instructEntries.map((e) => e.policyName);
164
+
163
165
  if (eventType === "Stop") {
164
166
  // Stop hook: exitCode 2 blocks Claude from stopping.
165
167
  // Reason goes to stderr so Claude Code receives it as context.
166
168
  return {
167
169
  exitCode: 2,
168
170
  stdout: "",
169
- stderr: instructReason,
170
- policyName: instructPolicyName,
171
- reason: instructReason,
171
+ stderr: combined,
172
+ policyName: policyNames[0],
173
+ policyNames,
174
+ reason: combined,
172
175
  decision: "instruct",
173
176
  };
174
177
  }
@@ -176,15 +179,16 @@ export async function evaluatePolicies(
176
179
  const response = {
177
180
  hookSpecificOutput: {
178
181
  hookEventName: eventType,
179
- additionalContext: `Instruction from failproofai: ${instructReason}`,
182
+ additionalContext: `Instruction from failproofai: ${combined}`,
180
183
  },
181
184
  };
182
185
  return {
183
186
  exitCode: 0,
184
187
  stdout: JSON.stringify(response),
185
188
  stderr: "",
186
- policyName: instructPolicyName,
187
- reason: instructReason,
189
+ policyName: policyNames[0],
190
+ policyNames,
191
+ reason: combined,
188
192
  decision: "instruct",
189
193
  };
190
194
  }