failproofai 1.0.12-beta.2 → 1.0.12

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 (196) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +4 -4
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +2 -2
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +3 -3
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  30. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  32. package/.next/standalone/.next/server/app/index.html +1 -1
  33. package/.next/standalone/.next/server/app/index.rsc +3 -3
  34. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  35. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
  36. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  37. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  38. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  41. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  43. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  51. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
  54. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/chunks/_0tovk6q0miud6._.js +1 -1
  57. package/.next/standalone/.next/server/chunks/_0trp3ycqv17cl._.js +1 -1
  58. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4vkpxxu7._.js +1 -1
  59. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08ch3staq3ejw._.js +1 -1
  60. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bigvy9xmv2ka._.js +1 -1
  61. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h86bl2qf0k_o._.js +1 -1
  62. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pia5h3p8re4c._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pwj2h2efe5f-._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-ey9fgesfbc0._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15usvs-ms-0hf._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00l2u6j86cb8y._.js → [root-of-the-server]__167mipqmjprq5._.js} +2 -2
  67. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16gb3_92zvk5x._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1avi2kypusyb_._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1r7drlqli-ado._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/ssr/_0bseae9u7g98r._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/ssr/_0eoura3_9gjb-._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/_0mouhaflye1ro._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/{_1fw0_nzkae5ao._.js → _1d98fyfrcaqhy._.js} +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/_1djqs9i27e4kg._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/_20hn4gn87nsjc._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo5_vm5f.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9snixb5fd-._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47q0xq5w._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/app_components_project-list_tsx_1ke6anr0vt64h._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x33jqb6._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpcz0uxur._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h47jmhhd._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_1fv03uigni8s5.js → node_modules_html-to-image_es_index_0tqc05n832v7a.js} +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0janj0i6dqko7._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/package_json_[json]_cjs_013tntikdft59._.js +1 -1
  87. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  88. package/.next/standalone/.next/server/pages/404.html +1 -1
  89. package/.next/standalone/.next/server/pages/500.html +1 -1
  90. package/.next/standalone/.next/server/preview-props.json +3 -3
  91. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
  93. package/.next/standalone/.next/static/chunks/{1c_jf6g5wnslk.js → 0el9n3h9_zp5m.js} +1 -1
  94. package/.next/standalone/.next/static/chunks/0k7g_8x3o6gd-.js +1 -0
  95. package/.next/standalone/.next/static/chunks/{2o6d37j425uci.js → 0umzct31duk5g.js} +1 -1
  96. package/.next/standalone/.next/static/chunks/{42xptz88mtq3x.js → 0wobqgosirs8p.js} +1 -1
  97. package/.next/standalone/.next/static/chunks/12rug3jilwqzs.js +1 -0
  98. package/.next/standalone/.next/static/chunks/{0ay__-d2h1xb5.js → 1kduj74-715jh.js} +1 -1
  99. package/.next/standalone/.next/static/chunks/{40oworn_-x12p.js → 2701amhrbsbot.js} +1 -1
  100. package/.next/standalone/.next/static/chunks/{0tbm3vjm62h-2.js → 27cavoc8paelg.js} +1 -1
  101. package/.next/standalone/.next/static/chunks/{1my3-5p47t7r-.js → 2ve23a7keupjw.js} +1 -1
  102. package/.next/standalone/.next/static/chunks/{21hckpmsc4g70.js → 3bv6xaoitjpnh.js} +2 -2
  103. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +19 -0
  104. package/.next/standalone/fp-cloud-cli/README.md +120 -0
  105. package/.next/standalone/fp-cloud-cli/fp_cli/app.py +15 -1
  106. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +198 -7
  107. package/.next/standalone/fp-cloud-cli/fp_cli/cloud_output.py +523 -0
  108. package/.next/standalone/fp-cloud-cli/fp_cli/commands/_cloud.py +45 -0
  109. package/.next/standalone/fp-cloud-cli/fp_cli/commands/_write.py +3 -1
  110. package/.next/standalone/fp-cloud-cli/fp_cli/commands/account_cmds.py +171 -0
  111. package/.next/standalone/fp-cloud-cli/fp_cli/commands/agent_cmds.py +80 -7
  112. package/.next/standalone/fp-cloud-cli/fp_cli/commands/alerts_cmds.py +23 -0
  113. package/.next/standalone/fp-cloud-cli/fp_cli/commands/audits_cmds.py +2 -0
  114. package/.next/standalone/fp-cloud-cli/fp_cli/commands/contracts_cmds.py +67 -0
  115. package/.next/standalone/fp-cloud-cli/fp_cli/commands/dashboards_cmds.py +159 -0
  116. package/.next/standalone/fp-cloud-cli/fp_cli/commands/evaluators_cmds.py +380 -0
  117. package/.next/standalone/fp-cloud-cli/fp_cli/commands/events_cmds.py +14 -1
  118. package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +82 -6
  119. package/.next/standalone/fp-cloud-cli/fp_cli/commands/list_cmds.py +4 -1
  120. package/.next/standalone/fp-cloud-cli/fp_cli/commands/orgs_cmds.py +17 -0
  121. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +143 -1
  122. package/.next/standalone/fp-cloud-cli/fp_cli/commands/queries_cmds.py +70 -0
  123. package/.next/standalone/fp-cloud-cli/fp_cli/commands/sentiment_cmds.py +49 -0
  124. package/.next/standalone/fp-cloud-cli/fp_cli/commands/sessions_cmds.py +95 -1
  125. package/.next/standalone/fp-cloud-cli/fp_cli/commands/settings_cmds.py +2 -0
  126. package/.next/standalone/fp-cloud-cli/fp_cli/commands/usage_cmds.py +21 -1
  127. package/.next/standalone/fp-cloud-cli/fp_cli/commands/users_cmds.py +14 -0
  128. package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +3 -0
  129. package/.next/standalone/fp-cloud-cli/fp_cli/evaluator_spec.py +266 -0
  130. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +187 -81
  131. package/.next/standalone/fp-cloud-cli/fp_cli/theme.py +21 -24
  132. package/.next/standalone/fp-cloud-cli/skill/SKILL.md +124 -23
  133. package/.next/standalone/fp-cloud-cli/skill/agents/openai.yaml +2 -1
  134. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +168 -16
  135. package/.next/standalone/fp-cloud-cli/tests/fake_cloud_smoke.py +312 -0
  136. package/.next/standalone/fp-cloud-cli/tests/test_cloud_account.py +125 -0
  137. package/.next/standalone/fp-cloud-cli/tests/test_cloud_agent.py +253 -0
  138. package/.next/standalone/fp-cloud-cli/tests/test_cloud_human_views.py +287 -0
  139. package/.next/standalone/fp-cloud-cli/tests/test_cloud_observability.py +116 -0
  140. package/.next/standalone/fp-cloud-cli/tests/test_cloud_transport.py +141 -0
  141. package/.next/standalone/fp-cloud-cli/tests/test_commands.py +3 -3
  142. package/.next/standalone/fp-cloud-cli/tests/test_dashboards_agent.py +3 -3
  143. package/.next/standalone/fp-cloud-cli/tests/test_evaluator_authoring.py +449 -0
  144. package/.next/standalone/fp-cloud-cli/tests/test_facets.py +1 -1
  145. package/.next/standalone/fp-cloud-cli/tests/test_help_table_coverage.py +15 -0
  146. package/.next/standalone/fp-cloud-cli/tests/test_operator.py +1 -1
  147. package/.next/standalone/fp-cloud-cli/tests/test_orgs.py +1 -1
  148. package/.next/standalone/fp-cloud-cli/tests/test_output.py +37 -37
  149. package/.next/standalone/fp-cloud-cli/tests/test_round4_behaviour.py +144 -0
  150. package/.next/standalone/fp-cloud-cli/tests/test_screen_language.py +80 -0
  151. package/.next/standalone/fp-cloud-cli/tests/test_session_reevaluate.py +33 -0
  152. package/.next/standalone/fp-cloud-cli/tests/test_staging_parity.py +198 -0
  153. package/.next/standalone/fp-cloud-cli/tests/test_whoami.py +1 -1
  154. package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
  155. package/.next/standalone/package.json +9 -9
  156. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +68 -176
  157. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +2 -2
  158. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +4 -4
  159. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +1 -1
  160. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package-lock.json +7 -7
  161. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package.json +1 -1
  162. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package-lock.json +7 -7
  163. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package.json +1 -1
  164. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +98 -115
  165. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +3 -3
  166. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package-lock.json +4 -4
  167. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package.json +1 -1
  168. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +4 -4
  169. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +1 -1
  170. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package-lock.json +149 -171
  171. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package.json +2 -2
  172. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package-lock.json +4 -4
  173. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package.json +1 -1
  174. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +25 -25
  175. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +1 -1
  176. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package-lock.json +7 -7
  177. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package.json +1 -1
  178. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +10 -10
  179. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package.json +1 -1
  180. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package-lock.json +7 -7
  181. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package.json +1 -1
  182. package/.next/standalone/server.js +1 -1
  183. package/dist/cli.mjs +85 -37
  184. package/dist/worker.mjs +73 -28
  185. package/hermes-plugin/plugin.yaml +1 -1
  186. package/package.json +9 -9
  187. package/src/hooks/agent-skill.ts +3 -0
  188. package/src/hooks/fp-home.ts +1 -1
  189. package/src/hooks/handler.ts +22 -3
  190. package/src/hooks/semantic/intent.ts +127 -24
  191. package/src/hooks/semantic/jev-review.ts +21 -2
  192. package/.next/standalone/.next/static/chunks/20py8k1gd_i1m.js +0 -1
  193. package/.next/standalone/.next/static/chunks/2hmex93ppj1ke.js +0 -1
  194. /package/.next/standalone/.next/static/{LHCtFxVG0mdvd4lsXAOFC → D7LY1YSrw96ZxKSpxxrFw}/_buildManifest.js +0 -0
  195. /package/.next/standalone/.next/static/{LHCtFxVG0mdvd4lsXAOFC → D7LY1YSrw96ZxKSpxxrFw}/_clientMiddlewareManifest.js +0 -0
  196. /package/.next/standalone/.next/static/{LHCtFxVG0mdvd4lsXAOFC → D7LY1YSrw96ZxKSpxxrFw}/_ssgManifest.js +0 -0
package/dist/worker.mjs CHANGED
@@ -1166,7 +1166,7 @@ function instruct(reason) {
1166
1166
  }
1167
1167
 
1168
1168
  // package.json
1169
- var version = "1.0.12-beta.2";
1169
+ var version = "1.0.12";
1170
1170
  var init_package = () => {};
1171
1171
 
1172
1172
  // src/hooks/agent-targets.ts
@@ -15706,6 +15706,7 @@ __export(exports_intent, {
15706
15706
  cleanHumanTurn: () => cleanHumanTurn,
15707
15707
  cleanUserSaid: () => cleanUserSaid,
15708
15708
  humanMessageText: () => humanMessageText,
15709
+ intentProfileKey: () => intentProfileKey,
15709
15710
  lastAgentMessage: () => lastAgentMessage,
15710
15711
  looseIntentStoreDir: () => looseIntentStoreDir,
15711
15712
  pruneExpiredSessions: () => pruneExpiredSessions,
@@ -15725,13 +15726,27 @@ import {
15725
15726
  writeFileSync as writeFileSync8
15726
15727
  } from "node:fs";
15727
15728
  import { dirname as dirname10, resolve as resolve17 } from "node:path";
15729
+ function intentProfileKey(profile) {
15730
+ if (validAgentIdentity(profile.agent))
15731
+ return `${profile.agent.integration}.${profile.agent.instanceId}`;
15732
+ return isKnownCli(profile.cli) ? profile.cli : null;
15733
+ }
15734
+ function storeSlot(sessionId, profile) {
15735
+ if (profile === undefined)
15736
+ return { dir: sessionsDir(), file: resolve17(sessionsDir(), `${sessionId}.json`) };
15737
+ const key = intentProfileKey(profile);
15738
+ if (key === null || !PROFILE_DIR_RE.test(key))
15739
+ return null;
15740
+ const dir = resolve17(sessionsDir(), key);
15741
+ return { dir, file: resolve17(dir, `${sessionId}.json`) };
15742
+ }
15728
15743
  function modesAreMeaningful2() {
15729
15744
  return process.platform !== "win32";
15730
15745
  }
15731
- function storeDirChain() {
15746
+ function storeDirChain(innermost = sessionsDir()) {
15732
15747
  const home = resolve17(failproofaiHome());
15733
15748
  const chain = [];
15734
- let dir = sessionsDir();
15749
+ let dir = innermost;
15735
15750
  for (let i = 0;i < 8; i++) {
15736
15751
  chain.push(dir);
15737
15752
  if (dir === home)
@@ -15743,10 +15758,10 @@ function storeDirChain() {
15743
15758
  }
15744
15759
  return chain;
15745
15760
  }
15746
- function looseIntentStoreDir() {
15761
+ function looseIntentStoreDir(innermost) {
15747
15762
  if (!modesAreMeaningful2())
15748
15763
  return null;
15749
- for (const dir of storeDirChain()) {
15764
+ for (const dir of storeDirChain(innermost)) {
15750
15765
  let mode;
15751
15766
  try {
15752
15767
  mode = statSync20(dir).mode & 511;
@@ -15765,23 +15780,22 @@ function looseIntentStoreDir() {
15765
15780
  }
15766
15781
  return null;
15767
15782
  }
15768
- function readIntentFile(sessionId) {
15783
+ function readIntentFile(slot) {
15769
15784
  try {
15770
- const parsed = JSON.parse(readFileSync19(intentFile(sessionId), "utf8"));
15785
+ const parsed = JSON.parse(readFileSync19(slot.file, "utf8"));
15771
15786
  return Array.isArray(parsed?.prompts) ? parsed : { prompts: [] };
15772
15787
  } catch {
15773
15788
  return { prompts: [] };
15774
15789
  }
15775
15790
  }
15776
- function appendPrompt(sessionId, entry) {
15791
+ function appendPrompt(slot, entry) {
15777
15792
  try {
15778
- const file = readIntentFile(sessionId);
15793
+ const file = readIntentFile(slot);
15779
15794
  const last = file.prompts[file.prompts.length - 1];
15780
15795
  const kept = last && last.text === entry.text ? file.prompts.slice(0, -1) : file.prompts;
15781
15796
  file.prompts = [...kept, entry].slice(-MAX_RECORDED_PROMPTS);
15782
- const dir = sessionsDir();
15783
- mkdirSync9(dir, { recursive: true, mode: 448 });
15784
- const target = intentFile(sessionId);
15797
+ mkdirSync9(slot.dir, { recursive: true, mode: 448 });
15798
+ const target = slot.file;
15785
15799
  const isNew = !fileExists2(target);
15786
15800
  const tmp = `${target}.${process.pid}.tmp`;
15787
15801
  writeFileSync8(tmp, JSON.stringify(file), { mode: 384 });
@@ -15802,9 +15816,24 @@ function fileExists2(path) {
15802
15816
  }
15803
15817
  }
15804
15818
  function pruneExpiredSessions(now = Date.now()) {
15819
+ const root = sessionsDir();
15820
+ let removed = pruneDir(root, now);
15821
+ try {
15822
+ for (const name of readdirSync13(root)) {
15823
+ if (!PROFILE_DIR_RE.test(name))
15824
+ continue;
15825
+ const dir = resolve17(root, name);
15826
+ try {
15827
+ if (statSync20(dir).isDirectory())
15828
+ removed += pruneDir(dir, now);
15829
+ } catch {}
15830
+ }
15831
+ } catch {}
15832
+ return removed;
15833
+ }
15834
+ function pruneDir(dir, now) {
15805
15835
  let removed = 0;
15806
15836
  try {
15807
- const dir = sessionsDir();
15808
15837
  for (const name of readdirSync13(dir)) {
15809
15838
  if (!SESSION_FILE_RE3.test(name))
15810
15839
  continue;
@@ -15820,23 +15849,29 @@ function pruneExpiredSessions(now = Date.now()) {
15820
15849
  } catch {}
15821
15850
  return removed;
15822
15851
  }
15823
- function recordPrompt(sessionId, prompt, agent, now) {
15852
+ function recordPrompt(sessionId, profile, prompt, agent, now) {
15824
15853
  if (!SESSION_ID_RE2.test(sessionId))
15825
15854
  return false;
15826
15855
  if (prompt.trim().length === 0)
15827
15856
  return false;
15857
+ const slot = storeSlot(sessionId, profile);
15858
+ if (!slot)
15859
+ return false;
15828
15860
  try {
15829
- return appendPrompt(sessionId, { at: now, text: storable(prompt.trim()), agent: agent === null ? null : storable(agent) });
15861
+ return appendPrompt(slot, { at: now, text: storable(prompt.trim()), agent: agent === null ? null : storable(agent) });
15830
15862
  } catch {
15831
15863
  return false;
15832
15864
  }
15833
15865
  }
15834
- function livePrompts(sessionId, now) {
15866
+ function livePrompts(sessionId, now, profile) {
15835
15867
  if (!sessionId || !SESSION_ID_RE2.test(sessionId))
15836
15868
  return [];
15837
- if (looseIntentStoreDir() !== null)
15869
+ const slot = storeSlot(sessionId, profile);
15870
+ if (!slot)
15838
15871
  return [];
15839
- return readIntentFile(sessionId).prompts.filter((p) => typeof p?.text === "string" && typeof p.at === "number" && now - p.at <= INTENT_MAX_AGE_MS && p.at - now <= MAX_CLOCK_SKEW_MS);
15872
+ if (looseIntentStoreDir(slot.dir) !== null)
15873
+ return [];
15874
+ return readIntentFile(slot).prompts.filter((p) => typeof p?.text === "string" && typeof p.at === "number" && now - p.at <= INTENT_MAX_AGE_MS && p.at - now <= MAX_CLOCK_SKEW_MS);
15840
15875
  }
15841
15876
  function replaceTagBlocks(text, tag, replace, attributes = false) {
15842
15877
  const open = attributes ? `<${tag}` : `<${tag}>`;
@@ -16183,16 +16218,16 @@ function captureIntent(ev, now = Date.now()) {
16183
16218
  const cleaned = cleanHumanTurn(raw);
16184
16219
  if (cleaned === null)
16185
16220
  return;
16186
- recordPrompt(sessionId, cleaned, lastAgentMessage(ev.transcriptPath), now);
16221
+ recordPrompt(sessionId, ev.profile, cleaned, lastAgentMessage(ev.transcriptPath), now);
16187
16222
  } catch {}
16188
16223
  }
16189
- function readIntent(sessionId, now = Date.now()) {
16190
- const prompts = livePrompts(sessionId, now);
16224
+ function readIntent(sessionId, now = Date.now(), profile) {
16225
+ const prompts = livePrompts(sessionId, now, profile);
16191
16226
  const latest = prompts[prompts.length - 1];
16192
16227
  const agent = latest && typeof latest.agent === "string" && latest.agent.length > 0 ? latest.agent : null;
16193
16228
  return { userSaid: prompts.map((p) => p.text), agentLastMessage: agent };
16194
16229
  }
16195
- var MAX_RECORDED_PROMPTS = 5, INTENT_MAX_AGE_MS, MAX_CLOCK_SKEW_MS, SESSION_ID_RE2, sessionsDir = () => resolve17(semanticDir(), "sessions"), intentFile = (sessionId) => resolve17(sessionsDir(), `${sessionId}.json`), DIR_WRITABLE_BY_OTHERS3 = 18, SESSION_FILE_RE3, NON_HUMAN_PREFIXES, CONTINUATION_PREFIX = "This session is being continued from a previous conversation", IDE_MACHINE_OPENERS, IDE_AMBIGUOUS_OPENERS, IDE_REQUEST_HEADINGS, obj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : undefined, CLAUDE_MACHINE_SOURCES, claudeMachineTurn = (payload) => typeof payload.source === "string" && CLAUDE_MACHINE_SOURCES.has(payload.source), piMachineTurn = (payload) => payload.input_source === "extension" || payload.source === "extension", openclawMachineRun = (payload) => {
16230
+ var MAX_RECORDED_PROMPTS = 5, INTENT_MAX_AGE_MS, MAX_CLOCK_SKEW_MS, SESSION_ID_RE2, sessionsDir = () => resolve17(semanticDir(), "sessions"), PROFILE_DIR_RE, DIR_WRITABLE_BY_OTHERS3 = 18, SESSION_FILE_RE3, NON_HUMAN_PREFIXES, CONTINUATION_PREFIX = "This session is being continued from a previous conversation", IDE_MACHINE_OPENERS, IDE_AMBIGUOUS_OPENERS, IDE_REQUEST_HEADINGS, obj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : undefined, CLAUDE_MACHINE_SOURCES, claudeMachineTurn = (payload) => typeof payload.source === "string" && CLAUDE_MACHINE_SOURCES.has(payload.source), piMachineTurn = (payload) => payload.input_source === "extension" || payload.source === "extension", openclawMachineRun = (payload) => {
16196
16231
  const meta = obj(payload.openclaw);
16197
16232
  if (!meta)
16198
16233
  return false;
@@ -16208,9 +16243,11 @@ var MAX_RECORDED_PROMPTS = 5, INTENT_MAX_AGE_MS, MAX_CLOCK_SKEW_MS, SESSION_ID_R
16208
16243
  var init_intent = __esm(() => {
16209
16244
  init_fp_home();
16210
16245
  init_envelope();
16246
+ init_agent_targets();
16211
16247
  INTENT_MAX_AGE_MS = 6 * 60 * 60 * 1000;
16212
16248
  MAX_CLOCK_SKEW_MS = 5 * 60 * 1000;
16213
16249
  SESSION_ID_RE2 = /^[A-Za-z0-9._-]{1,128}$/;
16250
+ PROFILE_DIR_RE = /^[a-z]+(?:\.agt_[0-9a-f]{16,32})?$/;
16214
16251
  SESSION_FILE_RE3 = /^[A-Za-z0-9._-]{1,128}\.json(?:\.\d+\.tmp)?$/;
16215
16252
  NON_HUMAN_PREFIXES = [
16216
16253
  "<local-command-caveat>",
@@ -17939,7 +17976,10 @@ function startJevReview(cfg, call) {
17939
17976
  }
17940
17977
  let intent;
17941
17978
  try {
17942
- intent = intentReader.readIntent(call.sessionId);
17979
+ intent = intentReader.readIntent(call.sessionId, undefined, {
17980
+ cli: call.cli,
17981
+ agent: call.agent !== undefined ? call.agent : call.cloud?.agent ?? null
17982
+ });
17943
17983
  } catch {
17944
17984
  intent = { userSaid: [], agentLastMessage: null };
17945
17985
  }
@@ -18351,6 +18391,7 @@ async function startTwoTier(canonicalEventType, parsed, session, cli, opts, acti
18351
18391
  permissionMode: session.permissionMode,
18352
18392
  sessionId: session.sessionId,
18353
18393
  cli,
18394
+ agent,
18354
18395
  ...loaded.cloud ? { cloud: loaded.cloud } : {},
18355
18396
  ...activePause ? { localPaused: true } : {}
18356
18397
  });
@@ -18377,13 +18418,16 @@ function jevTelemetry(activity) {
18377
18418
  return {};
18378
18419
  }
18379
18420
  }
18380
- async function captureJevIntent(canonicalEventType, decision, parsed, session, cli, opts) {
18421
+ async function captureJevIntent(canonicalEventType, decision, parsed, session, cli, opts, agent) {
18381
18422
  if (canonicalEventType !== "UserPromptSubmit" || jevForcedOff(opts))
18382
18423
  return;
18383
18424
  try {
18384
- if (!cloudJevAsks(readCloudJevMode()) && !jevChecksInstalled())
18425
+ const cloudAsks = cloudJevAsks(readCloudJevMode());
18426
+ if (!cloudAsks && !jevChecksInstalled())
18427
+ return;
18428
+ if (cloudAsks && cloudJevAssignmentsFor(agent)?.length === 0 && !packJevChecksInstalled())
18385
18429
  return;
18386
- if (!await readJevConfig())
18430
+ if (!await readJevConfig(agent))
18387
18431
  return;
18388
18432
  if (decision === "deny") {
18389
18433
  await Promise.resolve().then(() => init_enforcement_capability());
@@ -18397,7 +18441,8 @@ async function captureJevIntent(canonicalEventType, decision, parsed, session, c
18397
18441
  transcriptPath: session.transcriptPath,
18398
18442
  cli,
18399
18443
  payload: parsed,
18400
- prompt: parsed.prompt
18444
+ prompt: parsed.prompt,
18445
+ profile: { cli, agent }
18401
18446
  };
18402
18447
  captureIntent(event);
18403
18448
  } catch (err) {
@@ -18663,7 +18708,7 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
18663
18708
  }
18664
18709
  const durationMs = Math.round(performance.now() - startTime);
18665
18710
  hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
18666
- await captureJevIntent(canonicalEventType, result.decision, parsed, session, cli, opts);
18711
+ await captureJevIntent(canonicalEventType, result.decision, parsed, session, cli, opts, runtimeAgent);
18667
18712
  const matchedPolicies = matchedBeforeRelease ?? getPoliciesForEvent(canonicalEventType, parsed.tool_name).map((p) => p.name);
18668
18713
  const jevOrigin = result.policyName && result.twoTier?.decidedByJev ? result.twoTier.jevOrigin : undefined;
18669
18714
  const activityEntry = {
@@ -1,5 +1,5 @@
1
1
  name: failproofai
2
- version: 1.0.12-beta.2
2
+ version: 1.0.12
3
3
  description: Real-time FailproofAI policy enforcement for Hermes
4
4
  manifest_version: 2
5
5
  api_version: 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "1.0.12-beta.2",
3
+ "version": "1.0.12",
4
4
  "description": "Observability and enforcement for AI agent harnesses. 40 built-in policies hooked into 12 of them — Claude Code, Codex, Cursor, Hermes, OpenClaw and more — blocking the tool call before it runs. Local dashboard included, no account needed.",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs",
@@ -126,15 +126,15 @@
126
126
  "browserslist": "4.28.8"
127
127
  },
128
128
  "optionalDependencies": {
129
- "@failproofai/failproofaid-linux-x64": "1.0.12-beta.2",
130
- "@failproofai/failproofaid-linux-arm64": "1.0.12-beta.2",
131
- "@failproofai/failproofaid-darwin-x64": "1.0.12-beta.2",
132
- "@failproofai/failproofaid-darwin-arm64": "1.0.12-beta.2"
129
+ "@failproofai/failproofaid-linux-x64": "1.0.12",
130
+ "@failproofai/failproofaid-linux-arm64": "1.0.12",
131
+ "@failproofai/failproofaid-darwin-x64": "1.0.12",
132
+ "@failproofai/failproofaid-darwin-arm64": "1.0.12"
133
133
  },
134
134
  "failproofaidBinaries": {
135
- "linux-x64": "4c09117fdb80d87792d1d2b77d9359cdbd0c522b5a6222ab7b927659130ddf54",
136
- "linux-arm64": "dd390d76561267fc519ceb96fb90be340044e2f564bef36a44e284f227d93f29",
137
- "darwin-x64": "62381f7ffe21d02289c36c4b205166b2867dbde986444d1ea185241b94fd04ef",
138
- "darwin-arm64": "9ad798817d3048a5126e80dc71883538d37c1b6b6443e9c19389dc2583c8f675"
135
+ "linux-x64": "3b379ce2b03dd8a74393daed2c3120309f591fe6433e0c939d70c7b20da55be7",
136
+ "linux-arm64": "bd77d7fee68dc2723fb57cc9c7fcf957b7e939c797e61b099c412263880aa16e",
137
+ "darwin-x64": "fce3faab9406b1c2267955f0e07d04ef7a2b7a0b007e12ecd144fcfef563ebc8",
138
+ "darwin-arm64": "765732f8f64aa3d3dbd98e93b1aba71fc02956f65baeb73b08447b37fac3a17b"
139
139
  }
140
140
  }
@@ -65,6 +65,9 @@ For focused work, read the matching skill too. Each has the same layout, with it
65
65
  - FailproofAI Cloud and the \`fp\` CLI: ${SKILLS_RAW}/fp-cloud-cli/SKILL.md
66
66
  - Adding the SDK to an agent, or running an evaluator: ${SKILLS_RAW}/failproofai-sdk/SKILL.md
67
67
  - Planning evaluations: ${SKILLS_RAW}/failproofai-eval-brainstorm/SKILL.md
68
+ - Writing an evaluation: ${SKILLS_RAW}/failproofai-eval-author/SKILL.md
69
+ - Writing an alert: ${SKILLS_RAW}/failproofai-alert-author/SKILL.md
70
+ - Writing an audit: ${SKILLS_RAW}/failproofai-audit-author/SKILL.md
68
71
  `;
69
72
 
70
73
  /** An agent's own override for where it keeps its files, else the default. */
@@ -410,7 +410,7 @@ export const collectorHealthFile = (home?: string) => resolve(stateDir(home), "c
410
410
  /** Per-session enforcement pauses, keyed by a hash of the session id. */
411
411
  export const sessionPauseDir = () => resolve(stateDir(), "sessions");
412
412
  /**
413
- * The semantic (Jev) evaluator's local state: `sessions/<id>.json` holds the
413
+ * The semantic (Jev) evaluator's local state: `sessions/<profile>/<id>.json` holds the
414
414
  * human prompts recorded at `UserPromptSubmit`, and `verdicts.jsonl` one row
415
415
  * per semantic evaluation. Written only when a Jev config (BYOK) is present.
416
416
  */
@@ -505,6 +505,8 @@ async function startTwoTier(
505
505
  permissionMode: session.permissionMode,
506
506
  sessionId: session.sessionId,
507
507
  cli,
508
+ // The profile the intent store is read under: the one capture used.
509
+ agent,
508
510
  ...(loaded.cloud ? { cloud: loaded.cloud } : {}),
509
511
  // Cloud's checks are still asked; the installed packs' are not.
510
512
  ...(activePause ? { localPaused: true } : {}),
@@ -571,6 +573,15 @@ function jevTelemetry(activity: JevActivityFields | undefined): Record<string, u
571
573
  * marks that tell a human's prompt from a subagent's or an extension's are
572
574
  * elsewhere in it. `prompt` is §7's original field, still read by the
573
575
  * contract stub; T4's implementation reads `payload` and ignores it.
576
+ *
577
+ * The prompt is stored under the agent profile (`agent`, the runtime identity
578
+ * Cloud's targeting matches on) and read back by `startTwoTier` under the same
579
+ * one, so a prompt typed into one profile is never another's stated intent.
580
+ * Under a Cloud mode, a profile the deployment assigns no Cloud Jev check to,
581
+ * with no installed pack's check to review it either, has nothing that will
582
+ * ever read the prompt (`startTwoTier` returns before Jev for it), so nothing
583
+ * is kept: FailproofAI does not hold prompt text from a profile the
584
+ * deployment did not target.
574
585
  */
575
586
  async function captureJevIntent(
576
587
  canonicalEventType: string,
@@ -579,13 +590,20 @@ async function captureJevIntent(
579
590
  session: SessionMetadata,
580
591
  cli: IntegrationType,
581
592
  opts: EvaluateHookEventOptions | undefined,
593
+ agent: AgentIdentity | null,
582
594
  ): Promise<void> {
583
595
  if (canonicalEventType !== "UserPromptSubmit" || jevForcedOff(opts)) return;
584
596
  try {
585
597
  // Same gate as `startTwoTier`: a Cloud mode that asks needs what the human
586
598
  // typed whatever the installed packs declare.
587
- if (!cloudJevAsks(readCloudJevMode()) && !jevChecksInstalled()) return;
588
- if (!(await readJevConfig())) return;
599
+ const cloudAsks = cloudJevAsks(readCloudJevMode());
600
+ if (!cloudAsks && !jevChecksInstalled()) return;
601
+ // ...but not for a profile no Cloud Jev check targets and no installed
602
+ // pack's check reviews: `startTwoTier`'s own skip, without the pause
603
+ // (a pause can end while the session goes on). An older deployment with
604
+ // no assignment list (`null`) captures as before.
605
+ if (cloudAsks && cloudJevAssignmentsFor(agent)?.length === 0 && !packJevChecksInstalled()) return;
606
+ if (!(await readJevConfig(agent))) return;
589
607
  if (decision === "deny") {
590
608
  const { ENFORCEMENT_CAPABILITY } = await import("./enforcement-capability");
591
609
  if (ENFORCEMENT_CAPABILITY[cli]?.UserPromptSubmit === "block") return;
@@ -599,6 +617,7 @@ async function captureJevIntent(
599
617
  cli,
600
618
  payload: parsed,
601
619
  prompt: parsed.prompt,
620
+ profile: { cli, agent },
602
621
  };
603
622
  captureIntent(event);
604
623
  } catch (err) {
@@ -1180,7 +1199,7 @@ export async function evaluateHookEvent(
1180
1199
  const durationMs = Math.round(performance.now() - startTime);
1181
1200
  hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
1182
1201
 
1183
- await captureJevIntent(canonicalEventType, result.decision, parsed, session, cli, opts);
1202
+ await captureJevIntent(canonicalEventType, result.decision, parsed, session, cli, opts, runtimeAgent);
1184
1203
 
1185
1204
  // Which policies actually ran for this event, regardless of how they
1186
1205
  // decided. `result.policyName` names only the decider — null on a plain
@@ -81,6 +81,26 @@
81
81
  * Known limits.)
82
82
  *
83
83
  * Text inside a tool call claiming "the user approved this" is never consulted.
84
+ *
85
+ * WHOSE SESSION IT IS. Prompts are stored per AGENT PROFILE and session id
86
+ * (`sessions/<profile>/<sessionId>.json`), the profile being the runtime agent
87
+ * identity FailproofAI Cloud's agent targeting uses (`AgentIdentity`, read by
88
+ * the handler from the daemon's roster) — `<integration>.<instanceId>`, or the
89
+ * bare integration name when the profile has no rostered identity. Session ids
90
+ * are chosen by the harness, not by us, so two profiles can submit the same
91
+ * one; keyed by session id alone, a prompt typed into a profile a Cloud Jev
92
+ * deployment does NOT target could be read back as the stated intent of one it
93
+ * does, and clear a reviewable deny there. The hook path always names the
94
+ * profile on both sides (`captureIntent`'s `profile`, `readIntent`'s), so a
95
+ * prompt is only ever read by the profile it was typed into.
96
+ *
97
+ * The flat `sessions/<sessionId>.json` layout that came before is the store a
98
+ * caller that names NO profile still reads and writes (library and test use).
99
+ * The hook path never reads it: such a file cannot say which profile wrote it,
100
+ * so a pre-upgrade entry is ignored rather than guessed at, and expires under
101
+ * the six-hour sweep like any other. The cost is bounded and fail-closed: a
102
+ * session in flight across the upgrade judges with no stated intent until its
103
+ * next prompt is recorded.
84
104
  */
85
105
  import {
86
106
  chmodSync,
@@ -99,6 +119,7 @@ import { dirname, resolve } from "node:path";
99
119
  import { failproofaiHome, semanticDir } from "../fp-home";
100
120
  import type { IntegrationType } from "../types";
101
121
  import { MAX_USER_MESSAGE_CHARS, redactSecrets } from "./envelope";
122
+ import { validAgentIdentity, type AgentIdentity } from "../agent-targets";
102
123
 
103
124
  export const MAX_RECORDED_PROMPTS = 5;
104
125
  /** Older than this and a prompt no longer describes what the agent is doing. */
@@ -109,7 +130,51 @@ const MAX_CLOCK_SKEW_MS = 5 * 60 * 1000;
109
130
  const SESSION_ID_RE = /^[A-Za-z0-9._-]{1,128}$/;
110
131
 
111
132
  const sessionsDir = (): string => resolve(semanticDir(), "sessions");
112
- const intentFile = (sessionId: string): string => resolve(sessionsDir(), `${sessionId}.json`);
133
+
134
+ /**
135
+ * The agent profile a prompt belongs to: the harness, and the runtime agent
136
+ * identity the handler resolved for it (`readRuntimeAgentIdentity`) — the same
137
+ * identity Cloud Jev's agent targeting matches on. `agent` null or absent is a
138
+ * profile with no rostered identity, which targeting treats as "unknown" and
139
+ * which is kept apart from every rostered one.
140
+ */
141
+ export interface IntentProfile {
142
+ cli: string;
143
+ agent?: AgentIdentity | null;
144
+ }
145
+
146
+ /** A profile directory's name: `<integration>` or `<integration>.agt_<hex>`. */
147
+ const PROFILE_DIR_RE = /^[a-z]+(?:\.agt_[0-9a-f]{16,32})?$/;
148
+
149
+ /**
150
+ * The directory name a profile's prompts live under, or null when the profile
151
+ * cannot be named (an unknown harness): nothing is then recorded or read.
152
+ * Injective: integration names are lowercase letters and instance ids carry
153
+ * their own `agt_` prefix, so no two profiles share a directory.
154
+ */
155
+ export function intentProfileKey(profile: IntentProfile): string | null {
156
+ if (validAgentIdentity(profile.agent)) return `${profile.agent.integration}.${profile.agent.instanceId}`;
157
+ return isKnownCli(profile.cli) ? profile.cli : null;
158
+ }
159
+
160
+ /** Where one session's prompts are kept: the directory and the file in it. */
161
+ interface StoreSlot {
162
+ dir: string;
163
+ file: string;
164
+ }
165
+
166
+ /**
167
+ * The slot for `sessionId` under `profile`. No profile at all (`undefined`) is
168
+ * the flat, pre-profile layout — never used by the hook path (see the header).
169
+ * A profile that cannot be named has no slot.
170
+ */
171
+ function storeSlot(sessionId: string, profile: IntentProfile | undefined): StoreSlot | null {
172
+ if (profile === undefined) return { dir: sessionsDir(), file: resolve(sessionsDir(), `${sessionId}.json`) };
173
+ const key = intentProfileKey(profile);
174
+ if (key === null || !PROFILE_DIR_RE.test(key)) return null;
175
+ const dir = resolve(sessionsDir(), key);
176
+ return { dir, file: resolve(dir, `${sessionId}.json`) };
177
+ }
113
178
 
114
179
  /**
115
180
  * Group- or world-WRITE bits on a directory the store sits under. Anyone who
@@ -140,11 +205,11 @@ function modesAreMeaningful(): boolean {
140
205
  * (`fpai-collect`'s `create_dir_all`) and arrives at the umask, which on a
141
206
  * umask-002 box is 0775.
142
207
  */
143
- function storeDirChain(): string[] {
208
+ function storeDirChain(innermost: string = sessionsDir()): string[] {
144
209
  const home = resolve(failproofaiHome());
145
210
  const chain: string[] = [];
146
- let dir = sessionsDir();
147
- // Bounded: the chain is four deep, and a home that is not an ancestor (a
211
+ let dir = innermost;
212
+ // Bounded: the chain is five deep, and a home that is not an ancestor (a
148
213
  // layout change, a symlink) must not walk to the filesystem root.
149
214
  for (let i = 0; i < 8; i++) {
150
215
  chain.push(dir);
@@ -172,11 +237,12 @@ function storeDirChain(): string[] {
172
237
  * human's and could clear the four reviewable BLOCKING policies.
173
238
  *
174
239
  * Exported so `failproofai jev status` can report it the way it reports a
175
- * refused config.
240
+ * refused config. `innermost` is a profile's directory when the read is for
241
+ * one, so that directory is held to the same rule.
176
242
  */
177
- export function looseIntentStoreDir(): string | null {
243
+ export function looseIntentStoreDir(innermost?: string): string | null {
178
244
  if (!modesAreMeaningful()) return null;
179
- for (const dir of storeDirChain()) {
245
+ for (const dir of storeDirChain(innermost)) {
180
246
  let mode: number;
181
247
  try {
182
248
  mode = statSync(dir).mode & 0o777;
@@ -207,9 +273,9 @@ interface IntentFile {
207
273
  prompts: RecordedPrompt[];
208
274
  }
209
275
 
210
- function readIntentFile(sessionId: string): IntentFile {
276
+ function readIntentFile(slot: StoreSlot): IntentFile {
211
277
  try {
212
- const parsed = JSON.parse(readFileSync(intentFile(sessionId), "utf8")) as IntentFile;
278
+ const parsed = JSON.parse(readFileSync(slot.file, "utf8")) as IntentFile;
213
279
  return Array.isArray(parsed?.prompts) ? parsed : { prompts: [] };
214
280
  } catch {
215
281
  return { prompts: [] };
@@ -228,15 +294,14 @@ function readIntentFile(sessionId: string): IntentFile {
228
294
  * copies of one line. The replacement carries the new timestamp and the new
229
295
  * agent snapshot, so the entry stays the latest thing the human said.
230
296
  */
231
- function appendPrompt(sessionId: string, entry: RecordedPrompt): boolean {
297
+ function appendPrompt(slot: StoreSlot, entry: RecordedPrompt): boolean {
232
298
  try {
233
- const file = readIntentFile(sessionId);
299
+ const file = readIntentFile(slot);
234
300
  const last = file.prompts[file.prompts.length - 1];
235
301
  const kept = last && last.text === entry.text ? file.prompts.slice(0, -1) : file.prompts;
236
302
  file.prompts = [...kept, entry].slice(-MAX_RECORDED_PROMPTS);
237
- const dir = sessionsDir();
238
- mkdirSync(dir, { recursive: true, mode: 0o700 });
239
- const target = intentFile(sessionId);
303
+ mkdirSync(slot.dir, { recursive: true, mode: 0o700 });
304
+ const target = slot.file;
240
305
  const isNew = !fileExists(target);
241
306
  const tmp = `${target}.${process.pid}.tmp`;
242
307
  writeFileSync(tmp, JSON.stringify(file), { mode: 0o600 });
@@ -265,12 +330,31 @@ const SESSION_FILE_RE = /^[A-Za-z0-9._-]{1,128}\.json(?:\.\d+\.tmp)?$/;
265
330
  * file's mtime is its newest recorded prompt's, so every prompt in such a
266
331
  * file is already outside the window every read filters by and can never be
267
332
  * read again. Removing it loses nothing and keeps one-file-per-session from
268
- * growing without bound.
333
+ * growing without bound. Swept: the flat layout's files (pre-profile entries
334
+ * among them) and every profile directory's, one level down.
269
335
  */
270
336
  export function pruneExpiredSessions(now: number = Date.now()): number {
337
+ const root = sessionsDir();
338
+ let removed = pruneDir(root, now);
339
+ try {
340
+ for (const name of readdirSync(root)) {
341
+ if (!PROFILE_DIR_RE.test(name)) continue;
342
+ const dir = resolve(root, name);
343
+ try {
344
+ if (statSync(dir).isDirectory()) removed += pruneDir(dir, now);
345
+ } catch {
346
+ // Raced with something else: nothing to do.
347
+ }
348
+ }
349
+ } catch {
350
+ // No directory yet.
351
+ }
352
+ return removed;
353
+ }
354
+
355
+ function pruneDir(dir: string, now: number): number {
271
356
  let removed = 0;
272
357
  try {
273
- const dir = sessionsDir();
274
358
  for (const name of readdirSync(dir)) {
275
359
  if (!SESSION_FILE_RE.test(name)) continue;
276
360
  const path = resolve(dir, name);
@@ -301,11 +385,19 @@ export function pruneExpiredSessions(now: number = Date.now()): number {
301
385
  * leave half a secret the patterns no longer match. Never throws: losing intent
302
386
  * only means no override.
303
387
  */
304
- function recordPrompt(sessionId: string, prompt: string, agent: string | null, now: number): boolean {
388
+ function recordPrompt(
389
+ sessionId: string,
390
+ profile: IntentProfile | undefined,
391
+ prompt: string,
392
+ agent: string | null,
393
+ now: number,
394
+ ): boolean {
305
395
  if (!SESSION_ID_RE.test(sessionId)) return false;
306
396
  if (prompt.trim().length === 0) return false;
397
+ const slot = storeSlot(sessionId, profile);
398
+ if (!slot) return false;
307
399
  try {
308
- return appendPrompt(sessionId, { at: now, text: storable(prompt.trim()), agent: agent === null ? null : storable(agent) });
400
+ return appendPrompt(slot, { at: now, text: storable(prompt.trim()), agent: agent === null ? null : storable(agent) });
309
401
  } catch {
310
402
  return false;
311
403
  }
@@ -319,10 +411,12 @@ function recordPrompt(sessionId: string, prompt: string, agent: string | null, n
319
411
  * still asked, and judges with no stated intent, so no reviewable deny is
320
412
  * cleared on a prompt we cannot say is the owner's.
321
413
  */
322
- function livePrompts(sessionId: string | undefined, now: number): RecordedPrompt[] {
414
+ function livePrompts(sessionId: string | undefined, now: number, profile: IntentProfile | undefined): RecordedPrompt[] {
323
415
  if (!sessionId || !SESSION_ID_RE.test(sessionId)) return [];
324
- if (looseIntentStoreDir() !== null) return [];
325
- return readIntentFile(sessionId).prompts.filter(
416
+ const slot = storeSlot(sessionId, profile);
417
+ if (!slot) return [];
418
+ if (looseIntentStoreDir(slot.dir) !== null) return [];
419
+ return readIntentFile(slot).prompts.filter(
326
420
  (p) =>
327
421
  typeof p?.text === "string" &&
328
422
  typeof p.at === "number" &&
@@ -839,6 +933,13 @@ export interface CaptureEvent {
839
933
  cli: string;
840
934
  /** The stdin payload after `normalizeCliPayload`: the handler's `parsed`. */
841
935
  payload: Record<string, unknown>;
936
+ /**
937
+ * The agent profile the prompt was typed into. The hook path always sets it
938
+ * (the agent null when the profile has no rostered identity), and the prompt
939
+ * is then readable only under that same profile. Absent is the flat layout
940
+ * (see the header).
941
+ */
942
+ profile?: IntentProfile;
842
943
  }
843
944
 
844
945
  function isKnownCli(cli: string): cli is IntegrationType {
@@ -1167,7 +1268,7 @@ export function captureIntent(ev: CaptureEvent, now: number = Date.now()): void
1167
1268
  if (raw === null) return;
1168
1269
  const cleaned = cleanHumanTurn(raw);
1169
1270
  if (cleaned === null) return;
1170
- recordPrompt(sessionId, cleaned, lastAgentMessage(ev.transcriptPath), now);
1271
+ recordPrompt(sessionId, ev.profile, cleaned, lastAgentMessage(ev.transcriptPath), now);
1171
1272
  } catch {
1172
1273
  // Losing intent only means Jev judges without it.
1173
1274
  }
@@ -1177,13 +1278,15 @@ export function captureIntent(ev: CaptureEvent, now: number = Date.now()): void
1177
1278
  * What the human asked for recently (oldest first, at most
1178
1279
  * `MAX_RECORDED_PROMPTS`, none older than `INTENT_MAX_AGE_MS`) and the agent
1179
1280
  * message their latest prompt replied to — null when that prompt had none or
1180
- * nothing is recorded.
1281
+ * nothing is recorded. Read under `profile`, the one the prompts were captured
1282
+ * under; the hook path always passes it (see the header).
1181
1283
  */
1182
1284
  export function readIntent(
1183
1285
  sessionId?: string,
1184
1286
  now: number = Date.now(),
1287
+ profile?: IntentProfile,
1185
1288
  ): { userSaid: string[]; agentLastMessage: string | null } {
1186
- const prompts = livePrompts(sessionId, now);
1289
+ const prompts = livePrompts(sessionId, now, profile);
1187
1290
  const latest = prompts[prompts.length - 1];
1188
1291
  const agent = latest && typeof latest.agent === "string" && latest.agent.length > 0 ? latest.agent : null;
1189
1292
  return { userSaid: prompts.map((p) => p.text), agentLastMessage: agent };