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
@@ -0,0 +1,380 @@
1
+ """Author, test and deploy managed evaluators, independently of result readers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from datetime import datetime, timedelta, timezone
8
+ from typing import List, Optional
9
+
10
+ import typer
11
+
12
+ from .. import client as api, cloud_output, dates, evaluator_spec as spec, output
13
+ from .._context import GLOBALS_EPILOG, require_auth
14
+ from ..errors import ApiError, NotFoundError
15
+ from . import _cloud, _write
16
+
17
+ PATH = "/api/evaluator-definitions"
18
+
19
+
20
+ def definitions(cctx) -> list:
21
+ return _cloud.rows(api.cloud_request(cctx, "GET", PATH), "definitions")
22
+
23
+
24
+ def select(items: list, handle: str) -> dict:
25
+ key, _, version = handle.partition("@")
26
+ matches = [r for r in items if r.get("eval_key") == key and (not version or str(r.get("eval_version")) == version)]
27
+ if not matches:
28
+ raise NotFoundError(f"no evaluator {handle}; run `fp evaluators list --all`")
29
+ if not version:
30
+ enabled = [r for r in matches if r.get("enabled", True)]
31
+ matches = enabled or matches
32
+ if len(matches) != 1:
33
+ spec.fail(f"{handle} matches multiple versions; use KEY@VERSION")
34
+ return matches[0]
35
+
36
+
37
+ def list_defs(ctx: typer.Context, key: Optional[str] = typer.Option(None, "--key"),
38
+ all_versions: bool = typer.Option(False, "--all")) -> None:
39
+ """List definitions (enabled only unless --all). Needs evaluations:read."""
40
+ items = definitions(require_auth(ctx.obj))
41
+ items = [r for r in items if (not key or r.get("eval_key") == key) and (all_versions or r.get("enabled", True))]
42
+ _cloud.emit(ctx.obj, items, "Evaluators")
43
+
44
+
45
+ def show(ctx: typer.Context, handle: str = typer.Argument(..., metavar="KEY[@VERSION]")) -> None:
46
+ """Show one definition, including its full source."""
47
+ _cloud.emit(ctx.obj, select(definitions(require_auth(ctx.obj)), handle), "Evaluator")
48
+
49
+
50
+ def _create(state, cctx, body, sessions, disabled=False, force=False) -> dict:
51
+ if sessions:
52
+ if spec.source_kind(body) == "jev":
53
+ output.warn("Jev dry-runs spend model budget.")
54
+ preview = api.cloud_request(cctx, "POST", f"{PATH}/dry-run", body=spec.dry_run_body(body, sessions))
55
+ if not state.json:
56
+ cloud_output.dry_run(preview)
57
+ if dry_run_failed(preview) and force:
58
+ output.warn("Dry-run failed; --force authorizes creating the evaluator anyway.")
59
+ else:
60
+ check_dry_run(preview)
61
+ _write.confirm(state, f"Create evaluator {body['eval_key']}@{body['eval_version']}", assume_yes=False)
62
+ row = api.cloud_request(cctx, "POST", PATH, body=body)
63
+ if not isinstance(row, dict) or not row.get("id"):
64
+ raise ApiError("Cloud created no identifiable evaluator; check evaluators list --all before retrying")
65
+ if disabled:
66
+ api.cloud_request(cctx, "PATCH", f"{PATH}/{row['id']}", body={"enabled": False})
67
+ row = {**row, "enabled": False}
68
+ _write.record_action("evaluator_created", resource="evaluator", mode=spec.source_kind(body), success=True)
69
+ return row
70
+
71
+
72
+ def create(
73
+ ctx: typer.Context,
74
+ key: Optional[str] = typer.Option(None, "--key"),
75
+ name: Optional[str] = typer.Option(None, "--name"),
76
+ version: Optional[str] = typer.Option(None, "--version"),
77
+ label: Optional[List[str]] = typer.Option(None, "--label"),
78
+ condition: Optional[str] = typer.Option(None, "--condition", help="Python expression or @file."),
79
+ code: Optional[str] = typer.Option(None, "--code", help="Restricted Python expression file, or - for stdin."),
80
+ result_kind: Optional[str] = typer.Option(None, "--result-kind"),
81
+ timeout: Optional[float] = typer.Option(None, "--timeout"),
82
+ judge: bool = typer.Option(False, "--judge"),
83
+ criteria: Optional[str] = typer.Option(None, "--criteria"),
84
+ step: Optional[List[str]] = typer.Option(None, "--step"),
85
+ threshold: Optional[float] = typer.Option(None, "--threshold"),
86
+ jev: Optional[str] = typer.Option(None, "--jev", help="noul or score."),
87
+ instructions: Optional[str] = typer.Option(None, "--instructions"),
88
+ true: Optional[str] = typer.Option(None, "--true"),
89
+ false: Optional[str] = typer.Option(None, "--false"),
90
+ level: Optional[List[str]] = typer.Option(None, "--level", help="3–5 distinct levels, worst to best."),
91
+ file: Optional[str] = typer.Option(None, "--file"),
92
+ dry_run_on: Optional[List[str]] = typer.Option(None, "--dry-run-on"),
93
+ disabled: bool = typer.Option(False, "--disabled"),
94
+ force: bool = typer.Option(False, "--force", help="Create despite failed --dry-run-on results (review the failures first)."),
95
+ ) -> None:
96
+ """Create a code, LLM judge or Jev evaluator (enabled by default).
97
+
98
+ Needs evaluations:trigger; session dry-run also needs events:read.
99
+
100
+ Code: `fp --json evaluators create --key concise --name Concise --code concise.py --result-kind score --dry-run-on run-001`
101
+
102
+ Judge: `fp --json evaluators create --key relevance --name Relevance --judge --criteria "Grade relevance to the question" --step "Assess answer specificity" --threshold 0.7`
103
+
104
+ Jev: `fp --json evaluators create --key resolved --name Resolved --jev noul --instructions "Assess whether the request was resolved" --true "Fully resolved" --false "Not resolved" --threshold 0.8`
105
+
106
+ Spec: `fp --json evaluators create --file evaluator.json`
107
+ """
108
+ body = spec.build(
109
+ key=key, name=name, version=version, label=label, condition=condition,
110
+ code=code, result_kind=result_kind, timeout=timeout, judge=judge, criteria=criteria,
111
+ step=step, threshold_value=threshold, jev=jev, instructions=instructions,
112
+ true=true, false=false, level=level, file=file,
113
+ )
114
+ # Validate before any request, including the judge refusal / session bound.
115
+ if dry_run_on:
116
+ spec.dry_run_body(body, dry_run_on)
117
+ row = _create(ctx.obj, require_auth(ctx.obj), body, dry_run_on, disabled, force)
118
+ if ctx.obj.json:
119
+ output.emit_json(row)
120
+ else:
121
+ cloud_output.evaluator({**body, **row}, "Evaluator created")
122
+
123
+
124
+ def deploy(
125
+ ctx: typer.Context,
126
+ key: str = typer.Argument(..., metavar="EVAL_KEY"),
127
+ name: Optional[str] = typer.Option(None, "--name"),
128
+ version: Optional[str] = typer.Option(None, "--version"),
129
+ label: Optional[List[str]] = typer.Option(None, "--label"),
130
+ condition: Optional[str] = typer.Option(None, "--condition"),
131
+ code: Optional[str] = typer.Option(None, "--code"),
132
+ result_kind: Optional[str] = typer.Option(None, "--result-kind"),
133
+ timeout: Optional[float] = typer.Option(None, "--timeout"),
134
+ judge: bool = typer.Option(False, "--judge"),
135
+ criteria: Optional[str] = typer.Option(None, "--criteria"),
136
+ step: Optional[List[str]] = typer.Option(None, "--step"),
137
+ threshold: Optional[float] = typer.Option(None, "--threshold"),
138
+ jev: Optional[str] = typer.Option(None, "--jev"),
139
+ instructions: Optional[str] = typer.Option(None, "--instructions"),
140
+ true: Optional[str] = typer.Option(None, "--true"),
141
+ false: Optional[str] = typer.Option(None, "--false"),
142
+ level: Optional[List[str]] = typer.Option(None, "--level"),
143
+ file: Optional[str] = typer.Option(None, "--file"),
144
+ dry_run_on: Optional[List[str]] = typer.Option(None, "--dry-run-on"),
145
+ backfill: Optional[str] = typer.Option(None, "--backfill", help="Score stored sessions too, e.g. 7d (up to 90d). Judge/Jev backfills spend model budget."),
146
+ force: bool = typer.Option(False, "--force", help="Deploy despite failed --dry-run-on results (review the failures first)."),
147
+ ) -> None:
148
+ """Publish a NEW immutable version, then disable enabled predecessors.
149
+
150
+ Numeric versions auto-increment; for other version names pass --version.
151
+ A failed predecessor PATCH leaves the new version enabled and reports its id.
152
+ """
153
+ body = spec.build(
154
+ key=None if file else key, name=name, version=None if file else version, label=label,
155
+ condition=condition, code=code, result_kind=result_kind, timeout=timeout,
156
+ judge=judge, criteria=criteria, step=step, threshold_value=threshold, jev=jev,
157
+ instructions=instructions, true=true, false=false, level=level, file=file, complete=False,
158
+ )
159
+ if file and body.get("eval_key") is not None and body["eval_key"] != key:
160
+ spec.fail("The spec eval_key must match the deploy argument")
161
+ body["eval_key"] = key
162
+ if version is not None:
163
+ body["eval_version"] = version
164
+ if dry_run_on:
165
+ spec.dry_run_body(body, dry_run_on)
166
+ backfill_body = None
167
+ if backfill:
168
+ if not re.fullmatch(r"[1-9][0-9]*d", backfill) or int(backfill[:-1]) > 90:
169
+ spec.fail("--backfill must be a duration in days from 1d to 90d")
170
+ now = datetime.now(timezone.utc)
171
+ backfill_body = {
172
+ "from": dates._iso(now - timedelta(days=int(backfill[:-1]))),
173
+ "to": dates._iso(now),
174
+ "eval_key": key,
175
+ "dry_run": False,
176
+ }
177
+ if spec.source_kind(body) != "code":
178
+ output.warn("Backfilling a judge/Jev evaluator spends the org's model budget.")
179
+ cctx = require_auth(ctx.obj)
180
+ existing = [r for r in definitions(cctx) if r.get("eval_key") == key]
181
+ if "eval_version" not in body:
182
+ versions = [str(r.get("eval_version", "")) for r in existing]
183
+ if any(not v.isdigit() for v in versions):
184
+ spec.fail("Non-numeric versions exist; specify a new --version")
185
+ body["eval_version"] = str(max([int(v) for v in versions] or [0]) + 1)
186
+ if any(str(r.get("eval_version")) == body["eval_version"] for r in existing):
187
+ spec.fail(f"Version {key}@{body['eval_version']} already exists; deploy needs a NEW version")
188
+ body.setdefault("display_name", existing[-1].get("display_name", key) if existing else key)
189
+ if existing and not file:
190
+ # A new version keeps the previous version's scope and labels unless a flag
191
+ # changes them; silently dropping the condition would widen the evaluator to
192
+ # every session. A --file spec states every field itself.
193
+ latest = max(existing, key=lambda r: str(r.get("created_at") or ""))
194
+ if condition is None:
195
+ body["condition_source"] = latest.get("condition_source")
196
+ if not label:
197
+ body["labels"] = list(latest.get("labels") or [])
198
+ spec.validate(body)
199
+ created = _create(ctx.obj, cctx, body, dry_run_on, force=force)
200
+ disabled_ids = []
201
+ try:
202
+ for old in existing:
203
+ if old.get("enabled", True):
204
+ api.cloud_request(cctx, "PATCH", f"{PATH}/{old['id']}", body={"enabled": False})
205
+ disabled_ids.append(old["id"])
206
+ if backfill_body:
207
+ api.cloud_request(cctx, "POST", "/api/evaluations/backfill", body=backfill_body)
208
+ except Exception as exc:
209
+ output.warn(f"Published {key}@{body['eval_version']} (id {created['id']}); disabled {len(disabled_ids)} predecessors. Follow-up failed; inspect evaluators list --all before retrying.")
210
+ raise exc
211
+ _write.record_action("evaluator_deployed", resource="evaluator", success=True)
212
+ if ctx.obj.json:
213
+ output.emit_json({"created": created, "disabled": disabled_ids})
214
+ else:
215
+ cloud_output.deployed({**body, **created}, [r for r in existing if r["id"] in disabled_ids])
216
+
217
+
218
+ def draft(
219
+ ctx: typer.Context,
220
+ intent: str = typer.Argument(...),
221
+ kind: str = typer.Option("code", "--kind"),
222
+ out: str = typer.Option("evaluator.json", "--out"),
223
+ ) -> None:
224
+ """AI-compose an API spec file. Session only; review it before deployment."""
225
+ _cloud.session_only(ctx.obj, "evaluators draft")
226
+ if kind not in ("code", "judge", "jev"):
227
+ spec.fail("--kind must be code, judge or jev")
228
+ data = api.cloud_request(require_auth(ctx.obj), "POST", "/api/agent/compose-eval",
229
+ body={"intent": f"Author a {kind} evaluator. {intent}", "resultKind": "score"})
230
+ if not isinstance(data, dict):
231
+ spec.fail("Cloud draft was not an evaluator JSON spec")
232
+ body = spec.from_draft(data, kind)
233
+ _cloud.write_file(out, json.dumps(body, indent=2, ensure_ascii=False) + "\n")
234
+ _cloud.emit(ctx.obj, {"out": out, "spec": body}, "Evaluator draft")
235
+
236
+
237
+ def dry_run(
238
+ ctx: typer.Context,
239
+ code: Optional[str] = typer.Option(None, "--code"),
240
+ result_kind: Optional[str] = typer.Option(None, "--result-kind"),
241
+ timeout: Optional[float] = typer.Option(None, "--timeout"),
242
+ condition: Optional[str] = typer.Option(None, "--condition"),
243
+ judge: bool = typer.Option(False, "--judge"),
244
+ criteria: Optional[str] = typer.Option(None, "--criteria"),
245
+ step: Optional[List[str]] = typer.Option(None, "--step"),
246
+ threshold: Optional[float] = typer.Option(None, "--threshold"),
247
+ jev: Optional[str] = typer.Option(None, "--jev"),
248
+ instructions: Optional[str] = typer.Option(None, "--instructions"),
249
+ true: Optional[str] = typer.Option(None, "--true"),
250
+ false: Optional[str] = typer.Option(None, "--false"),
251
+ level: Optional[List[str]] = typer.Option(None, "--level"),
252
+ file: Optional[str] = typer.Option(None, "--file"),
253
+ session: Optional[List[str]] = typer.Option(None, "--session"),
254
+ ) -> None:
255
+ """Compile only, or run on ≤10 sessions. Cloud refuses judge dry-runs.
256
+
257
+ Jev dry-runs execute model calls and spend budget, including before deployment.
258
+ """
259
+ body = spec.build(
260
+ code=code, result_kind=result_kind, timeout=timeout, condition=condition,
261
+ judge=judge, criteria=criteria, step=step, threshold_value=threshold, jev=jev,
262
+ instructions=instructions, true=true, false=false, level=level, file=file, complete=False,
263
+ )
264
+ body = spec.dry_run_body(body, session)
265
+ if spec.source_kind(body) == "jev":
266
+ output.warn("Jev dry-runs spend model budget.")
267
+ result = api.cloud_request(require_auth(ctx.obj), "POST", f"{PATH}/dry-run", body=body)
268
+ _cloud.emit(ctx.obj, result, "Evaluator dry-run")
269
+ if dry_run_failed(result):
270
+ # Preserve the full dry-run result in --json, but do not signal success.
271
+ raise typer.Exit(code=1)
272
+
273
+
274
+ def dry_run_failed(result):
275
+ return isinstance(result, dict) and (
276
+ result.get("ok") is False
277
+ or any(r.get("ok") is False for r in result.get("runs", []) if isinstance(r, dict))
278
+ or result.get("summary", {}).get("failed", 0) > 0
279
+ )
280
+
281
+
282
+ def check_dry_run(result):
283
+ if dry_run_failed(result):
284
+ failures = [r for r in result.get("runs", []) if r.get("ok") is False]
285
+ detail = "; ".join(str(r.get("error_message") or r.get("error_kind") or "run failed") for r in failures)
286
+ raise ApiError(f"Dry-run failed; evaluator was not created. {detail}".strip())
287
+
288
+
289
+ def toggle(ctx, handle, enabled):
290
+ cctx = require_auth(ctx.obj)
291
+ row = select(definitions(cctx), handle)
292
+ _write.confirm(ctx.obj, f"{'Enable' if enabled else 'Disable'} {handle}", assume_yes=False)
293
+ result = api.cloud_request(cctx, "PATCH", f"{PATH}/{row['id']}", body={"enabled": enabled})
294
+ _write.record_action("evaluator_toggled", resource="evaluator", success=True)
295
+ if ctx.obj.json:
296
+ output.emit_json(result or {**row, "enabled": enabled})
297
+ else:
298
+ cloud_output.evaluator({**row, **(result or {}), "enabled": enabled})
299
+
300
+
301
+ def enable(ctx: typer.Context, handle: str = typer.Argument(...)) -> None:
302
+ """Enable KEY[@VERSION]."""
303
+ toggle(ctx, handle, True)
304
+
305
+
306
+ def disable(ctx: typer.Context, handle: str = typer.Argument(...)) -> None:
307
+ """Disable KEY[@VERSION]."""
308
+ toggle(ctx, handle, False)
309
+
310
+
311
+ def label_def(ctx: typer.Context, handle: str = typer.Argument(...),
312
+ add: Optional[List[str]] = typer.Option(None, "--add"),
313
+ remove: Optional[List[str]] = typer.Option(None, "--remove")) -> None:
314
+ """Add/remove labels on KEY[@VERSION]."""
315
+ if not add and not remove:
316
+ spec.fail("Pass --add or --remove")
317
+ if set(add or []) & set(remove or []):
318
+ spec.fail("A label cannot be both added and removed")
319
+ cctx = require_auth(ctx.obj)
320
+ row = select(definitions(cctx), handle)
321
+ labels = list(dict.fromkeys([v for v in row.get("labels", []) if v not in (remove or [])] + (add or [])))
322
+ spec.validate_labels(labels)
323
+ _write.confirm(ctx.obj, f"Label {handle}", assume_yes=False)
324
+ data = api.cloud_request(cctx, "PATCH", f"{PATH}/{row['id']}", body={"labels": labels})
325
+ if ctx.obj.json:
326
+ output.emit_json(data or {**row, "labels": labels})
327
+ else:
328
+ cloud_output.evaluator({**row, **(data or {}), "labels": labels}, "Evaluator labels")
329
+
330
+
331
+ def instances(ctx: typer.Context) -> None:
332
+ """List managed evaluator instances."""
333
+ _cloud.emit(ctx.obj, api.cloud_request(require_auth(ctx.obj), "GET", "/api/evaluator-instances"), "Instances")
334
+
335
+
336
+ def schema(ctx: typer.Context) -> None:
337
+ """Schema profile. Needs evaluations:trigger and events:read."""
338
+ _cloud.emit(ctx.obj, api.cloud_request(require_auth(ctx.obj), "GET", "/api/evaluator-schema-profile"), "Schema profile")
339
+
340
+
341
+ def jobs(ctx: typer.Context) -> None:
342
+ """List evaluation jobs."""
343
+ _cloud.emit(ctx.obj, api.cloud_request(require_auth(ctx.obj), "GET", "/api/evaluation-jobs"), "Evaluation jobs")
344
+
345
+
346
+ def backfill(ctx: typer.Context, frm: str = typer.Option(..., "--from"),
347
+ to: str = typer.Option(..., "--to"), key: Optional[str] = typer.Option(None, "--key"),
348
+ dry_run: bool = typer.Option(False, "--dry-run")) -> None:
349
+ """Score stored sessions an evaluator has not scored yet (at most 90 days).
350
+
351
+ Covers code, LLM judge and Jev evaluators (every enabled one, or only --key).
352
+ Only missing (session, evaluator) pairs are queued; --dry-run counts them.
353
+ Needs evaluations:trigger.
354
+ """
355
+ try:
356
+ frm = dates._validate_iso(frm, "--from")
357
+ to = dates._validate_iso(to, "--to")
358
+ start = datetime.fromisoformat(frm.replace("Z", "+00:00"))
359
+ end = datetime.fromisoformat(to.replace("Z", "+00:00"))
360
+ except ValueError as exc:
361
+ spec.fail(str(exc))
362
+ if end <= start or end - start > timedelta(days=90):
363
+ spec.fail("Backfill needs an increasing range of at most 90 days")
364
+ if not dry_run:
365
+ output.warn("Judge and Jev evaluators in the backfill spend the org's model budget; --dry-run counts the work first.")
366
+ _write.confirm(ctx.obj, "Backfill evaluations", assume_yes=dry_run)
367
+ _cloud.emit(ctx.obj, api.cloud_request(require_auth(ctx.obj), "POST", "/api/evaluations/backfill",
368
+ body={"from": dates._iso(start), "to": dates._iso(end), "eval_key": key, "dry_run": dry_run}), "Evaluation backfill")
369
+
370
+
371
+ def register(app: typer.Typer) -> None:
372
+ group = typer.Typer(no_args_is_help=True, rich_markup_mode="markdown", help="Author, test and deploy code, LLM-judge and Jev evaluators.", context_settings={"help_option_names": ["-h", "--help"]})
373
+ for name, fn in (
374
+ ("list", list_defs), ("show", show), ("create", create), ("deploy", deploy),
375
+ ("draft", draft), ("dry-run", dry_run), ("enable", enable), ("disable", disable),
376
+ ("label", label_def), ("instances", instances), ("schema", schema), ("jobs", jobs),
377
+ ("backfill", backfill),
378
+ ):
379
+ group.command(name, epilog=GLOBALS_EPILOG)(fn)
380
+ app.add_typer(group, name="evaluators")
@@ -70,6 +70,8 @@ def events(
70
70
  * `fp --json events --session-id run-001 --all` — a run's full timeline: event types + summaries (light, fast)
71
71
  * `fp --json events --full --session-id run-001 --all | jq '.events[].payload'` — that run's raw payloads (full feed, bounded to one run)
72
72
  """
73
+ if ctx.invoked_subcommand:
74
+ return
73
75
  state: AppState = ctx.obj
74
76
  cctx = require_auth(state)
75
77
  frm, to = resolve_dates(since, ts_from, ts_to)
@@ -155,5 +157,16 @@ def events(
155
157
  output.recheck_filters_hint(active_filters)
156
158
 
157
159
 
160
+ def latency(ctx: typer.Context, since: Optional[str] = typer.Option(None, "--since")) -> None:
161
+ """Aggregate event latency."""
162
+ from . import _cloud
163
+ frm, to = resolve_dates(since, None, None)
164
+ _cloud.emit(ctx.obj, api.cloud_request(require_auth(ctx.obj), "GET", "/api/events/latency_aggregate",
165
+ params={"ts_from": frm, "ts_to": to}), "Event latency")
166
+
167
+
158
168
  def register(app: typer.Typer) -> None:
159
- app.command("events", epilog=GLOBALS_EPILOG)(events)
169
+ group = typer.Typer(invoke_without_command=True, rich_markup_mode="markdown", context_settings={"help_option_names": ["-h", "--help"]})
170
+ group.callback(epilog=GLOBALS_EPILOG)(events)
171
+ group.command("latency", epilog=GLOBALS_EPILOG)(latency)
172
+ app.add_typer(group, name="events")
@@ -14,7 +14,8 @@ import typer
14
14
 
15
15
  from .. import client as api
16
16
  from .. import output
17
- from .._context import GLOBALS_EPILOG, AppState, require_auth, validate_limit
17
+ from .. import _click_compat as click
18
+ from .._context import GLOBALS_EPILOG, AppState, require_auth, resolve_dates, validate_limit
18
19
  from ..errors import ApiError, ForbiddenError, NotFoundError
19
20
  from . import _write
20
21
 
@@ -68,12 +69,40 @@ def _fail(state: AppState, exc: Exception, *, incident_id: str = "") -> None:
68
69
  raise exc
69
70
 
70
71
 
72
+ def _issue_filters(
73
+ agent_id, env, machine, source, audit_id, assignee, archived, search, since,
74
+ ) -> dict:
75
+ if archived is not None and archived not in ("false", "true", "only", "all"):
76
+ raise click.UsageError("--archived must be false, only or all")
77
+ if source is not None:
78
+ bad = [s for s in source.split(",") if s.strip() and s.strip() not in ("alert", "audit", "manual")]
79
+ if bad:
80
+ raise click.UsageError("--source must be alert, audit or manual (CSV)")
81
+ opened_from = None
82
+ if since is not None:
83
+ opened_from, _ = resolve_dates(since, None, None)
84
+ return {
85
+ "agent": agent_id, "environment": env, "machine": machine, "source": source,
86
+ "audit_id": audit_id, "assignee": assignee, "archived": archived, "q": search,
87
+ "opened_from": opened_from,
88
+ }
89
+
90
+
71
91
  def incidents_list(
72
92
  ctx: typer.Context,
73
93
  state_filter: Optional[str] = typer.Option(None, "--state", help="Filter by state(s): firing, acknowledged, resolved, closed (CSV)."),
74
94
  alert_id: Optional[str] = typer.Option(None, "--alert-id", help="Only incidents for this alert."),
75
95
  limit: int = typer.Option(50, "--limit", "-n", help="Max incidents to return."),
76
96
  show_id: bool = typer.Option(False, "--show-id", help="Show the full incident id instead of the short form (always full in --json)."),
97
+ agent_id: Optional[str] = typer.Option(None, "--agent-id", help="Only issues touching these agent ids (CSV)."),
98
+ env: Optional[str] = typer.Option(None, "--env", help="Only these environments (CSV)."),
99
+ machine: Optional[str] = typer.Option(None, "--machine", help="Only these machine ids (CSV)."),
100
+ source: Optional[str] = typer.Option(None, "--source", help="alert, audit and/or manual (CSV)."),
101
+ audit_id: Optional[str] = typer.Option(None, "--audit-id", help="Only issues from these audits (CSV of ids)."),
102
+ assignee: Optional[str] = typer.Option(None, "--assignee", help="Emails, `me` or `unassigned` (CSV)."),
103
+ archived: Optional[str] = typer.Option(None, "--archived", help="false (default), only or all."),
104
+ search: Optional[str] = typer.Option(None, "--search", help="Case-insensitive text in the title or summary."),
105
+ since: Optional[str] = typer.Option(None, "--since", help="Opened within: 15m 1h 6h 24h 7d."),
77
106
  ) -> None:
78
107
  """List incidents in a boxed table (newest-opened first).
79
108
 
@@ -94,7 +123,8 @@ def incidents_list(
94
123
  _validate_states(state_filter)
95
124
  validate_limit(limit)
96
125
  cctx = require_auth(state)
97
- incidents = api.list_incidents(cctx, state=state_filter, alert_id=alert_id, limit=limit)
126
+ extra = _issue_filters(agent_id, env, machine, source, audit_id, assignee, archived, search, since)
127
+ incidents = api.list_incidents(cctx, state=state_filter, alert_id=alert_id, limit=limit, filters=extra)
98
128
  if state.json:
99
129
  output.emit_json({"issues": incidents})
100
130
  return
@@ -105,6 +135,16 @@ def incidents_list(
105
135
  def incidents_count(
106
136
  ctx: typer.Context,
107
137
  state_filter: Optional[str] = typer.Option(None, "--state", help="Filter by state(s) (CSV)."),
138
+ alert_id: Optional[str] = typer.Option(None, "--alert-id", help="Only issues from these alerts (CSV of ids)."),
139
+ agent_id: Optional[str] = typer.Option(None, "--agent-id", help="Only issues touching these agent ids (CSV)."),
140
+ env: Optional[str] = typer.Option(None, "--env", help="Only these environments (CSV)."),
141
+ machine: Optional[str] = typer.Option(None, "--machine", help="Only these machine ids (CSV)."),
142
+ source: Optional[str] = typer.Option(None, "--source", help="alert, audit and/or manual (CSV)."),
143
+ audit_id: Optional[str] = typer.Option(None, "--audit-id", help="Only issues from these audits (CSV of ids)."),
144
+ assignee: Optional[str] = typer.Option(None, "--assignee", help="Emails, `me` or `unassigned` (CSV)."),
145
+ archived: Optional[str] = typer.Option(None, "--archived", help="false (default), only or all."),
146
+ search: Optional[str] = typer.Option(None, "--search", help="Case-insensitive text in the title or summary."),
147
+ since: Optional[str] = typer.Option(None, "--since", help="Opened within: 15m 1h 6h 24h 7d."),
108
148
  ) -> None:
109
149
  """Count incidents (optionally by state) as a compact stat card.
110
150
 
@@ -118,7 +158,10 @@ def incidents_count(
118
158
  state: AppState = ctx.obj
119
159
  _validate_states(state_filter)
120
160
  cctx = require_auth(state)
121
- count = api.count_incidents(cctx, state=state_filter)
161
+ extra = _issue_filters(agent_id, env, machine, source, audit_id, assignee, archived, search, since)
162
+ if alert_id:
163
+ extra["alert_id"] = alert_id
164
+ count = api.count_incidents(cctx, state=state_filter, filters=extra)
122
165
  if state.json:
123
166
  output.emit_json({"count": count})
124
167
  else:
@@ -261,10 +304,11 @@ def incidents_close(
261
304
 
262
305
  For an issue that came from an audit, this also marks the finding `dismissed`. It does
263
306
  NOT write the finding's org-wide suppression: closing one issue never hides that pattern
264
- in your other audits. Use `fp audits finding-status <id> --action dismiss` for that.
307
+ in your other audits. Use `fp audits dismiss <finding-id>` for that.
265
308
 
266
- Needs `issues:close`. Exits 9 (conflict) if the issue already ended — an issue ends once,
267
- and closing must not overwrite the record that someone believed they had fixed it.
309
+ Needs `issues:close`. Exits 1 (the server answers 409 conflict) if the issue already
310
+ ended — an issue ends once, and closing must not overwrite the record that someone
311
+ believed they had fixed it.
268
312
 
269
313
  With `--json`: `{"closed": true, "id": "<id>"}` (or `{cancelled: true}` on a declined
270
314
  prompt).
@@ -666,6 +710,37 @@ def incidents_open(
666
710
  output.render_incident_opened(summary=summary, severity=sev, state=st, title=hero)
667
711
 
668
712
 
713
+ def incidents_facets(
714
+ ctx: typer.Context,
715
+ state_filter: Optional[str] = typer.Option(None, "--state", help="Filter by state(s) (CSV)."),
716
+ alert_id: Optional[str] = typer.Option(None, "--alert-id", help="Only issues from these alerts (CSV of ids)."),
717
+ agent_id: Optional[str] = typer.Option(None, "--agent-id", help="Only issues touching these agent ids (CSV)."),
718
+ env: Optional[str] = typer.Option(None, "--env", help="Only these environments (CSV)."),
719
+ machine: Optional[str] = typer.Option(None, "--machine", help="Only these machine ids (CSV)."),
720
+ source: Optional[str] = typer.Option(None, "--source", help="alert, audit and/or manual (CSV)."),
721
+ audit_id: Optional[str] = typer.Option(None, "--audit-id", help="Only issues from these audits (CSV of ids)."),
722
+ assignee: Optional[str] = typer.Option(None, "--assignee", help="Emails, `me` or `unassigned` (CSV)."),
723
+ archived: Optional[str] = typer.Option(None, "--archived", help="false (default), only or all."),
724
+ search: Optional[str] = typer.Option(None, "--search", help="Case-insensitive text in the title or summary."),
725
+ since: Optional[str] = typer.Option(None, "--since", help="Opened within: 15m 1h 6h 24h 7d."),
726
+ ) -> None:
727
+ """The values the issue filters can take right now (agents, environments, machines,
728
+ sources, assignees), with counts — the dashboard inbox's filter bar. Needs `issues:read`.
729
+
730
+ Example:
731
+
732
+ * `fp issues facets --state firing,acknowledged`
733
+ """
734
+ from . import _cloud
735
+ state: AppState = ctx.obj
736
+ _validate_states(state_filter)
737
+ extra = _issue_filters(agent_id, env, machine, source, audit_id, assignee, archived, search, since)
738
+ params = {"state": state_filter, "alert_id": alert_id, **extra}
739
+ data = api.cloud_request(require_auth(state), "GET", "/api/issues/facets",
740
+ params={k: v for k, v in params.items() if v is not None})
741
+ _cloud.emit(state, data, "Issue facets")
742
+
743
+
669
744
  def register(app: typer.Typer) -> None:
670
745
  inc = typer.Typer(
671
746
  no_args_is_help=True,
@@ -675,6 +750,7 @@ def register(app: typer.Typer) -> None:
675
750
  )
676
751
  inc.command("list", epilog=GLOBALS_EPILOG)(incidents_list)
677
752
  inc.command("count", epilog=GLOBALS_EPILOG)(incidents_count)
753
+ inc.command("facets", epilog=GLOBALS_EPILOG)(incidents_facets)
678
754
  inc.command("show", epilog=GLOBALS_EPILOG)(incidents_show)
679
755
  inc.command("ack", epilog=GLOBALS_EPILOG)(incidents_ack)
680
756
  inc.command("assign", epilog=GLOBALS_EPILOG)(incidents_assign)
@@ -24,6 +24,9 @@ _LIST_KINDS = {
24
24
  "hooks": ("hook_names", "events:read", "Hook names seen across events.", "seen across events"),
25
25
  "tools": ("tool_names", "events:read", "Tool names seen across events.", "seen across events"),
26
26
  "error_types": ("error_types", "events:read", "Error types seen across events.", "seen across events"),
27
+ "trigger_events": ("trigger_events", "events:read", "Hook trigger events seen across events.", "seen across events"),
28
+ "eval_envs": ("eval_environments", "evaluations:read", "Environments that have evaluation results.", "with evaluation results"),
29
+ "metric_keys": ("metric_keys", "evaluations:read", "Evaluation metric keys (metrics, not scores).", "evaluation metric keys"),
27
30
  }
28
31
 
29
32
  def _make_list_cmd(name: str, facet_key: str, perm: str, summary: str, description: str):
@@ -56,7 +59,7 @@ def register(app: typer.Typer) -> None:
56
59
  context_settings={"help_option_names": ["-h", "--help"]},
57
60
  help="List the distinct values behind the dashboard's filter dropdowns. Subcommands: "
58
61
  "**envs**, **agents**, **event_types**, **score_filters**, **models**, **hooks**, "
59
- "**tools**, **error_types**.",
62
+ "**tools**, **error_types**, **trigger_events**, **eval_envs**, **metric_keys**.",
60
63
  )
61
64
  for name, (facet_key, perm, summary, description) in _LIST_KINDS.items():
62
65
  list_app.command(name, epilog=GLOBALS_EPILOG)(_make_list_cmd(name, facet_key, perm, summary, description))
@@ -316,4 +316,21 @@ def register(app: typer.Typer) -> None:
316
316
  orgs_app.command("switch", epilog=GLOBALS_EPILOG)(org_switch)
317
317
  orgs_app.command("current", epilog=GLOBALS_EPILOG)(org_current)
318
318
  orgs_app.command("perms", epilog=GLOBALS_EPILOG)(org_perms)
319
+ orgs_app.command("create", epilog=GLOBALS_EPILOG)(org_create)
319
320
  app.add_typer(orgs_app, name="orgs")
321
+
322
+
323
+ def org_create(ctx: typer.Context, name: str = typer.Option(..., "--name"),
324
+ slug: Optional[str] = typer.Option(None, "--slug")) -> None:
325
+ """Create an organization (session only; does not switch the saved org)."""
326
+ from . import _cloud
327
+ from .account_cmds import request
328
+ from .. import _click_compat as click
329
+ _cloud.session_only(ctx.obj, "orgs create")
330
+ if not name.strip():
331
+ raise click.UsageError("--name cannot be empty")
332
+ import re
333
+ slug = slug or re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
334
+ if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", slug):
335
+ raise click.UsageError("--slug must be lowercase letters/digits separated by hyphens")
336
+ request(ctx, "POST", "/api/orgs", {"name": name, "slug": slug})