failproofai 0.0.6-beta.1 → 0.0.6-beta.3

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 (177) hide show
  1. package/.next/standalone/.failproofai/policies/review-policies.mjs +4 -3
  2. package/.next/standalone/.next/BUILD_ID +1 -1
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +3 -3
  5. package/.next/standalone/.next/required-server-files.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  15. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  20. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  22. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  23. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  24. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  25. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  26. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  27. package/.next/standalone/.next/server/app/index.html +1 -1
  28. package/.next/standalone/.next/server/app/index.rsc +15 -15
  29. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  30. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  31. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  32. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  33. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  34. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  35. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  37. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  38. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  41. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  43. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  44. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  45. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  48. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g72weg._.js +1 -1
  51. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  52. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
  53. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0~kmh8w._.js → [root-of-the-server]__096k.db._.js} +2 -2
  54. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
  55. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
  56. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
  57. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0rh.18_._.js → [root-of-the-server]__0kyh86x._.js} +2 -2
  58. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0okos0k._.js +2 -2
  59. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +2 -2
  60. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
  61. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
  62. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
  63. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
  65. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  66. package/.next/standalone/.next/server/pages/404.html +2 -2
  67. package/.next/standalone/.next/server/pages/500.html +1 -1
  68. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  69. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  70. package/.next/standalone/.next/static/chunks/{0gbf4cphy8ksq.js → 0-dm_9a6nsc2l.js} +1 -1
  71. package/.next/standalone/.next/static/chunks/{12~yi9oj8av8p.js → 01pmw1-asbek~.js} +2 -2
  72. package/.next/standalone/.next/static/chunks/{0v.yd0kg_ld3r.js → 051m32nx~n5yr.js} +1 -1
  73. package/.next/standalone/.next/static/chunks/{09_k80d~cq2wg.js → 0a-yctdwn368y.js} +1 -1
  74. package/.next/standalone/.next/static/chunks/{0bvhsa6zva2o..js → 0ksdlt_1hucdm.js} +1 -1
  75. package/.next/standalone/.next/static/chunks/{01b~z8f1ws0rk.js → 0l-mu4okl-cj1.js} +1 -1
  76. package/.next/standalone/.next/static/chunks/{08t08igdql9yt.js → 0mazj-p-~2kc6.js} +1 -1
  77. package/.next/standalone/.next/static/chunks/0qakntsrpc~1j.js +6 -0
  78. package/.next/standalone/.next/static/chunks/{03rz6ykw-a2xi.js → 156zca6aewyr-.js} +1 -1
  79. package/.next/standalone/CHANGELOG.md +18 -0
  80. package/.next/standalone/bin/failproofai.mjs +91 -4
  81. package/.next/standalone/dist/cli.mjs +1156 -55
  82. package/.next/standalone/docs/ar/built-in-policies.mdx +140 -103
  83. package/.next/standalone/docs/ar/custom-policies.mdx +72 -72
  84. package/.next/standalone/docs/ar/examples.mdx +86 -33
  85. package/.next/standalone/docs/ar/getting-started.mdx +82 -29
  86. package/.next/standalone/docs/built-in-policies.mdx +3 -3
  87. package/.next/standalone/docs/de/built-in-policies.mdx +97 -60
  88. package/.next/standalone/docs/de/custom-policies.mdx +56 -56
  89. package/.next/standalone/docs/de/examples.mdx +72 -18
  90. package/.next/standalone/docs/de/getting-started.mdx +72 -20
  91. package/.next/standalone/docs/es/built-in-policies.mdx +91 -54
  92. package/.next/standalone/docs/es/custom-policies.mdx +55 -55
  93. package/.next/standalone/docs/es/examples.mdx +73 -19
  94. package/.next/standalone/docs/es/getting-started.mdx +72 -20
  95. package/.next/standalone/docs/fr/built-in-policies.mdx +99 -62
  96. package/.next/standalone/docs/fr/custom-policies.mdx +51 -51
  97. package/.next/standalone/docs/fr/examples.mdx +78 -24
  98. package/.next/standalone/docs/fr/getting-started.mdx +65 -13
  99. package/.next/standalone/docs/he/built-in-policies.mdx +139 -99
  100. package/.next/standalone/docs/he/custom-policies.mdx +75 -75
  101. package/.next/standalone/docs/he/examples.mdx +87 -33
  102. package/.next/standalone/docs/he/getting-started.mdx +84 -33
  103. package/.next/standalone/docs/hi/built-in-policies.mdx +203 -166
  104. package/.next/standalone/docs/hi/custom-policies.mdx +71 -70
  105. package/.next/standalone/docs/hi/examples.mdx +90 -36
  106. package/.next/standalone/docs/hi/getting-started.mdx +80 -27
  107. package/.next/standalone/docs/i18n/README.ar.md +69 -69
  108. package/.next/standalone/docs/i18n/README.de.md +46 -46
  109. package/.next/standalone/docs/i18n/README.es.md +42 -42
  110. package/.next/standalone/docs/i18n/README.fr.md +39 -39
  111. package/.next/standalone/docs/i18n/README.he.md +83 -83
  112. package/.next/standalone/docs/i18n/README.hi.md +69 -69
  113. package/.next/standalone/docs/i18n/README.it.md +72 -72
  114. package/.next/standalone/docs/i18n/README.ja.md +71 -71
  115. package/.next/standalone/docs/i18n/README.ko.md +52 -52
  116. package/.next/standalone/docs/i18n/README.pt-br.md +44 -44
  117. package/.next/standalone/docs/i18n/README.ru.md +66 -66
  118. package/.next/standalone/docs/i18n/README.tr.md +82 -83
  119. package/.next/standalone/docs/i18n/README.vi.md +70 -71
  120. package/.next/standalone/docs/i18n/README.zh.md +51 -51
  121. package/.next/standalone/docs/it/built-in-policies.mdx +115 -78
  122. package/.next/standalone/docs/it/custom-policies.mdx +69 -69
  123. package/.next/standalone/docs/it/examples.mdx +93 -39
  124. package/.next/standalone/docs/it/getting-started.mdx +73 -21
  125. package/.next/standalone/docs/ja/built-in-policies.mdx +155 -118
  126. package/.next/standalone/docs/ja/custom-policies.mdx +71 -71
  127. package/.next/standalone/docs/ja/examples.mdx +76 -22
  128. package/.next/standalone/docs/ja/getting-started.mdx +65 -13
  129. package/.next/standalone/docs/ko/built-in-policies.mdx +103 -66
  130. package/.next/standalone/docs/ko/custom-policies.mdx +67 -67
  131. package/.next/standalone/docs/ko/examples.mdx +87 -33
  132. package/.next/standalone/docs/ko/getting-started.mdx +61 -9
  133. package/.next/standalone/docs/pt-br/built-in-policies.mdx +72 -35
  134. package/.next/standalone/docs/pt-br/custom-policies.mdx +56 -56
  135. package/.next/standalone/docs/pt-br/examples.mdx +78 -24
  136. package/.next/standalone/docs/pt-br/getting-started.mdx +64 -12
  137. package/.next/standalone/docs/ru/built-in-policies.mdx +135 -98
  138. package/.next/standalone/docs/ru/custom-policies.mdx +82 -81
  139. package/.next/standalone/docs/ru/examples.mdx +77 -22
  140. package/.next/standalone/docs/ru/getting-started.mdx +74 -22
  141. package/.next/standalone/docs/tr/built-in-policies.mdx +126 -89
  142. package/.next/standalone/docs/tr/custom-policies.mdx +59 -60
  143. package/.next/standalone/docs/tr/examples.mdx +97 -42
  144. package/.next/standalone/docs/tr/getting-started.mdx +75 -23
  145. package/.next/standalone/docs/vi/built-in-policies.mdx +116 -81
  146. package/.next/standalone/docs/vi/custom-policies.mdx +68 -68
  147. package/.next/standalone/docs/vi/examples.mdx +93 -38
  148. package/.next/standalone/docs/vi/getting-started.mdx +74 -22
  149. package/.next/standalone/docs/zh/built-in-policies.mdx +117 -82
  150. package/.next/standalone/docs/zh/custom-policies.mdx +49 -49
  151. package/.next/standalone/docs/zh/examples.mdx +90 -36
  152. package/.next/standalone/docs/zh/getting-started.mdx +73 -21
  153. package/.next/standalone/package.json +1 -1
  154. package/.next/standalone/server.js +1 -1
  155. package/.next/standalone/src/auth/login.ts +104 -0
  156. package/.next/standalone/src/auth/logout.ts +50 -0
  157. package/.next/standalone/src/auth/token-store.ts +64 -0
  158. package/.next/standalone/src/hooks/builtin-policies.ts +27 -21
  159. package/.next/standalone/src/hooks/handler.ts +35 -15
  160. package/.next/standalone/src/relay/daemon.ts +362 -0
  161. package/.next/standalone/src/relay/pid.ts +76 -0
  162. package/.next/standalone/src/relay/queue.ts +225 -0
  163. package/bin/failproofai.mjs +91 -4
  164. package/dist/cli.mjs +1156 -55
  165. package/package.json +1 -1
  166. package/src/auth/login.ts +104 -0
  167. package/src/auth/logout.ts +50 -0
  168. package/src/auth/token-store.ts +64 -0
  169. package/src/hooks/builtin-policies.ts +27 -21
  170. package/src/hooks/handler.ts +35 -15
  171. package/src/relay/daemon.ts +362 -0
  172. package/src/relay/pid.ts +76 -0
  173. package/src/relay/queue.ts +225 -0
  174. package/.next/standalone/.next/static/chunks/0wlyoif4_kj_t.js +0 -6
  175. /package/.next/standalone/.next/static/{CkmOT-ZvDN-sVULinGVKT → r-wX0MuAfCjbhJm3phQc8}/_buildManifest.js +0 -0
  176. /package/.next/standalone/.next/static/{CkmOT-ZvDN-sVULinGVKT → r-wX0MuAfCjbhJm3phQc8}/_clientMiddlewareManifest.js +0 -0
  177. /package/.next/standalone/.next/static/{CkmOT-ZvDN-sVULinGVKT → r-wX0MuAfCjbhJm3phQc8}/_ssgManifest.js +0 -0
@@ -1,13 +1,13 @@
1
1
  ---
2
- title: 快速入门
2
+ title: 快速开始
3
3
  description: "安装 failproofai,启用策略,让你的 Agent 稳定运行"
4
4
  icon: rocket
5
5
  ---
6
6
 
7
- ## 系统要求
7
+ ## 环境要求
8
8
 
9
9
  - **Node.js** >= 20.9.0
10
- - **Bun** >= 1.3.0(可选 - 仅从源码构建时需要)
10
+ - **Bun** >= 1.3.0(可选,仅在从源码构建时需要)
11
11
 
12
12
  ---
13
13
 
@@ -27,7 +27,7 @@ bun add -g failproofai
27
27
 
28
28
  ---
29
29
 
30
- ## 快速开始
30
+ ## 快速上手
31
31
 
32
32
  <Steps>
33
33
  <Step title="启用策略">
@@ -37,37 +37,37 @@ bun add -g failproofai
37
37
  failproofai policies --install
38
38
  ```
39
39
 
40
- 此命令会将 hook 条目写入 Claude Code 的 `settings.json`。你也可以针对单个项目安装,或选择特定策略:
40
+ 该命令会将 hook 条目写入 Claude Code 的 `settings.json`。你也可以仅为单个项目安装,或选择特定策略:
41
41
 
42
42
  ```bash
43
43
  failproofai policies --install --scope project
44
44
  failproofai policies --install block-sudo block-rm-rf sanitize-api-keys
45
45
  ```
46
46
  </Step>
47
- <Step title="验证">
47
+ <Step title="验证安装">
48
48
  ```bash
49
49
  failproofai policies
50
50
  ```
51
51
 
52
- 显示所有策略、是否已启用以及已配置的参数。
52
+ 显示所有策略、启用状态及已配置的参数。
53
53
  </Step>
54
- <Step title="启动仪表板">
54
+ <Step title="启动控制台">
55
55
  ```bash
56
56
  failproofai
57
57
  ```
58
58
 
59
- 在 `http://localhost:8020` 打开本地仪表板,你可以在此浏览会话、查看工具调用详情并管理策略。
59
+ 在 `http://localhost:8020` 打开本地控制台,你可以在此浏览会话记录、查看工具调用详情并管理策略。
60
60
  </Step>
61
61
  <Step title="运行你的 Agent">
62
- 像往常一样启动 Claude Code。如果 Agent 尝试执行高风险操作,failproofai 会自动拦截。让它在无人值守的情况下运行,之后在仪表板中查看执行记录。
62
+ 像往常一样启动 Claude Code。如果 Agent 尝试执行风险操作,failproofai 会自动拦截。你可以让其在无人值守的情况下运行,之后在控制台中回顾发生的一切。
63
63
  </Step>
64
64
  </Steps>
65
65
 
66
66
  ---
67
67
 
68
- ## 策略的工作原理
68
+ ## 策略工作原理
69
69
 
70
- 每当 Agent 运行工具时,Claude Code 会将 failproofai 作为子进程调用:
70
+ 每当 Agent 运行工具时,Claude Code 会以子进程方式调用 failproofai
71
71
 
72
72
  ```text
73
73
  Claude Code → failproofai --hook PreToolUse → reads stdin JSON
@@ -75,11 +75,11 @@ Claude Code → failproofai --hook PreToolUse → reads stdin JSON
75
75
  writes decision to stdout
76
76
  ```
77
77
 
78
- 每个策略返回以下三种决策之一:
78
+ 每条策略会返回以下三种决策之一:
79
79
 
80
80
  - **allow** - Agent 正常继续执行
81
- - **deny** - 操作被阻止,并告知 Agent 原因
82
- - **instruct** - 向 Agent 的提示词中追加额外上下文
81
+ - **deny** - 操作被拦截,并将原因告知 Agent
82
+ - **instruct** - 向 Agent 的提示词中追加额外的上下文信息
83
83
 
84
84
  <Note>
85
85
  策略在你的本地进程中运行,不会向任何远程服务发送数据。
@@ -87,6 +87,58 @@ Claude Code → failproofai --hook PreToolUse → reads stdin JSON
87
87
 
88
88
  ---
89
89
 
90
+ ## 使用约定式策略为团队制定统一规范
91
+
92
+ 在团队中建立质量标准最快的方式是使用 `.failproofai/policies/` 约定目录。只需将策略文件放入该目录,它们会自动加载——无需任何标志、配置变更或安装命令。
93
+
94
+ <Steps>
95
+ <Step title="创建策略目录">
96
+ ```bash
97
+ mkdir -p .failproofai/policies
98
+ ```
99
+ </Step>
100
+ <Step title="添加策略文件">
101
+ 复制示例文件或编写你自己的策略:
102
+
103
+ ```bash
104
+ cp node_modules/failproofai/examples/convention-policies/*.mjs .failproofai/policies/
105
+ ```
106
+
107
+ 或者新建一个策略文件:
108
+
109
+ ```js
110
+ // .failproofai/policies/team-policies.mjs
111
+ import { customPolicies, allow, deny, instruct } from "failproofai";
112
+
113
+ customPolicies.add({
114
+ name: "test-before-commit",
115
+ match: { events: ["PreToolUse"] },
116
+ fn: async (ctx) => {
117
+ if (ctx.toolName !== "Bash") return allow();
118
+ if (/git\s+commit/.test(ctx.toolInput?.command ?? "")) {
119
+ return instruct("Run tests before committing.");
120
+ }
121
+ return allow();
122
+ },
123
+ });
124
+ ```
125
+ </Step>
126
+ <Step title="提交到 git">
127
+ ```bash
128
+ git add .failproofai/policies/
129
+ git commit -m "Add team quality policies"
130
+ ```
131
+
132
+ 所有安装了 failproofai 的团队成员都会自动获取这些策略,无需单独配置。
133
+ </Step>
134
+ </Steps>
135
+
136
+ <Tip>
137
+ 将 `.failproofai/policies/` 提交到代码仓库,让整个团队共享同一套规范。随着团队发现新的故障模式,持续添加策略并推送更新——所有人在下次 `git pull` 时即可获取最新内容。随着时间推移,这些策略将成为不断演进的质量标准。
138
+ </Tip>
139
+
140
+ ---
141
+
90
142
  ## 数据存储
91
143
 
92
144
  所有配置和日志均保存在本地:
@@ -94,10 +146,10 @@ Claude Code → failproofai --hook PreToolUse → reads stdin JSON
94
146
  | 路径 | 存储内容 |
95
147
  |------|----------------|
96
148
  | `~/.failproofai/policies-config.json` | 全局策略配置 |
97
- | `~/.failproofai/hook-activity.jsonl` | Hook 执行历史记录 |
149
+ | `~/.failproofai/hook-activity.jsonl` | Hook 执行历史 |
98
150
  | `~/.failproofai/hook.log` | 自定义 hook 错误的调试日志 |
99
- | `.failproofai/policies-config.json` | 项目级配置(可提交至版本库) |
100
- | `.failproofai/policies-config.local.json` | 个人覆盖配置(已被 gitignore) |
151
+ | `.failproofai/policies-config.json` | 项目级配置(可提交至仓库) |
152
+ | `.failproofai/policies-config.local.json` | 个人覆盖配置(已加入 gitignore) |
101
153
 
102
154
  ---
103
155
 
@@ -115,16 +167,16 @@ failproofai policies --uninstall
115
167
 
116
168
  <CardGroup cols={2}>
117
169
 
118
- <Card title="配置" icon="gear" href="/zh/configuration">
170
+ <Card title="配置说明" icon="gear" href="/zh/configuration">
119
171
  作用域与配置文件格式
120
172
  </Card>
121
173
 
122
174
  <Card title="内置策略" icon="shield" href="/zh/built-in-policies">
123
- 全部 26 个策略及其参数说明
175
+ 全部 26 条策略及其参数说明
124
176
  </Card>
125
177
 
126
178
  <Card title="自定义策略" icon="code" href="/zh/custom-policies">
127
- JavaScript 编写你自己的策略
179
+ 使用 JavaScript 编写你自己的策略
128
180
  </Card>
129
181
 
130
182
  <Card title="Agent 监控" icon="chart-line" href="/zh/dashboard">
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "0.0.6-beta.1",
3
+ "version": "0.0.6-beta.3",
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"
@@ -9,7 +9,7 @@ const currentPort = parseInt(process.env.PORT, 10) || 3000
9
9
  const hostname = process.env.HOSTNAME || '0.0.0.0'
10
10
 
11
11
  let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
12
- const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"0.0.6-beta.1"},"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":true,"customCacheHandler":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{"serverFunctions":true,"browserToTerminal":"warn"},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/home/runner/work/failproofai/failproofai","cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"appNewScrollHandler":false,"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"cachedNavigations":false,"partialFallbacks":false,"dynamicOnHover":false,"varyParams":false,"prefetchInlining":false,"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":3,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"imgOptSkipMetadata":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"strictRouteTypes":false,"viewTransition":false,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":false,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":true,"serverComponentsHmrCache":true,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"gestureTransition":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":"warn","lockDistDir":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":false,"turbopackInferModuleSideEffects":true,"turbopackPluginRuntimeStrategy":"childProcesses","optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","turbopack":{"root":"/home/runner/work/failproofai/failproofai"},"distDirRoot":".next"}
12
+ const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"0.0.6-beta.3"},"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":true,"customCacheHandler":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{"serverFunctions":true,"browserToTerminal":"warn"},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/home/runner/work/failproofai/failproofai","cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"appNewScrollHandler":false,"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"cachedNavigations":false,"partialFallbacks":false,"dynamicOnHover":false,"varyParams":false,"prefetchInlining":false,"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":3,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"imgOptSkipMetadata":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"strictRouteTypes":false,"viewTransition":false,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":false,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":true,"serverComponentsHmrCache":true,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"gestureTransition":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":"warn","lockDistDir":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":false,"turbopackInferModuleSideEffects":true,"turbopackPluginRuntimeStrategy":"childProcesses","optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","turbopack":{"root":"/home/runner/work/failproofai/failproofai"},"distDirRoot":".next"}
13
13
 
14
14
  process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
15
15
 
@@ -0,0 +1,104 @@
1
+ import { spawn } from "node:child_process";
2
+ import { platform } from "node:os";
3
+ import { writeTokens, type AuthTokens } from "./token-store";
4
+
5
+ const DEFAULT_SERVER_URL = process.env.FAILPROOFAI_SERVER_URL ?? "https://api.befailproof.ai";
6
+ const HTTP_TIMEOUT_MS = 10_000;
7
+
8
+ interface DeviceCodeResponse {
9
+ device_code: string;
10
+ user_code: string;
11
+ verification_url: string;
12
+ expires_in: number;
13
+ interval: number;
14
+ }
15
+
16
+ interface TokenResponse {
17
+ access_token: string;
18
+ refresh_token: string;
19
+ expires_in: number;
20
+ user: { id: string; email: string; name?: string };
21
+ }
22
+
23
+ function openBrowser(url: string): void {
24
+ const os = platform();
25
+ try {
26
+ if (os === "darwin") {
27
+ spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
28
+ } else if (os === "win32") {
29
+ // On cmd's `start`, the first quoted token is treated as a window
30
+ // title. Pass an empty title so URLs containing "&" or spaces are
31
+ // interpreted as the target, not the title.
32
+ spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
33
+ } else {
34
+ spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
35
+ }
36
+ } catch {
37
+ // Fallback: the URL is already printed above.
38
+ }
39
+ }
40
+
41
+ async function postJson<T>(url: string, body: unknown, timeoutMs = HTTP_TIMEOUT_MS): Promise<T> {
42
+ const resp = await fetch(url, {
43
+ method: "POST",
44
+ headers: { "Content-Type": "application/json" },
45
+ body: JSON.stringify(body),
46
+ signal: AbortSignal.timeout(timeoutMs),
47
+ });
48
+ if (!resp.ok) {
49
+ throw new Error(`${url} → ${resp.status} ${resp.statusText}`);
50
+ }
51
+ return (await resp.json()) as T;
52
+ }
53
+
54
+ export async function login(): Promise<void> {
55
+ const serverUrl = DEFAULT_SERVER_URL;
56
+
57
+ console.log("Requesting device code...");
58
+ const dc = await postJson<DeviceCodeResponse>(`${serverUrl}/api/v1/auth/device-code`, {});
59
+
60
+ console.log(`\n Open this URL in your browser (will be opened automatically):`);
61
+ console.log(` ${dc.verification_url}\n`);
62
+ console.log(` Your code: ${dc.user_code}\n`);
63
+
64
+ openBrowser(dc.verification_url);
65
+
66
+ const deadline = Date.now() + dc.expires_in * 1000;
67
+ const intervalMs = dc.interval * 1000;
68
+
69
+ while (Date.now() < deadline) {
70
+ await new Promise((r) => setTimeout(r, intervalMs));
71
+ try {
72
+ const result = await postJson<TokenResponse | { status: string }>(
73
+ `${serverUrl}/api/v1/auth/device-token`,
74
+ { device_code: dc.device_code },
75
+ );
76
+ if ("access_token" in result) {
77
+ const tokens: AuthTokens = {
78
+ access_token: result.access_token,
79
+ refresh_token: result.refresh_token,
80
+ expires_at: Math.floor(Date.now() / 1000) + result.expires_in,
81
+ user_email: result.user.email,
82
+ user_id: result.user.id,
83
+ server_url: serverUrl,
84
+ };
85
+ writeTokens(tokens);
86
+ console.log(`Logged in as ${result.user.email}`);
87
+
88
+ // Auto-start relay daemon
89
+ try {
90
+ const { ensureRelayRunning } = await import("../relay/daemon");
91
+ ensureRelayRunning();
92
+ console.log("Relay daemon started.");
93
+ } catch (e) {
94
+ console.warn("Failed to auto-start relay daemon:", e);
95
+ }
96
+ return;
97
+ }
98
+ } catch {
99
+ // Pending or transient error — keep polling
100
+ }
101
+ }
102
+
103
+ throw new Error("Login timed out. Run `failproofai login` again.");
104
+ }
@@ -0,0 +1,50 @@
1
+ import { readTokens, clearTokens } from "./token-store";
2
+ import { stopRelay } from "../relay/pid";
3
+
4
+ const LOGOUT_TIMEOUT_MS = 3_000;
5
+
6
+ export async function logout(): Promise<void> {
7
+ const tokens = readTokens();
8
+ if (!tokens) {
9
+ console.log("Not logged in.");
10
+ return;
11
+ }
12
+
13
+ // Best-effort server revoke with a short timeout — the local logout
14
+ // must not block on a slow network.
15
+ try {
16
+ await fetch(`${tokens.server_url}/api/v1/auth/logout`, {
17
+ method: "POST",
18
+ headers: { "Content-Type": "application/json" },
19
+ body: JSON.stringify({ refresh_token: tokens.refresh_token }),
20
+ signal: AbortSignal.timeout(LOGOUT_TIMEOUT_MS),
21
+ });
22
+ } catch {
23
+ // Network or timeout — proceed to local clear anyway
24
+ }
25
+
26
+ try {
27
+ stopRelay();
28
+ } catch {
29
+ // Best-effort daemon stop
30
+ }
31
+
32
+ clearTokens();
33
+ console.log("Logged out.");
34
+ }
35
+
36
+ export function whoami(): void {
37
+ const tokens = readTokens();
38
+ if (!tokens) {
39
+ console.log("Not logged in. Run `failproofai login` to authenticate.");
40
+ process.exit(1);
41
+ }
42
+ console.log(`Logged in as ${tokens.user_email}`);
43
+ console.log(`Server: ${tokens.server_url}`);
44
+ const expiresIn = tokens.expires_at - Math.floor(Date.now() / 1000);
45
+ if (expiresIn > 0) {
46
+ console.log(`Access token expires in ${Math.floor(expiresIn / 60)} minutes`);
47
+ } else {
48
+ console.log(`Access token expired (will refresh on next use)`);
49
+ }
50
+ }
@@ -0,0 +1,64 @@
1
+ import {
2
+ readFileSync,
3
+ writeFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ unlinkSync,
7
+ renameSync,
8
+ openSync,
9
+ closeSync,
10
+ } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { homedir } from "node:os";
13
+
14
+ export interface AuthTokens {
15
+ access_token: string;
16
+ refresh_token: string;
17
+ expires_at: number;
18
+ user_email: string;
19
+ user_id: string;
20
+ server_url: string;
21
+ }
22
+
23
+ const AUTH_DIR = join(homedir(), ".failproofai");
24
+ const AUTH_FILE = join(AUTH_DIR, "auth.json");
25
+
26
+ function ensureAuthDir(): void {
27
+ if (!existsSync(AUTH_DIR)) mkdirSync(AUTH_DIR, { recursive: true, mode: 0o700 });
28
+ }
29
+
30
+ export function readTokens(): AuthTokens | null {
31
+ if (!existsSync(AUTH_FILE)) return null;
32
+ try {
33
+ const raw = readFileSync(AUTH_FILE, "utf8");
34
+ return JSON.parse(raw) as AuthTokens;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Write tokens atomically with 0600 permissions *from creation*.
42
+ * We open with O_WRONLY|O_CREAT|O_TRUNC and explicit mode 0600 so the
43
+ * file is never world-readable, not even briefly during the write.
44
+ * Then rename into place (atomic on POSIX).
45
+ */
46
+ export function writeTokens(tokens: AuthTokens): void {
47
+ ensureAuthDir();
48
+ const tmpPath = `${AUTH_FILE}.tmp`;
49
+ const fd = openSync(tmpPath, "w", 0o600);
50
+ try {
51
+ writeFileSync(fd, JSON.stringify(tokens, null, 2));
52
+ } finally {
53
+ closeSync(fd);
54
+ }
55
+ renameSync(tmpPath, AUTH_FILE);
56
+ }
57
+
58
+ export function clearTokens(): void {
59
+ if (existsSync(AUTH_FILE)) unlinkSync(AUTH_FILE);
60
+ }
61
+
62
+ export function isLoggedIn(): boolean {
63
+ return existsSync(AUTH_FILE);
64
+ }
@@ -171,7 +171,7 @@ function getCurrentBranch(cwd: string): string | null {
171
171
  if (branch === undefined) {
172
172
  branch = execSync("git rev-parse --abbrev-ref HEAD", {
173
173
  cwd,
174
- encoding: "utf8",
174
+ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
175
175
  timeout: 3000,
176
176
  }).trim();
177
177
  gitBranchCache.set(cwd, branch);
@@ -186,7 +186,7 @@ function getHeadSha(cwd: string): string | null {
186
186
  try {
187
187
  const sha = execSync("git rev-parse HEAD", {
188
188
  cwd,
189
- encoding: "utf8",
189
+ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
190
190
  timeout: 3000,
191
191
  }).trim();
192
192
  return sha || null;
@@ -214,7 +214,7 @@ function getThirdPartyCheckRuns(cwd: string, sha: string): CiCheck[] {
214
214
  ],
215
215
  {
216
216
  cwd,
217
- encoding: "utf8",
217
+ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
218
218
  timeout: 15000,
219
219
  },
220
220
  ).trim();
@@ -239,7 +239,7 @@ function getCommitStatuses(cwd: string, sha: string): CiCheck[] {
239
239
  ],
240
240
  {
241
241
  cwd,
242
- encoding: "utf8",
242
+ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
243
243
  timeout: 15000,
244
244
  },
245
245
  ).trim();
@@ -676,7 +676,9 @@ function extractAbsolutePaths(command: string): string[] {
676
676
  }
677
677
 
678
678
  function blockReadOutsideCwd(ctx: PolicyContext): PolicyResult {
679
- const cwd = ctx.session?.cwd;
679
+ // Prefer $CLAUDE_PROJECT_DIR (stable project root) over ctx.session.cwd,
680
+ // which tracks the live shell CWD and drifts when Claude `cd`s into a subdir.
681
+ const cwd = process.env.CLAUDE_PROJECT_DIR || ctx.session?.cwd;
680
682
  if (!cwd) return allow(); // Can't enforce without cwd
681
683
 
682
684
  const allowPaths = ((ctx.params?.allowPaths ?? []) as string[]);
@@ -964,7 +966,7 @@ function requireCommitBeforeStop(ctx: PolicyContext): PolicyResult {
964
966
  try {
965
967
  const status = execSync("git status --porcelain", {
966
968
  cwd,
967
- encoding: "utf8",
969
+ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
968
970
  timeout: 5000,
969
971
  }).trim();
970
972
 
@@ -986,7 +988,7 @@ function requirePushBeforeStop(ctx: PolicyContext): PolicyResult {
986
988
  try {
987
989
  const remotes = execSync("git remote", {
988
990
  cwd,
989
- encoding: "utf8",
991
+ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
990
992
  timeout: 3000,
991
993
  }).trim();
992
994
 
@@ -1009,7 +1011,7 @@ function requirePushBeforeStop(ctx: PolicyContext): PolicyResult {
1009
1011
  const ahead = execFileSync(
1010
1012
  "git",
1011
1013
  ["log", `${remote}/${baseBranch}..HEAD`, "--oneline"],
1012
- { cwd, encoding: "utf8", timeout: 5000 },
1014
+ { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 },
1013
1015
  ).trim();
1014
1016
 
1015
1017
  if (!ahead) {
@@ -1022,7 +1024,7 @@ function requirePushBeforeStop(ctx: PolicyContext): PolicyResult {
1022
1024
  const diff = execFileSync(
1023
1025
  "git",
1024
1026
  ["diff", "--stat", `${remote}/${baseBranch}`, "HEAD"],
1025
- { cwd, encoding: "utf8", timeout: 5000 },
1027
+ { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 },
1026
1028
  ).trim();
1027
1029
 
1028
1030
  if (!diff) {
@@ -1037,7 +1039,7 @@ function requirePushBeforeStop(ctx: PolicyContext): PolicyResult {
1037
1039
  try {
1038
1040
  execFileSync("git", ["rev-parse", "--verify", `${remote}/${branch}`], {
1039
1041
  cwd,
1040
- encoding: "utf8",
1042
+ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
1041
1043
  timeout: 3000,
1042
1044
  });
1043
1045
  hasTracking = true;
@@ -1055,7 +1057,7 @@ function requirePushBeforeStop(ctx: PolicyContext): PolicyResult {
1055
1057
  // Check for unpushed commits
1056
1058
  const unpushed = execFileSync("git", ["log", `${remote}/${branch}..HEAD`, "--oneline"], {
1057
1059
  cwd,
1058
- encoding: "utf8",
1060
+ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
1059
1061
  timeout: 5000,
1060
1062
  }).trim();
1061
1063
 
@@ -1080,7 +1082,7 @@ function requirePrBeforeStop(ctx: PolicyContext): PolicyResult {
1080
1082
  try {
1081
1083
  // Check if gh CLI is available
1082
1084
  try {
1083
- execSync("gh --version", { cwd, encoding: "utf8", timeout: 3000 });
1085
+ execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
1084
1086
  } catch {
1085
1087
  return allow("GitHub CLI (gh) not installed, skipping PR check.");
1086
1088
  }
@@ -1100,7 +1102,7 @@ function requirePrBeforeStop(ctx: PolicyContext): PolicyResult {
1100
1102
  const ahead = execFileSync(
1101
1103
  "git",
1102
1104
  ["log", `origin/${baseBranch}..HEAD`, "--oneline"],
1103
- { cwd, encoding: "utf8", timeout: 5000 },
1105
+ { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 },
1104
1106
  ).trim();
1105
1107
 
1106
1108
  if (!ahead) {
@@ -1113,7 +1115,7 @@ function requirePrBeforeStop(ctx: PolicyContext): PolicyResult {
1113
1115
  const diff = execFileSync(
1114
1116
  "git",
1115
1117
  ["diff", "--stat", `origin/${baseBranch}`, "HEAD"],
1116
- { cwd, encoding: "utf8", timeout: 5000 },
1118
+ { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 },
1117
1119
  ).trim();
1118
1120
 
1119
1121
  if (!diff) {
@@ -1128,7 +1130,7 @@ function requirePrBeforeStop(ctx: PolicyContext): PolicyResult {
1128
1130
  try {
1129
1131
  prJson = execSync("gh pr view --json number,url,state", {
1130
1132
  cwd,
1131
- encoding: "utf8",
1133
+ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
1132
1134
  timeout: 15000,
1133
1135
  }).trim();
1134
1136
  } catch {
@@ -1151,13 +1153,13 @@ function requirePrBeforeStop(ctx: PolicyContext): PolicyResult {
1151
1153
  try {
1152
1154
  execFileSync("git", ["fetch", "origin", `+refs/heads/${baseBranch}:refs/remotes/origin/${baseBranch}`], {
1153
1155
  cwd,
1154
- encoding: "utf8",
1156
+ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
1155
1157
  timeout: 10000,
1156
1158
  });
1157
1159
  const freshAhead = execFileSync(
1158
1160
  "git",
1159
1161
  ["log", `origin/${baseBranch}..HEAD`, "--oneline"],
1160
- { cwd, encoding: "utf8", timeout: 5000 },
1162
+ { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 },
1161
1163
  ).trim();
1162
1164
  if (!freshAhead) {
1163
1165
  return allow(`PR #${pr.number} was merged; branch is up to date with ${baseBranch}.`);
@@ -1165,7 +1167,7 @@ function requirePrBeforeStop(ctx: PolicyContext): PolicyResult {
1165
1167
  const freshDiff = execFileSync(
1166
1168
  "git",
1167
1169
  ["diff", "--stat", `origin/${baseBranch}`, "HEAD"],
1168
- { cwd, encoding: "utf8", timeout: 5000 },
1170
+ { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 },
1169
1171
  ).trim();
1170
1172
  if (!freshDiff) {
1171
1173
  return allow(`PR #${pr.number} was merged; no file changes vs ${baseBranch}.`);
@@ -1190,7 +1192,7 @@ function requireCiGreenBeforeStop(ctx: PolicyContext): PolicyResult {
1190
1192
  try {
1191
1193
  // Check if gh CLI is available
1192
1194
  try {
1193
- execSync("gh --version", { cwd, encoding: "utf8", timeout: 3000 });
1195
+ execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
1194
1196
  } catch {
1195
1197
  return allow("GitHub CLI (gh) not installed, skipping CI check.");
1196
1198
  }
@@ -1204,7 +1206,7 @@ function requireCiGreenBeforeStop(ctx: PolicyContext): PolicyResult {
1204
1206
  const runsJson = execFileSync(
1205
1207
  "gh",
1206
1208
  ["run", "list", "--branch", branch, "--limit", "5", "--json", "status,conclusion,name"],
1207
- { cwd, encoding: "utf8", timeout: 15000 },
1209
+ { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 },
1208
1210
  ).trim();
1209
1211
 
1210
1212
  if (runsJson && runsJson !== "[]") {
@@ -1229,7 +1231,11 @@ function requireCiGreenBeforeStop(ctx: PolicyContext): PolicyResult {
1229
1231
  if (allChecks.length === 0) return allow(`No CI runs found for branch "${branch}".`);
1230
1232
 
1231
1233
  const failing = allChecks.filter(
1232
- (r) => r.status === "completed" && r.conclusion !== "success" && r.conclusion !== "skipped",
1234
+ (r) =>
1235
+ r.status === "completed" &&
1236
+ r.conclusion !== "success" &&
1237
+ r.conclusion !== "skipped" &&
1238
+ r.conclusion !== "cancelled",
1233
1239
  );
1234
1240
  if (failing.length > 0) {
1235
1241
  const names = failing.map((r) => `"${r.name}"`).join(", ");