fruxon 0.13.0__tar.gz → 0.13.2__tar.gz

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 (147) hide show
  1. {fruxon-0.13.0 → fruxon-0.13.2}/HISTORY.md +188 -0
  2. {fruxon-0.13.0 → fruxon-0.13.2}/PKG-INFO +9 -7
  3. {fruxon-0.13.0 → fruxon-0.13.2}/README.md +8 -6
  4. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/__init__.py +4 -0
  5. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/_version.py +2 -2
  6. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/__init__.py +61 -1
  7. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/_crash.py +37 -4
  8. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/_schema.py +5 -2
  9. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/agents_draft.py +28 -3
  10. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/agents_revisions.py +12 -1
  11. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/examples.py +48 -1
  12. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/guides.py +8 -1
  13. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/tools.py +121 -3
  14. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/triggers.py +117 -2
  15. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/triggers_ledger.py +604 -22
  16. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/fruxon.py +222 -15
  17. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/models.py +177 -5
  18. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/skills/fruxon-agent-mode/SKILL.md +1 -1
  19. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/skills/fruxon-build-agent/SKILL.md +2 -2
  20. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/skills/fruxon-debug-trace/SKILL.md +49 -9
  21. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/skills/fruxon-meet/SKILL.md +5 -2
  22. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/skills/fruxon-use-integrations/SKILL.md +22 -1
  23. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_cli.py +1 -1
  24. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_cli_crash.py +57 -0
  25. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_describe.py +14 -0
  26. fruxon-0.13.2/tests/test_kb_trial_cli.py +263 -0
  27. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_triggers_ledger.py +972 -5
  28. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_triggers_write.py +121 -2
  29. {fruxon-0.13.0 → fruxon-0.13.2}/.gitignore +0 -0
  30. {fruxon-0.13.0 → fruxon-0.13.2}/LICENSE +0 -0
  31. {fruxon-0.13.0 → fruxon-0.13.2}/pyproject.toml +0 -0
  32. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/__main__.py +0 -0
  33. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/_ssl.py +0 -0
  34. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/_shared.py +0 -0
  35. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/_stream.py +0 -0
  36. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/agents.py +0 -0
  37. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/agents_approvals.py +0 -0
  38. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/agents_budget.py +0 -0
  39. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/agents_channels.py +0 -0
  40. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/agents_executions.py +0 -0
  41. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/agents_lifecycle.py +0 -0
  42. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/agents_memory.py +0 -0
  43. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/agents_sandbox.py +0 -0
  44. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/agents_sandbox_test.py +0 -0
  45. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/agents_slots.py +0 -0
  46. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/agents_tests.py +0 -0
  47. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/agents_topics.py +0 -0
  48. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/applications.py +0 -0
  49. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/applications_entry_points.py +0 -0
  50. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/assets.py +0 -0
  51. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/auth.py +0 -0
  52. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/capabilities.py +0 -0
  53. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/completion.py +0 -0
  54. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/config.py +0 -0
  55. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/consult_pins.py +0 -0
  56. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/describe.py +0 -0
  57. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/doctor.py +0 -0
  58. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/environments.py +0 -0
  59. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/escalations.py +0 -0
  60. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/integrations.py +0 -0
  61. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/keys.py +0 -0
  62. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/llm_providers.py +0 -0
  63. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/messages.py +0 -0
  64. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/metrics.py +0 -0
  65. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/participants.py +0 -0
  66. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/pipelines.py +0 -0
  67. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/secrets.py +0 -0
  68. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/skills.py +0 -0
  69. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/storage.py +0 -0
  70. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/triggers_questions.py +0 -0
  71. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/triggers_shape.py +0 -0
  72. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/cli/workspaces.py +0 -0
  73. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/credentials.py +0 -0
  74. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/device_auth.py +0 -0
  75. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/doctor.py +0 -0
  76. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/exceptions.py +0 -0
  77. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/flow_validation.py +0 -0
  78. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/output.py +0 -0
  79. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/params.py +0 -0
  80. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/sandbox_scenario.py +0 -0
  81. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/skills/__init__.py +0 -0
  82. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/skills/fruxon-create-integration/SKILL.md +0 -0
  83. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/telemetry.py +0 -0
  84. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/ui.py +0 -0
  85. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/update_check.py +0 -0
  86. {fruxon-0.13.0 → fruxon-0.13.2}/src/fruxon/validation.py +0 -0
  87. {fruxon-0.13.0 → fruxon-0.13.2}/tests/__init__.py +0 -0
  88. {fruxon-0.13.0 → fruxon-0.13.2}/tests/conftest.py +0 -0
  89. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_actor.py +0 -0
  90. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_agent_lifecycle.py +0 -0
  91. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_agent_slots.py +0 -0
  92. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_agents_check_cli.py +0 -0
  93. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_agents_revisions_cli.py +0 -0
  94. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_agents_sandbox.py +0 -0
  95. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_applications.py +0 -0
  96. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_approvals.py +0 -0
  97. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_assets.py +0 -0
  98. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_budgets.py +0 -0
  99. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_capabilities.py +0 -0
  100. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_channels.py +0 -0
  101. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_cli_base_url_routing.py +0 -0
  102. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_client.py +0 -0
  103. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_connect_nudge.py +0 -0
  104. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_consult.py +0 -0
  105. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_credentials.py +0 -0
  106. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_device_auth.py +0 -0
  107. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_doctor.py +0 -0
  108. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_draft_bootstrap.py +0 -0
  109. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_draft_evaluate_cli.py +0 -0
  110. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_draft_pull_shape.py +0 -0
  111. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_draft_validate_cli.py +0 -0
  112. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_drafts.py +0 -0
  113. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_entry_points.py +0 -0
  114. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_environments.py +0 -0
  115. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_escalations.py +0 -0
  116. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_execution_records.py +0 -0
  117. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_flow_validation.py +0 -0
  118. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_fruxon.py +0 -0
  119. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_guides.py +0 -0
  120. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_integration_triggers_cli.py +0 -0
  121. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_integrations_authorize_cli.py +0 -0
  122. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_llm_provider_configs_cli.py +0 -0
  123. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_memory.py +0 -0
  124. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_messages.py +0 -0
  125. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_metrics.py +0 -0
  126. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_online_secret_refs.py +0 -0
  127. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_output.py +0 -0
  128. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_params.py +0 -0
  129. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_participants.py +0 -0
  130. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_participants_write.py +0 -0
  131. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_pipelines.py +0 -0
  132. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_sandbox_scenario.py +0 -0
  133. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_schema.py +0 -0
  134. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_secrets.py +0 -0
  135. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_skills.py +0 -0
  136. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_ssl.py +0 -0
  137. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_storage.py +0 -0
  138. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_telemetry.py +0 -0
  139. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_test_chats.py +0 -0
  140. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_topics.py +0 -0
  141. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_triggers.py +0 -0
  142. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_triggers_questions.py +0 -0
  143. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_triggers_shape.py +0 -0
  144. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_ui.py +0 -0
  145. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_update_check.py +0 -0
  146. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_validation.py +0 -0
  147. {fruxon-0.13.0 → fruxon-0.13.2}/tests/test_workspaces_cli.py +0 -0
@@ -1,5 +1,193 @@
1
1
  # History
2
2
 
3
+ ## Unreleased — What a listing leaves out, and what a typo returns
4
+
5
+ A patch release from a CLI trial run against a live workspace. Four defects,
6
+ all of them the same shape: the CLI gave a confident answer that was missing
7
+ something the server had already sent, or that no driver could parse.
8
+
9
+ - **`triggers ledger items` carries each item's `label`.** The server sends
10
+ it, the Work grid shows it, and the CLI dropped it — so a listing named every
11
+ row by the provider's opaque key and nothing else. In `-o text` it sits next
12
+ to the key, and only when the door supplies one.
13
+ - **`triggers doors` carries `labelPaths` and `labelFieldName`.** Where a door
14
+ reads an item's title from, printed as `labels work from`. These are the
15
+ paths a `door.projection` is forbidden to drop, so an author who could not
16
+ see them was authoring blind against a save-time refusal.
17
+ - **A ledger listing is one page of 20, not the whole backlog.** The healthy
18
+ state of a busy ledger is thousands of `READY` rows served ten per request,
19
+ and walking them all was hundreds of round trips that the server rate-limits
20
+ long before the end — so an unqualified `triggers ledger items` returned an
21
+ error or nothing at all against a real automation. A full page now reports
22
+ what it is a page of; `-n` sizes it and `--all` still walks the lot. The SDK's
23
+ `list_ledger_items()` is unchanged: a library caller asking for items still
24
+ gets them all.
25
+ - **`tools list` recovers the internal platform tools.** `knowledge_base` and
26
+ `assets` answered `[]` about tools an agent can attach and run, because the
27
+ tenant list endpoint filters them and takes no parameter that says otherwise.
28
+ The single-tool endpoint has no such filter, so the listing fetches them by
29
+ ID — each one confirmed against the server, so a stale ID disappears instead
30
+ of sending you to a `tools get` that 404s. `--no-include-internal` returns the
31
+ raw server answer.
32
+ - **Every parse error is the same envelope, at every depth.** A bad flag on the
33
+ root command (`fruxon --bogus`) was the one case that still printed Click's
34
+ ASCII box to a driver that had been promised `{"error": {"code": "usage"}}`
35
+ and exit 2 for every other one. A person at the terminal still gets Click's
36
+ `Usage:` block.
37
+ - **SDK:** `LedgerItem` gains `label`; `WorkShapeDoor` gains `label_paths` and
38
+ `label_field_name`.
39
+
40
+ ## Unreleased — A ledger automation can say what it cost
41
+
42
+ The ledger could account for *where* work sat but not for what it came to, so
43
+ a schedule's spend was only reachable one execution record at a time. The
44
+ backend now carries cost on the accounting reads and adds a per-fire view
45
+ (fruxon-backend#741).
46
+
47
+ - **`triggers ledger funnel` and `triggers ledger items` carry `cost` and
48
+ `runs`.** The funnel therefore answers which *stage* the money goes to, not
49
+ only how much the ledger has spent; its trailing line totals both and shows
50
+ cost per item over the items that have actually run. An item's `runs` counts
51
+ every stage it passed through and so is deliberately not `stageAttempts`,
52
+ which resets per stage. `cost` is not sortable — the endpoint's `--order-by`
53
+ allow-list does not carry it, and an unlisted field is a 400.
54
+ - **A cost column distinguishes "never ran" from "ran for free".** `runs` of 0
55
+ prints as `—`, not `$0.0000`: the second is a real and different state (an
56
+ exclusion that fired before any model call), and carrying `runs` alongside
57
+ `cost` is what keeps the two apart.
58
+ - **New `fruxon triggers ledger passes <trigger>`** — one row per fire, newest
59
+ first, with `admitted` / `dispatched` / `completed` / `runs` / `cost` and the
60
+ fire log's own detail. `runs` sits below `dispatched` while runs are in
61
+ flight or when a dispatch was parked on a human, and a pass's `cost` rises
62
+ after `firedAt` as those runs finish, so an early read is a floor. `-n` /
63
+ `--limit` bounds the walk.
64
+ - **SDK:** `FruxonClient.list_ledger_passes()` and the `LedgerPass` model;
65
+ `LedgerItem` and `LedgerFunnelCell` gain defaulted `cost` / `runs`.
66
+
67
+ ## Unreleased — Keeping only the fields an item needs
68
+
69
+ - **`workShape.door.projection` (a list of dotted record paths)** names the
70
+ fields an admitted item keeps out of each source record. Without one an item
71
+ carries the record whole — stored per item, shown in the Work grid, and
72
+ handed to every stage on every pass. **Empty keeps everything**, which is
73
+ what every shape saved before this does, so no existing automation changes
74
+ shape. The item key, the door's label paths, the cursor and a write-back
75
+ target are kept whatever you name, and a projection that drops the id or a
76
+ label is refused at save time rather than quietly repaired.
77
+ - **`triggers test-source` prints the projected record.** Each `record` is what
78
+ an item would actually carry, and an `item keeps` line names the paths that
79
+ survived (`projection` in `-o json`). Without it a field the projection drops
80
+ and a field the source never sent are the same silence, and they call for
81
+ opposite fixes.
82
+ - **`triggers doors` carries `defaultProjection`.** The door's own suggestion
83
+ for what to keep, printed as `suggests keeping` — what to seed a new shape's
84
+ `door.projection` with. The sheet and table doors suggest nothing, because a
85
+ row's columns are yours and the row is the work.
86
+
87
+ ## Unreleased — Counting an exclusion before you undo one
88
+
89
+ `redrive-many --ignore-reason` could reconsider what one revised instruction set
90
+ aside, but the count it confirms had to be assembled here: the CLI listed the
91
+ ledger and filtered it in memory, which is 40 round trips and 4,000 items on a
92
+ real automation, and a second opinion about a set that moves.
93
+
94
+ - **`triggers ledger items --ignore-reason "…"`** narrows to the items one
95
+ exclusion reason set aside, server-side — the same filter `redrive-many`
96
+ selects by, matched whole and case-sensitively, and only ever against
97
+ `IGNORED` items. It is how you read the batch a revised instruction is about
98
+ before putting it back.
99
+ - **The selection verbs confirm the server's count** whenever the endpoint's
100
+ filters express the selection exactly: a `--status` or an `--ignore-reason`,
101
+ with no `--id` and no `--before`/`--after`. Everything else still resolves
102
+ here — an admission window has no query parameter, ids have to be intersected
103
+ and the invented ones named, and "DEAD or FAILED" is not a filter the endpoint
104
+ can be handed.
105
+ - An API that reports no total sends the CLI back to the listing rather than
106
+ confirming a zero, which is the one count a selection verb accepts while
107
+ moving nothing.
108
+
109
+ ## Unreleased — Seeing what a source filter drops before you save it
110
+
111
+ A work shape can now say which listed records it does not want:
112
+ `workShape.door.skipWhen` is a `{match, conditions[]}` filter in the same
113
+ vocabulary as a trigger's event filter, and a record it matches is never
114
+ admitted — no item, no stage, no model call, nothing in the IGNORED pile.
115
+ Before this, the only place such a rule could live was a first stage's
116
+ `ignoreReasons`, which spends a model call per record to decide what a dotted
117
+ path already knows.
118
+
119
+ It has to be on the `door` rather than on `source`: saving re-expands `source`
120
+ from the door, so a filter written onto `source` is discarded by the next save.
121
+
122
+ - **`triggers test-source` marks each dropped record `FILTERED`** and counts
123
+ them under the listing, which is where to see what a rule costs before it
124
+ goes onto a live automation. A filter that reached the server and matched
125
+ nothing says so, because otherwise it is indistinguishable from one that
126
+ never arrived.
127
+ - **`--count` reports `filtered` beside the total**, and `already held` is now
128
+ counted after the filter, so `total − filtered − already held` is the work
129
+ that would actually arrive. The CLI prints that subtraction rather than
130
+ leaving four numbers on one line for you to do it. On a source with no filter
131
+ every number is exactly what it was.
132
+ - Records already admitted stay admitted: the rule governs admission, never
133
+ retraction.
134
+
135
+ ## Unreleased — Trying an automation on five instead of three thousand
136
+
137
+ A work-shape automation had two settings: off, or draining everything. `triggers
138
+ fire` ran a whole pass — admission to exhaustion, every stage claiming its full
139
+ limit — so trying the Restigo TexterChat automation on a handful meant firing it
140
+ at 3,401 resolved chats, and draining a stuck merge queue by hand pulled twenty
141
+ more chats in behind the pile on every fire.
142
+
143
+ - **`triggers fire` takes three ceilings for that one pass.** `--max-admit N`
144
+ stops admission after that many genuinely-new items (`0` leaves the source
145
+ untouched); `--max-dispatch N` caps how many items the whole pass claims,
146
+ across stages rather than per stage; `--stage <id>`, repeatable, claims only at
147
+ the stages named. They ride the request: the schedule keeps running the
148
+ automation exactly as configured, and the next fire is unbounded again unless
149
+ it asks for bounds of its own. A stage the work shape does not declare is
150
+ refused by the backend with a message naming the stages it does have.
151
+ - **A bounded pass that stops mid-listing leaves the cursor where it was**, so
152
+ nothing is skipped — the next pass re-lists the same window and admission
153
+ de-duplicates it. That is also why the bound counts items admitted rather than
154
+ records listed: a second bounded fire reads past what it already knows and
155
+ takes the next N.
156
+ - **The fire result says the pass was bounded.** `fire_trigger(...)` returns the
157
+ bounds under `bounds`, and the text output prints `admitted N, dispatched N —
158
+ bounded pass (…)`. Without it a pass sized by hand and an automation with
159
+ nothing to do print the same two counts. `--max-admit -1` is refused locally
160
+ rather than spending a round trip, because `0` means something specific here.
161
+
162
+ ## Unreleased — Patch: KB-trial CLI crash fixes
163
+
164
+ The 2026-09-11 knowledge-base trial exposed a common failure underneath six
165
+ apparently unrelated commands: Typer 0.26 vendors Click, so its parse exceptions
166
+ are not subclasses of the top-level `click.ClickException` the new crash handler
167
+ excluded. Unknown options, missing arguments, and no-argument help were therefore
168
+ reported as Fruxon bugs.
169
+
170
+ - **Agent-mode usage failures are typed again.** `NoSuchOption`,
171
+ `MissingParameter`, and `NoArgsIsHelpError` from either external or
172
+ Typer-vendored Click now emit the standard `{"error": {"code": "usage",
173
+ "exit_code": 2, ...}}` envelope and never write a crash report.
174
+ - **The trial command forms now match the surrounding CLI.** `agents revisions
175
+ get` accepts `-o/--output json`; `tools get <integration> --tool/-t <id>`
176
+ matches `tools run --tool` (the old positional tool ID remains accepted for
177
+ compatibility); and `agents draft discard` now exposes `--yes/-y`, confirms
178
+ for humans, and requires the flag under an agent driver.
179
+ - **Schema read timeouts are network errors, not crashes.** The live OpenAPI
180
+ fetch used by `--schema` catches a socket `TimeoutError`, exits 15, and points
181
+ at `--base-url` and the expected Swagger endpoint.
182
+ - **Hidden internal tools are discoverable.** The backend's tenant list API
183
+ intentionally omits the internal `knowledge_base` and `assets` tools. An empty
184
+ `tools list` now explains that behavior, names the built-in IDs shipped with
185
+ this release, and shows how to inspect each tool's parameters with `tools get
186
+ ... --tool ... -o json`.
187
+ - **`fruxon guides` prints its local help directly**, including in agent mode;
188
+ `integrations configs list` without its integration argument is a typed usage
189
+ error naming `INTEGRATION`.
190
+
3
191
  ## Unreleased — A channel connected through an entry point is a way in
4
192
 
5
193
  `agents check` looked for an inbound path in two places — a bound trigger and a
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fruxon
3
- Version: 0.13.0
3
+ Version: 0.13.2
4
4
  Summary: The Fruxon SDK is a lightweight Python client for integrating with the Fruxon platform.
5
5
  Project-URL: bugs, https://github.com/fruxon-ai/fruxon-sdk/issues
6
6
  Project-URL: changelog, https://github.com/fruxon-ai/fruxon-sdk/blob/main/HISTORY.md
@@ -134,18 +134,20 @@ Integrations + tools:
134
134
  | `fruxon integrations authorize <integration>` | Mint an application-level OAuth authorization URL to **connect** an integration — auto-detects its OAuth2 method (`--auth-method` to pick, `--scope`/`--config-param` to tune). OAuth needs a browser consent step, so the CLI hands you the link; a human clicks it, and the connection is saved as a tenant config a slot can pin. |
135
135
  | `fruxon integrations triggers <integration>` | List the event types an integration can fire an agent on — each descriptor's `id` is the `eventType` a trigger listens for, plus the `payloadFields` (dotted paths) a binding's `parameterMappings` can read. Discovery for wiring an inbound-event trigger without guessing. |
136
136
  | `fruxon integrations mcp …` | Inspect and enable the MCP server for an integration. |
137
- | `fruxon tools list/get/create/update/delete/run` | Manage the tools inside an integration. Integration-scoped. `run` executes one tool outside any agent — real credentials, real result — via `--tool` plus `-p key=value`, with `-c` naming the config that supplies credentials. |
137
+ | `fruxon tools list/get/create/update/delete/run` | Manage the tools inside an integration. Integration-scoped. `get` and `run` select the tool with `--tool`; `run` executes it outside any agent — real credentials, real result — with `-p key=value`, and `-c` naming the config that supplies credentials. The tenant list API hides internal `knowledge_base` / `assets` tools; `tools list` recovers them by ID and lists them like any other (`--no-include-internal` for the raw server answer). |
138
138
  | `fruxon keys list/mint/revoke/delete/history/scopes` | Audit and revoke scoped tokens. Minting opens the dashboard so the secret never enters the CLI process. |
139
139
  | `fruxon llm-providers list/get/models` | Browse LLM providers and models supported at the tenant level. |
140
140
  | `fruxon assets create/list/get/wait/operations/delete` | Manage knowledge-base (RAG) assets a step can query — upload local files, wait for async ingestion, inspect operations, and use the ids for `assetConfig.assetIds`. `delete` confirms + refuses without `--yes` in agent mode. |
141
- | `fruxon assets search/documents/chunks` | Reproduce and inspect a retrieval. `search` runs the same query an agent's `search_assets` does (`--mode HYBRID/VECTOR/FULL_TEXT`, `--top-k`, fusion weights) — note `score` is on a different scale per mode, and in hybrid the top hit is always 1.0. `documents` lists what the index contains (a `chunkCount` of 0 means that file yielded no indexable text); `chunks` reads one document's chunks in order. |
141
+ | `fruxon assets search/documents/chunks` | Reproduce and inspect a retrieval. `search` runs the same query an agent's `search_assets` does (`--mode HYBRID/VECTOR/FULL_TEXT`, `--top-k`, fusion weights) — note `score` is on a different scale per mode, and in hybrid the top hit is always 1.0. `documents` lists what the index contains (a `chunkCount` of 0 means that file yielded no indexable text); `chunks` reads one document's chunks in order. The `documentId` these print is the **index's** id, which is what `chunks` takes — on a knowledge base's backing asset it is not the editable document's id, and it changes whenever a document's body is re-indexed. The `fileName` (`<knowledge document id>.md`) is the document a `knowledge_base` tool would edit. |
142
142
  | `fruxon metrics list` | Browse the evaluation-metric catalog — the ids a step's LLM-judge config (`judge.metrics[]`) binds, with a default weight each. |
143
143
  | `fruxon triggers list/get/create/update/delete/fire/bind/unbind` | Manage triggers — the schedule/event sources that fire agents. `create`/`update` take `--file` (+`--schema`); `create` also takes `--application`, the Application that will own the trigger and must own everything it fires. `bind`/`unbind` wire which agents fire; `fire` runs it now. `fire`/`delete`/`unbind` confirm + refuse without `--yes` in agent mode. The control plane for autonomy. `get` returns the full stored `workShape` and every `scheduleTimes` slot, so `get -o json | jq .workShape` produces a body `update --file` can post straight back — a PATCH replaces the whole shape, so edit what you read. |
144
- | `fruxon triggers doors` | The pre-configured source doors a work shape can be built on — the **first** question of authoring an automation, and the one `--schema` cannot answer: it tells you a `workShape.door` carries a `doorId`, not which ids exist. Each row is the integration a chosen `integrationConfigId` must belong to, plus the one query field that is genuinely yours (label + example), and `supportsStartFrom` — whether that door's source accepts a lower bound, and so whether a `door.startFrom` horizon does anything there. Read-only and static: a door is a claim that one combination of list tool, identity path and watermark actually works, made in platform code by someone who verified it. An integration with no door is still fully usable — configure `workShape.source` by hand. |
145
- | `fruxon triggers preview-shape/test-source` | Dry-run a work shape before saving or activating it; both write nothing. `preview-shape` renders, per stage, the resolved instruction, the whole `user_query` the agent is handed (item payload included), and the JSON schema the stage's declared `produces` keys become — against a real backlog item when you name a trigger. `test-source` runs the door's list tool once and says, per record, whether admission would take it, plus the exact arguments the tool was called with (for Gmail, your query with the day-granular `after:` folded in). `--count` pages the whole door instead and answers how much is waiting: the total, how many the ledger already holds, how many clear the watermark, and which server ceiling stopped the scan. `--file` tests a shape being edited — and a draft whose `door.startFrom` (Unix ms) sets a first-run horizon gets a `first run sends` line with the `after:` term that horizon produces, which is the only place to see it: `startFrom` seeds the ledger cursor once, when the ledger is created. |
146
- | `fruxon triggers ledger funnel/items/cursor/transitions/effects/batches` | The work items a schedule trigger's work shape admitted. `funnel` counts per stage and status (every item is in exactly one cell). `items` is the grid — `--stage`, `--status`, `--item-key` (client-side; the endpoint has no such filter), and each item's accumulated `data`, which is where a stage's declared `produces` keys land. `cursor` is the watermark and its freshness. `transitions` is one item's attempt history; `effects` is what it did to the outside world — an `UNKNOWN` row parks the item until someone settles it. `batches` is what an ASK stage is waiting on a human for. |
144
+ | `fruxon triggers doors` | The pre-configured source doors a work shape can be built on — the **first** question of authoring an automation, and the one `--schema` cannot answer: it tells you a `workShape.door` carries a `doorId`, not which ids exist. Each row is the integration a chosen `integrationConfigId` must belong to, plus the one query field that is genuinely yours (label + example), and `supportsStartFrom` — whether that door's source accepts a lower bound, and so whether a `door.startFrom` horizon does anything there. `door.skipWhen` is the other field that is yours rather than the integration's: a `{match, conditions[]}` filter, same shape as a trigger's event filter, whose matching records are **never admitted** — no item, no stage, no model call. It has to live on the `door` because saving re-expands `source` from it, so the same filter written onto `source` is discarded by the next save. `door.projection` is the third: a list of dotted record paths naming the fields an admitted item **keeps**, with the door's own `defaultProjection` as the suggestion to seed it from — without one an item carries the whole record, into the Work grid and into every stage's prompt on every pass. Read that against the door's `labelPaths` (or, for the sheet and table doors, `labelFieldName` naming the field whose answer picks the label column): those are where an item's title comes from, and a projection that drops one is refused at save time rather than quietly repaired. Read-only and static: a door is a claim that one combination of list tool, identity path and watermark actually works, made in platform code by someone who verified it. An integration with no door is still fully usable — configure `workShape.source` by hand. |
145
+ | `fruxon triggers preview-shape/test-source` | Dry-run a work shape before saving or activating it; both write nothing. `preview-shape` renders, per stage, the resolved instruction, the whole `user_query` the agent is handed (item payload included), and the JSON schema the stage's declared `produces` keys become — against a real backlog item when you name a trigger. `test-source` runs the door's list tool once and says, per record, whether admission would take it, plus the exact arguments the tool was called with (for Gmail, your query with the day-granular `after:` folded in). Records a `door.skipWhen` filter would drop read `FILTERED`, with a footer count — which is where to see what a rule costs before it goes onto a live automation. `--count` pages the whole door instead and answers how much is waiting: the total, how many the filter drops, how many the ledger already holds, how many clear the watermark, and which server ceiling stopped the scan — so `total − filtered − already held` is the work that would actually arrive. Each record prints **as the item will carry it** — narrowed to the shape's `door.projection`, with an `item keeps` line naming the paths that survived, so a field your projection drops and a field the source never sent are not the same silence. `--file` tests a shape being edited — and a draft whose `door.startFrom` (Unix ms) sets a first-run horizon gets a `first run sends` line with the `after:` term that horizon produces, which is the only place to see it: `startFrom` seeds the ledger cursor once, when the ledger is created. |
146
+ | `fruxon triggers ledger funnel/items/cursor/transitions/effects/batches` | The work items a schedule trigger's work shape admitted. `funnel` counts per stage and status (every item is in exactly one cell). `items` is the grid — `--stage`, `--status`, `--ignore-reason`, `--item-key` (client-side; the endpoint has no such filter), each item's `label` (what the Work grid calls the row, as against the provider's opaque `itemKey`), and its accumulated `data`, which is where a stage's declared `produces` keys land. A listing is **one page of 20** that reports how many matched in total — a ledger's healthy state is thousands of rows served ten per request, so an unbounded walk is rate-limited long before the end; `-n` sizes the page and `--all` walks the lot. `--ignore-reason` narrows to the items one exclusion reason set aside — the same filter `redrive-many` selects by, matched whole and case-sensitively, so it is how you read a batch before putting it back. `cursor` is the watermark and its freshness. `transitions` is one item's attempt history; `effects` is what it did to the outside world — an `UNKNOWN` row parks the item until someone settles it. `batches` is what an ASK stage is waiting on a human for. `funnel` and `items` also carry `cost` (all-in USD) and `runs` — so the funnel says *which stage* the money goes to, not merely how much. An item's `runs` spans every stage it passed through and so is not `stageAttempts`, which resets per stage; a `Cost` of `—` means nothing there has ever been claimed, as against `$0.0000`, which means it ran and was free. Cost is not sortable: the endpoint's `--order-by` allow-list does not carry it. |
147
+ | `fruxon triggers ledger passes` | Every time the automation fired, newest first — `admitted` (what the door took), `dispatched` (what the pass claimed and sent to work, questions parked on a human included), `completed` (how many of *this pass's* dispatches reached the terminal stage), `runs`, and the pass's all-in USD. `runs` sits **below** `dispatched` while runs are in flight or when a dispatch went to a person rather than a model — that gap is normal, not a lost item. And a pass's `cost` **rises after** it fired, as those runs finish, so a pass read seconds later reports a floor rather than its bill. An `outcome` other than `FIRED` means no sweep happened: `FILTERED` / `COALESCED` are healthy (nothing to do, or folded into a pass already running), `FAILED` / `FILTER_ERROR` are not. `-n/--limit` bounds the walk. |
147
148
  | `fruxon triggers ledger redrive/ignore/settle/cancel-batch` | The operator verbs; all confirm and refuse without `--yes` in agent mode. `redrive` returns a DEAD or FAILED item to the queue with its attempts reset — settle any `UNKNOWN` effect first or the next attempt hits the same refusal. `ignore` excludes an item for a **required** reason, which is the only thing separating IGNORED from DEAD later. `settle` answers an effect the platform could not confirm (`--landed` / `--did-not-land` + `--reason`); wrong in the second direction duplicates the write. `cancel-batch` calls off a question — the items go back to READY and a later pass re-batches them. |
148
- | `fruxon triggers ledger redrive-many/ignore-many` | The same two verbs over a **selection** — `--id` (repeatable), `--stage`, `--status`, `--before`/`--after` (Unix ms). The CLI resolves the selection to a count under the verb's own eligibility, shows it, and the server acts on exactly that count (409 if the set changed). `ignore-many --stage X` is how you drain a stage. When the selection is `--id` values, the result names every id it did **not** act on and why — not a work item, another automation's, or a status the verb cannot touch (usually a pass holding the claim). |
149
+ | `fruxon triggers ledger redrive-many/ignore-many` | The same two verbs over a **selection** — `--id` (repeatable), `--stage`, `--status`, `--before`/`--after` (Unix ms). The CLI resolves the selection to a count under the verb's own eligibility, shows it, and the server acts on exactly that count (409 if the set changed). The count is the server's own whenever the endpoint's filters say exactly what the selection says (a `--status` or an `--ignore-reason`, with no `--id` and no `--before`/`--after`) — one request instead of walking every page of a 4,000-item ledger, and one fewer place for the two sides to disagree. `ignore-many --stage X` is how you drain a stage. When the selection is `--id` values, the result names every id it did **not** act on and why — not a work item, another automation's, or a status the verb cannot touch (usually a pass holding the claim). |
150
+ | `fruxon triggers ledger redrive-many --ignore-reason "…"` | Reconsider what one instruction set aside. `IGNORED` is terminal for the scheduler, not for you: a re-drive returns an excluded item to `READY` at the stage that set it aside, with a fresh attempt budget. Exclusions are **opt-in** — they come in only with `--ignore-reason`, `--status IGNORED`, or `--id`, so `--stage X` still means the stuck items there. `--status IGNORED` alone is refused with the count it would have moved, because each item costs an agent run. The reason is matched whole and case-sensitively against what the stage recorded; a wording the shape does not declare but an item carries is still accepted. |
149
151
  | `fruxon triggers ledger reset-cursor/purge` | `reset-cursor` moves where the next scan starts, as a compare-and-swap on the version it just read (`--value` omitted = the door's own horizon); it never re-runs admitted work. It is the *after* to `workShape.door.startFrom`'s *before* — a horizon set at creation seeds the cursor before anything sweeps, and is ignored once a pass has read the source. `purge` starts the ledger over — cancels open questions, deletes items/effects/transitions, bumps the cursor version — and confirms the exact item count first. Destructive; the effects' external targets are not touched. |
150
152
  | `fruxon triggers questions list/answer` | The human half of an ASK stage, tenant-wide. `ledger batches <trigger>` answers *what is this automation waiting on*; this answers *what is waiting on anyone* — soonest deadline first, which is how an undelivered question gets found at all. Read **`delivery`** first: `UNDELIVERABLE` (nobody reachable in the slot) and `FAILED` (a target resolved, the send did not land) both mean nobody was asked and nobody will answer unless you do — `QUEUE_ONLY` is healthy. `--undelivered-only` narrows to that pair; `--trigger` filters client-side (the endpoint has no such filter). `answer` settles one on the assignee's behalf and is `--yes`-gated: the reply interpreter **fails closed**, so an answer it cannot place advances nothing and the question stays open, and each item advances only once the stage's `answeredWhenPath` is filled — answering two of three advances two. |
151
153
  | `fruxon triggers set-stages <id> --file` | Replace an automation's stages without rewriting the rest of its work shape (a PATCH replaces the whole shape, so this reads it, splices, and posts). Takes a bare stages array or any document carrying one. Pre-flights the two refusals the server only reports after the write: live items at a stage your list drops, and a `door` that will re-expand `source` on save. `--dry-run` prints the merged shape and the warnings. |
@@ -101,18 +101,20 @@ Integrations + tools:
101
101
  | `fruxon integrations authorize <integration>` | Mint an application-level OAuth authorization URL to **connect** an integration — auto-detects its OAuth2 method (`--auth-method` to pick, `--scope`/`--config-param` to tune). OAuth needs a browser consent step, so the CLI hands you the link; a human clicks it, and the connection is saved as a tenant config a slot can pin. |
102
102
  | `fruxon integrations triggers <integration>` | List the event types an integration can fire an agent on — each descriptor's `id` is the `eventType` a trigger listens for, plus the `payloadFields` (dotted paths) a binding's `parameterMappings` can read. Discovery for wiring an inbound-event trigger without guessing. |
103
103
  | `fruxon integrations mcp …` | Inspect and enable the MCP server for an integration. |
104
- | `fruxon tools list/get/create/update/delete/run` | Manage the tools inside an integration. Integration-scoped. `run` executes one tool outside any agent — real credentials, real result — via `--tool` plus `-p key=value`, with `-c` naming the config that supplies credentials. |
104
+ | `fruxon tools list/get/create/update/delete/run` | Manage the tools inside an integration. Integration-scoped. `get` and `run` select the tool with `--tool`; `run` executes it outside any agent — real credentials, real result — with `-p key=value`, and `-c` naming the config that supplies credentials. The tenant list API hides internal `knowledge_base` / `assets` tools; `tools list` recovers them by ID and lists them like any other (`--no-include-internal` for the raw server answer). |
105
105
  | `fruxon keys list/mint/revoke/delete/history/scopes` | Audit and revoke scoped tokens. Minting opens the dashboard so the secret never enters the CLI process. |
106
106
  | `fruxon llm-providers list/get/models` | Browse LLM providers and models supported at the tenant level. |
107
107
  | `fruxon assets create/list/get/wait/operations/delete` | Manage knowledge-base (RAG) assets a step can query — upload local files, wait for async ingestion, inspect operations, and use the ids for `assetConfig.assetIds`. `delete` confirms + refuses without `--yes` in agent mode. |
108
- | `fruxon assets search/documents/chunks` | Reproduce and inspect a retrieval. `search` runs the same query an agent's `search_assets` does (`--mode HYBRID/VECTOR/FULL_TEXT`, `--top-k`, fusion weights) — note `score` is on a different scale per mode, and in hybrid the top hit is always 1.0. `documents` lists what the index contains (a `chunkCount` of 0 means that file yielded no indexable text); `chunks` reads one document's chunks in order. |
108
+ | `fruxon assets search/documents/chunks` | Reproduce and inspect a retrieval. `search` runs the same query an agent's `search_assets` does (`--mode HYBRID/VECTOR/FULL_TEXT`, `--top-k`, fusion weights) — note `score` is on a different scale per mode, and in hybrid the top hit is always 1.0. `documents` lists what the index contains (a `chunkCount` of 0 means that file yielded no indexable text); `chunks` reads one document's chunks in order. The `documentId` these print is the **index's** id, which is what `chunks` takes — on a knowledge base's backing asset it is not the editable document's id, and it changes whenever a document's body is re-indexed. The `fileName` (`<knowledge document id>.md`) is the document a `knowledge_base` tool would edit. |
109
109
  | `fruxon metrics list` | Browse the evaluation-metric catalog — the ids a step's LLM-judge config (`judge.metrics[]`) binds, with a default weight each. |
110
110
  | `fruxon triggers list/get/create/update/delete/fire/bind/unbind` | Manage triggers — the schedule/event sources that fire agents. `create`/`update` take `--file` (+`--schema`); `create` also takes `--application`, the Application that will own the trigger and must own everything it fires. `bind`/`unbind` wire which agents fire; `fire` runs it now. `fire`/`delete`/`unbind` confirm + refuse without `--yes` in agent mode. The control plane for autonomy. `get` returns the full stored `workShape` and every `scheduleTimes` slot, so `get -o json | jq .workShape` produces a body `update --file` can post straight back — a PATCH replaces the whole shape, so edit what you read. |
111
- | `fruxon triggers doors` | The pre-configured source doors a work shape can be built on — the **first** question of authoring an automation, and the one `--schema` cannot answer: it tells you a `workShape.door` carries a `doorId`, not which ids exist. Each row is the integration a chosen `integrationConfigId` must belong to, plus the one query field that is genuinely yours (label + example), and `supportsStartFrom` — whether that door's source accepts a lower bound, and so whether a `door.startFrom` horizon does anything there. Read-only and static: a door is a claim that one combination of list tool, identity path and watermark actually works, made in platform code by someone who verified it. An integration with no door is still fully usable — configure `workShape.source` by hand. |
112
- | `fruxon triggers preview-shape/test-source` | Dry-run a work shape before saving or activating it; both write nothing. `preview-shape` renders, per stage, the resolved instruction, the whole `user_query` the agent is handed (item payload included), and the JSON schema the stage's declared `produces` keys become — against a real backlog item when you name a trigger. `test-source` runs the door's list tool once and says, per record, whether admission would take it, plus the exact arguments the tool was called with (for Gmail, your query with the day-granular `after:` folded in). `--count` pages the whole door instead and answers how much is waiting: the total, how many the ledger already holds, how many clear the watermark, and which server ceiling stopped the scan. `--file` tests a shape being edited — and a draft whose `door.startFrom` (Unix ms) sets a first-run horizon gets a `first run sends` line with the `after:` term that horizon produces, which is the only place to see it: `startFrom` seeds the ledger cursor once, when the ledger is created. |
113
- | `fruxon triggers ledger funnel/items/cursor/transitions/effects/batches` | The work items a schedule trigger's work shape admitted. `funnel` counts per stage and status (every item is in exactly one cell). `items` is the grid — `--stage`, `--status`, `--item-key` (client-side; the endpoint has no such filter), and each item's accumulated `data`, which is where a stage's declared `produces` keys land. `cursor` is the watermark and its freshness. `transitions` is one item's attempt history; `effects` is what it did to the outside world — an `UNKNOWN` row parks the item until someone settles it. `batches` is what an ASK stage is waiting on a human for. |
111
+ | `fruxon triggers doors` | The pre-configured source doors a work shape can be built on — the **first** question of authoring an automation, and the one `--schema` cannot answer: it tells you a `workShape.door` carries a `doorId`, not which ids exist. Each row is the integration a chosen `integrationConfigId` must belong to, plus the one query field that is genuinely yours (label + example), and `supportsStartFrom` — whether that door's source accepts a lower bound, and so whether a `door.startFrom` horizon does anything there. `door.skipWhen` is the other field that is yours rather than the integration's: a `{match, conditions[]}` filter, same shape as a trigger's event filter, whose matching records are **never admitted** — no item, no stage, no model call. It has to live on the `door` because saving re-expands `source` from it, so the same filter written onto `source` is discarded by the next save. `door.projection` is the third: a list of dotted record paths naming the fields an admitted item **keeps**, with the door's own `defaultProjection` as the suggestion to seed it from — without one an item carries the whole record, into the Work grid and into every stage's prompt on every pass. Read that against the door's `labelPaths` (or, for the sheet and table doors, `labelFieldName` naming the field whose answer picks the label column): those are where an item's title comes from, and a projection that drops one is refused at save time rather than quietly repaired. Read-only and static: a door is a claim that one combination of list tool, identity path and watermark actually works, made in platform code by someone who verified it. An integration with no door is still fully usable — configure `workShape.source` by hand. |
112
+ | `fruxon triggers preview-shape/test-source` | Dry-run a work shape before saving or activating it; both write nothing. `preview-shape` renders, per stage, the resolved instruction, the whole `user_query` the agent is handed (item payload included), and the JSON schema the stage's declared `produces` keys become — against a real backlog item when you name a trigger. `test-source` runs the door's list tool once and says, per record, whether admission would take it, plus the exact arguments the tool was called with (for Gmail, your query with the day-granular `after:` folded in). Records a `door.skipWhen` filter would drop read `FILTERED`, with a footer count — which is where to see what a rule costs before it goes onto a live automation. `--count` pages the whole door instead and answers how much is waiting: the total, how many the filter drops, how many the ledger already holds, how many clear the watermark, and which server ceiling stopped the scan — so `total − filtered − already held` is the work that would actually arrive. Each record prints **as the item will carry it** — narrowed to the shape's `door.projection`, with an `item keeps` line naming the paths that survived, so a field your projection drops and a field the source never sent are not the same silence. `--file` tests a shape being edited — and a draft whose `door.startFrom` (Unix ms) sets a first-run horizon gets a `first run sends` line with the `after:` term that horizon produces, which is the only place to see it: `startFrom` seeds the ledger cursor once, when the ledger is created. |
113
+ | `fruxon triggers ledger funnel/items/cursor/transitions/effects/batches` | The work items a schedule trigger's work shape admitted. `funnel` counts per stage and status (every item is in exactly one cell). `items` is the grid — `--stage`, `--status`, `--ignore-reason`, `--item-key` (client-side; the endpoint has no such filter), each item's `label` (what the Work grid calls the row, as against the provider's opaque `itemKey`), and its accumulated `data`, which is where a stage's declared `produces` keys land. A listing is **one page of 20** that reports how many matched in total — a ledger's healthy state is thousands of rows served ten per request, so an unbounded walk is rate-limited long before the end; `-n` sizes the page and `--all` walks the lot. `--ignore-reason` narrows to the items one exclusion reason set aside — the same filter `redrive-many` selects by, matched whole and case-sensitively, so it is how you read a batch before putting it back. `cursor` is the watermark and its freshness. `transitions` is one item's attempt history; `effects` is what it did to the outside world — an `UNKNOWN` row parks the item until someone settles it. `batches` is what an ASK stage is waiting on a human for. `funnel` and `items` also carry `cost` (all-in USD) and `runs` — so the funnel says *which stage* the money goes to, not merely how much. An item's `runs` spans every stage it passed through and so is not `stageAttempts`, which resets per stage; a `Cost` of `—` means nothing there has ever been claimed, as against `$0.0000`, which means it ran and was free. Cost is not sortable: the endpoint's `--order-by` allow-list does not carry it. |
114
+ | `fruxon triggers ledger passes` | Every time the automation fired, newest first — `admitted` (what the door took), `dispatched` (what the pass claimed and sent to work, questions parked on a human included), `completed` (how many of *this pass's* dispatches reached the terminal stage), `runs`, and the pass's all-in USD. `runs` sits **below** `dispatched` while runs are in flight or when a dispatch went to a person rather than a model — that gap is normal, not a lost item. And a pass's `cost` **rises after** it fired, as those runs finish, so a pass read seconds later reports a floor rather than its bill. An `outcome` other than `FIRED` means no sweep happened: `FILTERED` / `COALESCED` are healthy (nothing to do, or folded into a pass already running), `FAILED` / `FILTER_ERROR` are not. `-n/--limit` bounds the walk. |
114
115
  | `fruxon triggers ledger redrive/ignore/settle/cancel-batch` | The operator verbs; all confirm and refuse without `--yes` in agent mode. `redrive` returns a DEAD or FAILED item to the queue with its attempts reset — settle any `UNKNOWN` effect first or the next attempt hits the same refusal. `ignore` excludes an item for a **required** reason, which is the only thing separating IGNORED from DEAD later. `settle` answers an effect the platform could not confirm (`--landed` / `--did-not-land` + `--reason`); wrong in the second direction duplicates the write. `cancel-batch` calls off a question — the items go back to READY and a later pass re-batches them. |
115
- | `fruxon triggers ledger redrive-many/ignore-many` | The same two verbs over a **selection** — `--id` (repeatable), `--stage`, `--status`, `--before`/`--after` (Unix ms). The CLI resolves the selection to a count under the verb's own eligibility, shows it, and the server acts on exactly that count (409 if the set changed). `ignore-many --stage X` is how you drain a stage. When the selection is `--id` values, the result names every id it did **not** act on and why — not a work item, another automation's, or a status the verb cannot touch (usually a pass holding the claim). |
116
+ | `fruxon triggers ledger redrive-many/ignore-many` | The same two verbs over a **selection** — `--id` (repeatable), `--stage`, `--status`, `--before`/`--after` (Unix ms). The CLI resolves the selection to a count under the verb's own eligibility, shows it, and the server acts on exactly that count (409 if the set changed). The count is the server's own whenever the endpoint's filters say exactly what the selection says (a `--status` or an `--ignore-reason`, with no `--id` and no `--before`/`--after`) — one request instead of walking every page of a 4,000-item ledger, and one fewer place for the two sides to disagree. `ignore-many --stage X` is how you drain a stage. When the selection is `--id` values, the result names every id it did **not** act on and why — not a work item, another automation's, or a status the verb cannot touch (usually a pass holding the claim). |
117
+ | `fruxon triggers ledger redrive-many --ignore-reason "…"` | Reconsider what one instruction set aside. `IGNORED` is terminal for the scheduler, not for you: a re-drive returns an excluded item to `READY` at the stage that set it aside, with a fresh attempt budget. Exclusions are **opt-in** — they come in only with `--ignore-reason`, `--status IGNORED`, or `--id`, so `--stage X` still means the stuck items there. `--status IGNORED` alone is refused with the count it would have moved, because each item costs an agent run. The reason is matched whole and case-sensitively against what the stage recorded; a wording the shape does not declare but an item carries is still accepted. |
116
118
  | `fruxon triggers ledger reset-cursor/purge` | `reset-cursor` moves where the next scan starts, as a compare-and-swap on the version it just read (`--value` omitted = the door's own horizon); it never re-runs admitted work. It is the *after* to `workShape.door.startFrom`'s *before* — a horizon set at creation seeds the cursor before anything sweeps, and is ignored once a pass has read the source. `purge` starts the ledger over — cancels open questions, deletes items/effects/transitions, bumps the cursor version — and confirms the exact item count first. Destructive; the effects' external targets are not touched. |
117
119
  | `fruxon triggers questions list/answer` | The human half of an ASK stage, tenant-wide. `ledger batches <trigger>` answers *what is this automation waiting on*; this answers *what is waiting on anyone* — soonest deadline first, which is how an undelivered question gets found at all. Read **`delivery`** first: `UNDELIVERABLE` (nobody reachable in the slot) and `FAILED` (a target resolved, the send did not land) both mean nobody was asked and nobody will answer unless you do — `QUEUE_ONLY` is healthy. `--undelivered-only` narrows to that pair; `--trigger` filters client-side (the endpoint has no such filter). `answer` settles one on the assignee's behalf and is `--yes`-gated: the reply interpreter **fails closed**, so an answer it cannot place advances nothing and the question stays open, and each item advances only once the stage's `answeredWhenPath` is filled — answering two of three advances two. |
118
120
  | `fruxon triggers set-stages <id> --file` | Replace an automation's stages without rewriting the rest of its work shape (a PATCH replaces the whole shape, so this reads it, splices, and posts). Takes a bare stages array or any document carrying one. Pre-flights the two refusals the server only reports after the write: live items at a stage your list drops, and a `door` that will re-expand `source` on save. `--dry-run` prints the merged shape and the warnings. |
@@ -48,6 +48,7 @@ from fruxon.models import (
48
48
  LedgerFunnelCell,
49
49
  LedgerItem,
50
50
  LedgerItemTransition,
51
+ LedgerPass,
51
52
  LedgerPurgeResult,
52
53
  LedgerSelectionResult,
53
54
  LedgerSkippedItem,
@@ -73,6 +74,7 @@ from fruxon.models import (
73
74
  TriggerRevision,
74
75
  UnhandledEscalation,
75
76
  WorkShapeDoor,
77
+ WorkShapeDoorField,
76
78
  WorkShapeSourceCount,
77
79
  WorkShapeSourceTest,
78
80
  WorkShapeSourceTestRecord,
@@ -119,6 +121,7 @@ __all__ = [
119
121
  "LedgerFunnelCell",
120
122
  "LedgerItem",
121
123
  "LedgerItemTransition",
124
+ "LedgerPass",
122
125
  "LedgerPurgeResult",
123
126
  "LedgerSelectionResult",
124
127
  "LedgerSkippedItem",
@@ -147,6 +150,7 @@ __all__ = [
147
150
  "UnhandledEscalation",
148
151
  "ValidationError",
149
152
  "WorkShapeDoor",
153
+ "WorkShapeDoorField",
150
154
  "WorkShapeSourceCount",
151
155
  "WorkShapeSourceTest",
152
156
  "WorkShapeSourceTestRecord",
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.13.0'
22
- __version_tuple__ = version_tuple = (0, 13, 0)
21
+ __version__ = version = '0.13.2'
22
+ __version_tuple__ = version_tuple = (0, 13, 2)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -76,6 +76,8 @@ def _classify_exception(exc: BaseException) -> str:
76
76
  import ssl
77
77
  import urllib.error as _ue
78
78
 
79
+ from fruxon.cli import _crash
80
+
79
81
  if isinstance(exc, typer.Exit):
80
82
  # ``typer.Exit`` is a normal-flow signal even with a non-zero exit
81
83
  # code — it's how we surface a handled failure, not a crash. But it
@@ -86,6 +88,8 @@ def _classify_exception(exc: BaseException) -> str:
86
88
  # ``fail()``" and the schema's ``auth`` / ``not_found`` /
87
89
  # ``server_error`` values were allowlisted but never once sent.
88
90
  return _EXIT_CODE_CLASSES.get(getattr(exc, "exit_code", 1), "other")
91
+ if _crash.is_usage_error(exc):
92
+ return "user_input"
89
93
  if isinstance(exc, KeyboardInterrupt):
90
94
  return "user_input"
91
95
  if isinstance(exc, (_ue.URLError, socket.timeout, ConnectionError, ssl.SSLError, OSError)):
@@ -143,6 +147,31 @@ def _report_crash(exc: BaseException, command: str) -> None:
143
147
  stderr.print(" [dim]Full traceback here:[/dim] FRUXON_DEBUG=1 fruxon …")
144
148
 
145
149
 
150
+ def _render_usage_error(exc: BaseException, command: str) -> None:
151
+ """Turn a Click parse failure into the agent contract's ``usage`` envelope.
152
+
153
+ Typer 0.26 vendors Click, so these exceptions bypass Typer's normal
154
+ top-level formatter while our custom group is on the stack — and what
155
+ reaches an AI driver is Click's bordered ASCII box, which is neither JSON
156
+ nor distinguishable from a crash. The contract (``fruxon guides show
157
+ fruxon-agent-mode``) says a parse error is one JSON envelope on stderr and
158
+ exit 2, and this is the single place that renders it.
159
+
160
+ Raises ``typer.Exit(2)``; never returns.
161
+ """
162
+ message = exc.format_message().strip() if hasattr(exc, "format_message") else str(exc).strip()
163
+ if not message:
164
+ message = "A subcommand is required."
165
+ usage_ctx = getattr(exc, "ctx", None)
166
+ command_path = getattr(usage_ctx, "command_path", None) or f"fruxon {command}"
167
+ say_err(
168
+ message,
169
+ hint=f"Run [bold]{command_path} --help[/bold] for usage.",
170
+ exit_code=2,
171
+ )
172
+ raise typer.Exit(2) from None
173
+
174
+
146
175
  class _FruxonTyperGroup(TyperGroup):
147
176
  """Branded replacement for Typer's stock ``no such command`` error.
148
177
 
@@ -160,6 +189,34 @@ class _FruxonTyperGroup(TyperGroup):
160
189
  signal.
161
190
  """
162
191
 
192
+ def make_context(self, info_name, args, parent=None, **extra):
193
+ """Catch the parse failures that happen *before* any subcommand runs.
194
+
195
+ ``invoke`` below covers everything raised while dispatching into the
196
+ tree, which is most of it. It cannot cover the root's own argument
197
+ parsing: Click builds the root context first and hands the failure
198
+ straight to its top-level handler, so ``fruxon --bogus`` printed the
199
+ ASCII box to an AI driver that had been promised JSON for every other
200
+ parse error in the CLI. Same envelope, same exit code, one fewer shape
201
+ for a caller to special-case.
202
+ """
203
+ try:
204
+ return super().make_context(info_name, args, parent=parent, **extra)
205
+ except BaseException as exc:
206
+ from fruxon.cli import _crash
207
+
208
+ if not (_crash.is_usage_error(exc) and is_agent_mode()):
209
+ raise
210
+ # The event the command would have sent had it got far enough to
211
+ # run. Reported as "(root)" because that is literally as far as it
212
+ # got — argv here has not been parsed, and putting an unparsed
213
+ # token on the wire is the thing `_command_path` exists to prevent.
214
+ telemetry.track(
215
+ "CLI Command Failed",
216
+ {"command": "(root)", "error_class": "user_input", "duration_ms": 0},
217
+ )
218
+ _render_usage_error(exc, "(root)")
219
+
163
220
  def invoke(self, ctx):
164
221
  """Wrap subcommand execution with telemetry timing.
165
222
 
@@ -206,6 +263,9 @@ class _FruxonTyperGroup(TyperGroup):
206
263
  properties["error_fingerprint"] = mark
207
264
  telemetry.track("CLI Command Failed", properties)
208
265
 
266
+ if _crash.is_usage_error(exc) and is_agent_mode():
267
+ _render_usage_error(exc, command_name)
268
+
209
269
  if unexpected and not _crash.debug_enabled():
210
270
  _report_crash(exc, command_name)
211
271
  # Replaces the traceback rather than printing alongside it.
@@ -263,7 +323,7 @@ class _FruxonTyperGroup(TyperGroup):
263
323
  )
264
324
  else:
265
325
  hint = "Run [bold]fruxon --help[/bold] for the full command list."
266
- say_err(f"Unknown command [bold]{cmd_name}[/bold].", hint=hint)
326
+ say_err(f"Unknown command [bold]{cmd_name}[/bold].", hint=hint, exit_code=2)
267
327
  ctx.exit(2)
268
328
  return None # unreachable but keeps the type checker happy
269
329
 
@@ -47,15 +47,48 @@ def is_unexpected(exc: BaseException) -> bool:
47
47
  changing their mind, and greeting it with a crash report would be absurd.
48
48
  What is left is a bug.
49
49
  """
50
- import click
51
50
  import typer
52
51
 
53
- return not isinstance(
54
- exc,
55
- (typer.Exit, typer.Abort, click.ClickException, click.exceptions.Abort, SystemExit, KeyboardInterrupt),
52
+ return (
53
+ not isinstance(exc, (typer.Exit, typer.Abort, SystemExit, KeyboardInterrupt))
54
+ and not _is_click_exception(exc)
55
+ and not _is_click_exception(exc, class_name="Abort")
56
56
  )
57
57
 
58
58
 
59
+ def is_usage_error(exc: BaseException) -> bool:
60
+ """Whether ``exc`` is a parse/usage error from either Click namespace.
61
+
62
+ Typer 0.26 vendors Click under :mod:`typer._click`. Its
63
+ ``NoSuchOption`` / ``MissingParameter`` / ``NoArgsIsHelpError`` classes
64
+ are therefore *not* subclasses of the top-level ``click.UsageError``.
65
+ Checking both namespaces is what keeps an ordinary typo out of the crash
66
+ reporter while still supporting Typer releases that use external Click.
67
+ """
68
+ return _is_click_exception(exc, class_name="UsageError")
69
+
70
+
71
+ def _is_click_exception(exc: BaseException, *, class_name: str = "ClickException") -> bool:
72
+ """Match Click's public exceptions and Typer's vendored equivalents."""
73
+ import click
74
+
75
+ classes: list[type[BaseException]] = []
76
+ external = getattr(click.exceptions, class_name, None)
77
+ if isinstance(external, type) and issubclass(external, BaseException):
78
+ classes.append(external)
79
+
80
+ try:
81
+ from typer._click import exceptions as typer_click_exceptions
82
+
83
+ vendored = getattr(typer_click_exceptions, class_name, None)
84
+ if isinstance(vendored, type) and issubclass(vendored, BaseException):
85
+ classes.append(vendored)
86
+ except ImportError: # Older Typer releases use the top-level Click package.
87
+ pass
88
+
89
+ return bool(classes) and isinstance(exc, tuple(classes))
90
+
91
+
59
92
  def fingerprint(exc: BaseException) -> str | None:
60
93
  """``"KeyError at cli/agents_revisions.py:97"`` — safe to put on the wire.
61
94
 
@@ -71,10 +71,13 @@ def _fetch_spec(base_url: str) -> dict:
71
71
  try:
72
72
  with urllib.request.urlopen(url, timeout=10, context=ctx) as resp: # noqa: S310 — http(s) only
73
73
  data = json.load(resp)
74
- except urllib.error.URLError as exc:
74
+ except (urllib.error.URLError, TimeoutError) as exc:
75
75
  fail(
76
76
  f"Couldn't fetch the OpenAPI spec from {url}: {exc}",
77
- hint="Check that the server is reachable, then try again.",
77
+ hint=(
78
+ "Schema output is fetched from the target server. Check that it is reachable, "
79
+ "or pass --base-url for a server that exposes /swagger/v1/swagger.json."
80
+ ),
78
81
  code=EXIT_NETWORK,
79
82
  )
80
83
  except json.JSONDecodeError as exc:
@@ -17,7 +17,7 @@ from typing import Annotated
17
17
 
18
18
  import typer
19
19
 
20
- from fruxon.cli._shared import build_client
20
+ from fruxon.cli._shared import build_client, confirm_or_die
21
21
  from fruxon.cli.agents import (
22
22
  _hint_for_get_error,
23
23
  _hint_for_write_error,
@@ -1021,6 +1021,10 @@ def draft_evaluate(
1021
1021
  @draft_app.command("discard")
1022
1022
  def draft_discard(
1023
1023
  agent: Annotated[str, typer.Argument(help="Agent identifier.")],
1024
+ yes: Annotated[
1025
+ bool,
1026
+ typer.Option("--yes", "-y", help="Skip the confirmation prompt. REQUIRED under agent mode (CLAUDECODE / CI)."),
1027
+ ] = False,
1024
1028
  workspace: Annotated[str | None, typer.Option("--workspace", help="Workspace identifier override.")] = None,
1025
1029
  base_url: Annotated[str | None, typer.Option("--base-url", help="API base URL override.")] = None,
1026
1030
  output: Annotated[
@@ -1032,7 +1036,29 @@ def draft_discard(
1032
1036
  ),
1033
1037
  ] = None,
1034
1038
  ):
1035
- """Delete the draft on the server and remove the local sidecar."""
1039
+ """Delete the draft on the server and remove the local sidecar.
1040
+
1041
+ The draft and its undo history cannot be recovered. Confirms first; under
1042
+ agent mode, pass ``--yes`` explicitly so the command never blocks waiting
1043
+ for input.
1044
+
1045
+ Examples:
1046
+ fruxon agents draft discard my-agent --yes
1047
+ fruxon agents draft discard my-agent --yes -o json
1048
+ """
1049
+ output = resolve_output_format(output, human_default="text", agent_default="json")
1050
+ if output not in {"text", "json"}:
1051
+ fail(
1052
+ f"Unknown output format: [bold]{output}[/bold]",
1053
+ hint="Valid formats: text, json.",
1054
+ code=EXIT_VALIDATION,
1055
+ )
1056
+ confirm_or_die(
1057
+ f"Discard the server draft for [bold]{agent}[/bold]? Its undo history will also be deleted.",
1058
+ yes=yes,
1059
+ action=f"discard the draft for {agent}",
1060
+ )
1061
+
1036
1062
  client = build_client(workspace, base_url)
1037
1063
  meta = _read_sidecar(agent)
1038
1064
  try:
@@ -1041,7 +1067,6 @@ def draft_discard(
1041
1067
  fail_from_api_error(e, hint=_hint_for_write_error(e))
1042
1068
  _sidecar_path(agent).unlink(missing_ok=True)
1043
1069
 
1044
- output = resolve_output_format(output, human_default="text", agent_default="json")
1045
1070
  if output == "json":
1046
1071
  print(json.dumps({"agent": agent, "discarded": True}, indent=2))
1047
1072
  return
@@ -146,6 +146,10 @@ def revisions_get(
146
146
  ] = False,
147
147
  workspace: Annotated[str | None, typer.Option("--workspace", help="Workspace identifier override.")] = None,
148
148
  base_url: Annotated[str | None, typer.Option("--base-url", help="API base URL override.")] = None,
149
+ output: Annotated[
150
+ str | None,
151
+ typer.Option("--output", "-o", help="Output format: json (the only format for this pipe-oriented command)."),
152
+ ] = None,
149
153
  ):
150
154
  """Print a revision's full body.
151
155
 
@@ -168,9 +172,16 @@ def revisions_get(
168
172
  Examples:
169
173
  fruxon agents revisions get my-agent # current deployed revision
170
174
  fruxon agents revisions get my-agent 3 # a specific revision
171
- fruxon agents revisions get my-agent --as-draft # postable body
175
+ fruxon agents revisions get my-agent --as-draft -o json # postable body
172
176
  fruxon agents revisions get my-agent | jq .definition # just the agent
173
177
  """
178
+ if output not in {None, "json"}:
179
+ fail(
180
+ f"Unknown output format: [bold]{output}[/bold]",
181
+ hint="This command always emits JSON; pass [bold]--output json[/bold] or omit the flag.",
182
+ code=EXIT_VALIDATION,
183
+ )
184
+
174
185
  client = build_client(workspace, base_url)
175
186
 
176
187
  target_revision: int | str