gravity-cli 0.5.3__tar.gz → 0.5.4__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 (121) hide show
  1. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/AGENT_STANDARD.md +20 -18
  2. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/PKG-INFO +29 -15
  3. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/README.md +27 -13
  4. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/gravity_schema/cron.py +26 -1
  5. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/gravity_schema/manifest.py +56 -0
  6. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/gravity_schema/validation.py +11 -1
  7. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/pyproject.toml +1 -1
  8. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/tests/test_cron.py +16 -1
  9. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/tests/test_manifest.py +106 -0
  10. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/pyproject.toml +2 -2
  11. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/BUILDER_GUIDE.md +37 -26
  12. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/_runner_script.py +9 -3
  13. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/api.py +57 -15
  14. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/cli.py +13 -0
  15. gravity_cli-0.5.4/src/gravity_cli/commands/adapt.py +182 -0
  16. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/commands/build.py +13 -2
  17. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/commands/connect.py +21 -4
  18. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/commands/dev.py +1 -1
  19. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/commands/login.py +19 -3
  20. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/commands/push.py +16 -2
  21. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/commands/run.py +30 -4
  22. gravity_cli-0.5.4/src/gravity_cli/commands/runs.py +103 -0
  23. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/commands/schedule.py +23 -6
  24. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/commands/validate.py +95 -19
  25. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/config.py +27 -8
  26. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/dev.py +9 -3
  27. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/project.py +7 -2
  28. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/timeline.py +3 -2
  29. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/ui.py +49 -6
  30. gravity_cli-0.5.4/tests/test_adapt.py +134 -0
  31. gravity_cli-0.5.4/tests/test_agents.py +91 -0
  32. gravity_cli-0.5.4/tests/test_api.py +218 -0
  33. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/tests/test_build_push.py +70 -3
  34. gravity_cli-0.5.4/tests/test_connect.py +146 -0
  35. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/tests/test_dev.py +27 -3
  36. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/tests/test_run.py +71 -7
  37. gravity_cli-0.5.4/tests/test_runs.py +91 -0
  38. gravity_cli-0.5.4/tests/test_schedule.py +63 -0
  39. gravity_cli-0.5.4/tests/test_session.py +146 -0
  40. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/tests/test_templates.py +21 -0
  41. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/tests/test_ui.py +14 -0
  42. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/tests/test_validate.py +69 -2
  43. gravity_cli-0.5.4/tests/test_welcome.py +98 -0
  44. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/uv.lock +6 -6
  45. gravity_cli-0.5.3/src/gravity_cli/commands/adapt.py +0 -154
  46. gravity_cli-0.5.3/src/gravity_cli/commands/runs.py +0 -49
  47. gravity_cli-0.5.3/tests/test_adapt.py +0 -71
  48. gravity_cli-0.5.3/tests/test_api.py +0 -101
  49. gravity_cli-0.5.3/tests/test_connect.py +0 -78
  50. gravity_cli-0.5.3/tests/test_session.py +0 -80
  51. gravity_cli-0.5.3/tests/test_welcome.py +0 -43
  52. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/.env.example +0 -0
  53. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/.github/workflows/ci.yml +0 -0
  54. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/.github/workflows/release.yml +0 -0
  55. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/.gitignore +0 -0
  56. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/LICENSE +0 -0
  57. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/NOTICE +0 -0
  58. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/RELEASING.md +0 -0
  59. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/TEMPLATES.md +0 -0
  60. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/LICENSE +0 -0
  61. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/NOTICE +0 -0
  62. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/README.md +0 -0
  63. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/gravity_schema/__init__.py +0 -0
  64. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/gravity_schema/__main__.py +0 -0
  65. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/gravity_schema/catalog.py +0 -0
  66. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/gravity_schema/catalog.yaml +0 -0
  67. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/gravity_schema/catalog_imported.yaml +0 -0
  68. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/gravity_schema/contract.py +0 -0
  69. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/gravity_schema/errors.py +0 -0
  70. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/gravity_schema/export.py +0 -0
  71. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/gravity_schema/gen_catalog.py +0 -0
  72. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/gravity_schema/source_scan.py +0 -0
  73. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/schema/manifest.schema.json +0 -0
  74. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/tests/conftest.py +0 -0
  75. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/tests/test_catalog_parity.py +0 -0
  76. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/tests/test_errors.py +0 -0
  77. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/tests/test_json_schema.py +0 -0
  78. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/tests/test_kind_outputs.py +0 -0
  79. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/tests/test_slots.py +0 -0
  80. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/tests/test_source_scan.py +0 -0
  81. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/packages/gravity-schema/tests/test_validation.py +0 -0
  82. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/__init__.py +0 -0
  83. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/__main__.py +0 -0
  84. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/commands/__init__.py +0 -0
  85. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/commands/guide.py +0 -0
  86. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/commands/init.py +0 -0
  87. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/commands/tools.py +0 -0
  88. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/dev_page/index.html +0 -0
  89. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/localrun.py +0 -0
  90. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/manifest.py +0 -0
  91. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/chat/.gitignore +0 -0
  92. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/chat/manifest.yaml +0 -0
  93. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/chat/requirements.txt +0 -0
  94. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/chat/src/agent.py +0 -0
  95. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/deepagent/.gitignore +0 -0
  96. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/deepagent/manifest.yaml +0 -0
  97. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/deepagent/requirements.txt +0 -0
  98. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/deepagent/src/agent.py +0 -0
  99. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/minimal/.gitignore +0 -0
  100. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/minimal/manifest.yaml +0 -0
  101. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/minimal/requirements.txt +0 -0
  102. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/minimal/src/agent.py +0 -0
  103. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/structured/.gitignore +0 -0
  104. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/structured/manifest.yaml +0 -0
  105. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/structured/requirements.txt +0 -0
  106. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/structured/src/agent.py +0 -0
  107. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/structured/src/gateway.py +0 -0
  108. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/structured/src/nodes/agent.py +0 -0
  109. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/structured/src/prompts.py +0 -0
  110. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/src/gravity_cli/templates/structured/src/state.py +0 -0
  111. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/starter/manifest.yaml +0 -0
  112. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/starter/requirements.txt +0 -0
  113. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/starter/src/agent.py +0 -0
  114. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/starter/src/summary.py +0 -0
  115. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/starter/tests/test_summary.py +0 -0
  116. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/tests/conftest.py +0 -0
  117. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/tests/test_gateway_token.py +0 -0
  118. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/tests/test_localrun.py +0 -0
  119. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/tests/test_manifest_integration.py +0 -0
  120. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/tests/test_timeline.py +0 -0
  121. {gravity_cli-0.5.3 → gravity_cli-0.5.4}/tests/test_tools.py +0 -0
@@ -39,7 +39,7 @@ description: One line. # ≤500 chars
39
39
 
40
40
  runtime:
41
41
  framework: langgraph # only value accepted
42
- python: "3.13" # 3.10–3.13; the lockfile is compiled for it
42
+ python: "3.12" # the platform runs 3.12; validate refuses any other
43
43
  entrypoint: src/agent.py:graph # file:object — see §3
44
44
  timeout_seconds: 300 # 10–900
45
45
 
@@ -67,7 +67,7 @@ resources:
67
67
  triggers: # optional — what starts a run
68
68
  manual: true # default
69
69
  # schedule:
70
- # cron: "0 9 * * 1" # 5 fields: digits, * / , - only (no @daily, no MON)
70
+ # cron: "0 9 * * 1" # 5 fields: digits, * / , - only (no @daily, no MON); every 5 minutes at the fastest; one day field at most; Asia/Kolkata
71
71
  # consumer_can_change: true
72
72
 
73
73
  approvals: # optional — tools that pause the run for the consumer's yes
@@ -78,8 +78,9 @@ approvals: # optional — tools that pause the run for th
78
78
  ```
79
79
 
80
80
  **`approvals` is enforced by the gateway.** A listed tool is held until a
81
- human answers: in `gravity run` you're asked (`Approve? [y/N]`, default no;
82
- no answer is a no); in the product it's the consumer's approval inbox. Not
81
+ human answers: in `gravity run` you're asked (`y approve · n reject · enter reject`,
82
+ default no; no answer is a no) and `gravity dev` shows Approve/Reject buttons; in the
83
+ product it's the consumer's approval inbox. Not
83
84
  approved, the call returns `APPROVAL_REJECTED` (or `APPROVAL_TIMEOUT` after 4
84
85
  minutes) and never runs — handle that and fall back, as `meeting-notes`
85
86
  keeps the recap as a draft. Your code can't skip the question: the gateway
@@ -109,7 +110,7 @@ async def graph(): # ✓ recommended — an async factory
109
110
  ...
110
111
  return workflow.compile()
111
112
 
112
- graph = asyncio.run(_build()) # ✓ a graph compiled at import
113
+ graph = asyncio.run(_build()) # ✗ gravity validate refuses asyncio.run at import
113
114
  graph = _build() # ✓ also fine — a coroutine is awaited
114
115
  ```
115
116
 
@@ -179,9 +180,10 @@ tools = await client.get_tools() # exactly permissions.tools, nothing else
179
180
  Tool names are `<app>.<action>`, lowercase, from `gravity tools` — Gravity's
180
181
  names, never a vendor's (`gmail.read`, not `GMAIL_FETCH_EMAILS`). The gateway
181
182
  hands them to your model with `_` for `.` (`gmail_read`), because
182
- OpenAI-family models reject dots in function names. The sandbox allows no
183
- other egress — `requests.get(...)` to anywhere times out. Declare what you
184
- need; don't reach around it.
183
+ OpenAI-family models reject dots in function names. Outbound network from the
184
+ sandbox is open today (it may be locked down later), but anything that acts on a
185
+ user's account must go through a tool: that is where the credentials are, and a
186
+ direct call would have none. Declare what you need; don't reach around it.
185
187
 
186
188
  **Models** go through the LLM gateway with the same token:
187
189
 
@@ -241,12 +243,12 @@ Windows or macOS pins the wrong wheels and only fails at deploy.
241
243
 
242
244
  `gravity run --input k=v` runs your graph in your own venv against a real
243
245
  gateway, exactly as production does: tools over `GRAVITY_GATEWAY_URL`,
244
- models over `GRAVITY_LLM_URL`, both with `GRAVITY_RUN_TOKEN`. You set a
245
- different token once, `GRAVITY_DEV_TOKEN`, in `~/.gravity/.env`: each run
246
- trades it for a run token scoped to your `permissions.tools`, so an
247
- undeclared tool is refused locally exactly as it will be when hosted. Your
248
- agent only ever sees the run token. `GRAVITY_GATEWAY_URL` defaults to
249
- `http://127.0.0.1:8000/mcp` and `GRAVITY_LLM_URL` to the same host's `/v1`.
246
+ models over `GRAVITY_LLM_URL`, both with `GRAVITY_RUN_TOKEN`. `gravity login`
247
+ gives you a gateway token; each run trades it for a run token scoped to your
248
+ `permissions.tools`, so an undeclared tool is refused locally exactly as it
249
+ will be when hosted. Your agent only ever sees the run token. The gateway is
250
+ the one Gravity names (`GET /api/cli/config`); set `GRAVITY_GATEWAY_URL` (and
251
+ `GRAVITY_DEV_TOKEN`, in `~/.gravity/.env`) only for a gateway you run yourself.
250
252
 
251
253
  `gravity dev` opens a local dashboard over the same runs: a form from your
252
254
  manifest, a live timeline, approvals, the result and a history. A run from
@@ -275,10 +277,10 @@ graph. `gravity` is preinstalled in the image; don't put it in
275
277
 
276
278
  **Tools: `await gravity.tools()`** instead of your own `MultiServerMCPClient`.
277
279
  The same LangChain tools under the same names (`gmail_read`), from the same env
278
- vars. Off the platform (`gravity run`, tests) it is exactly the client in §4,
279
- but only if your venv has it: it stays out of `requirements.txt` and `gravity run`
280
- doesn't add it, so install it yourself (`uv pip install -e <repo>/code-first/python/gravity-sdk`)
281
- or `gravity run` fails with `No module named 'gravity'`.
280
+ vars. Off the platform (`gravity run`, `gravity dev`, tests) the package is not
281
+ installed and is not on PyPI: use the client in §4 directly, as the templates do
282
+ (it is the same tools under the same names), or guard the import
283
+ (`try: import gravity except ImportError: ...`). A plain dict works for the corpus.
282
284
  On the platform, each tool in `approvals.required_for` is wrapped: calling it
283
285
  checkpoints the run and stops it until the consumer decides, with no
284
286
  connection held open. Approved, the call runs with the arguments the consumer
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gravity-cli
3
- Version: 0.5.3
3
+ Version: 0.5.4
4
4
  Summary: gravity cli — init, run, and push python agents to the Gravity marketplace
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
7
7
  License-File: NOTICE
8
8
  Requires-Python: >=3.12
9
- Requires-Dist: gravity-schema==0.5.3
9
+ Requires-Dist: gravity-schema==0.5.4
10
10
  Requires-Dist: httpx>=0.27.0
11
11
  Requires-Dist: keyring>=25.0.0
12
12
  Requires-Dist: python-dotenv>=1.0.0
@@ -108,10 +108,10 @@ The same rules apply on your laptop and when the agent runs hosted, because both
108
108
 
109
109
  | | |
110
110
  |---|---|
111
- | Commands | 16, grouped into build, publish, inspect, account and help |
111
+ | Commands | 18, grouped into build an agent, publish, your agents, account and help |
112
112
  | Starter templates | 4: `minimal`, `structured`, `deepagent`, `chat` |
113
- | Tool catalog | 1,139 tools across 8 apps (Gmail, Slack, Linear, GitHub, Calendar, Drive, Sheets, LinkedIn), 25 of them curated |
114
- | Test suite | 285 tests, run on Ubuntu and Windows with Python 3.12 and 3.13 |
113
+ | Tool catalog | 1,402 tools across 15 apps (Gmail, Slack, Linear, GitHub, Calendar, Drive, Sheets, LinkedIn, Jira, Confluence, Notion, WhatsApp, web search, and two built-ins), 28 of them curated |
114
+ | Test suite | 466 tests (214 CLI, 252 schema), run on Ubuntu and Windows with Python 3.12 and 3.13 |
115
115
  | Runtime contract | 3 environment variables, no Gravity import in your code |
116
116
 
117
117
  ## Five things to know before you start
@@ -212,7 +212,7 @@ Run `gravity help` for this list in the terminal, and add `-h` to any command fo
212
212
  | **Help** | [`gravity guide`](#gravity-guide) | Read the builder guide: how to build a good agent |
213
213
  | | [`gravity help`](#gravity-help) | Show the list of commands |
214
214
 
215
- `gravity --api-url URL <command>` saves a different control-plane address to `~/.gravity/config.json`. Wherever a command takes `[path]`, it is the agent's folder and defaults to the current one.
215
+ `gravity --api-url URL <command>` saves a different control-plane address to `~/.gravity/config.json`; it must start with `http://` or `https://`. The flag applies to the command it is given with; on later commands a `GRAVITY_API_URL` in `~/.gravity/.env` or the shell still wins over `config.json`, and the CLI says so when that is the case. `gravity --version` (or `-V`) prints the installed version. Wherever a command takes `[path]`, it is the agent's folder and defaults to the current one.
216
216
 
217
217
  ### `gravity init`
218
218
 
@@ -261,6 +261,9 @@ What it checks:
261
261
  - every imported write tool is under `approvals.required_for`
262
262
  - your code never names a write tool the manifest does not grant
263
263
  - a warning, never a failure, for read tools your code names but the manifest does not declare
264
+ - `graph` is an `async def` (or a compiled graph), not a plain `def` or a bare value; a name bound by an import counts
265
+ - nothing under `src/` reads `os.environ` or calls `asyncio.run` at module level, and no import is relative
266
+ - a schedule runs at most every 5 minutes and restricts the day of the month or the weekday, not both
264
267
 
265
268
  ```bash
266
269
  gravity validate
@@ -318,7 +321,7 @@ gravity dev [path] [--port 4747] [--no-open] [--llm-url URL]
318
321
 
319
322
  ### `gravity schedule`
320
323
 
321
- Run the agent on its `triggers.schedule`, the way the platform will, until you press Ctrl+C. Each tick is an ordinary run with the same inputs.
324
+ Run the agent on its `triggers.schedule`, the way the platform will, until you press Ctrl+C. Ticks are timed in Asia/Kolkata, as the hosted schedules are. Each tick is an ordinary run with the same inputs; a missing required input is refused before the first tick.
322
325
 
323
326
  ```
324
327
  gravity schedule [path] [--input KEY=VALUE]... [--cron EXPR] [--now]
@@ -358,7 +361,7 @@ gravity adapt <slot> <name> --tools TOOLS [--hint TEXT] [--model MODEL] [--path
358
361
  gravity adapt deliver slack --tools slack.send,slack.list_channels --hint "post to #leads"
359
362
  ```
360
363
 
361
- Requires `tests/test_<slot>_adapters.py` in the project, `GRAVITY_DEV_TOKEN` and a reachable gateway. It makes up to 3 attempts, feeding each test failure back to the model.
364
+ Requires `tests/test_<slot>_adapters.py` in the project and a gateway token (`gravity login`, or `GRAVITY_DEV_TOKEN`). It makes up to 3 attempts, feeding each test failure back to the model.
362
365
 
363
366
  ### `gravity build`
364
367
 
@@ -368,7 +371,7 @@ Validate the agent, then bundle it into `build/agent.zip` with its hashes in `bu
368
371
  gravity build [path]
369
372
  ```
370
373
 
371
- The bundle holds `manifest.yaml`, `requirements.txt`, `requirements.lock`, `src/` and `tests/`. The same source always produces the same bytes.
374
+ The bundle holds `manifest.yaml`, `requirements.txt`, `requirements.lock`, `src/`, `tests/` and `tools_needed.yaml` when present; dotfiles under `src/` and `tests/` are left out. It refuses to build without `requirements.lock` (run `gravity validate` first). The same source always produces the same bytes.
372
375
 
373
376
  ```bash
374
377
  gravity build
@@ -386,6 +389,9 @@ gravity push [path]
386
389
  - Refuses if there is no build, or if the source changed since the build. Run `gravity build` again.
387
390
  - A published version can never be replaced. Raise `version` in the manifest to publish again.
388
391
  - Press Ctrl+C to stop waiting; the build carries on, and `gravity agents` shows its status.
392
+ - The bundle may be at most 30 MB; a larger `agent.zip` is refused before upload. Remove large files from `src/` or `tests/` and build again.
393
+ - The manifest travels in a request header, so `manifest.yaml` must stay under 12 KB once encoded; a larger one is refused before upload. Keep descriptions and help text short.
394
+ - Re-pushing a version whose build failed, without changing the source, is refused with the failure reason: fix it and raise `version` in the manifest.
389
395
 
390
396
  ```bash
391
397
  gravity build && gravity push
@@ -393,7 +399,7 @@ gravity build && gravity push
393
399
 
394
400
  ### `gravity agents`
395
401
 
396
- List your published agents with their version, status and last update.
402
+ List your published agents with their version, status, review and last update. Under the table it prints what the Gravity team told you: the reviewer's notes on an approved or rejected version, and the reason if an agent was turned off. A rejected version needs a fix and a new `version:` in `manifest.yaml` before you push again. A failed build prints its reason under the table.
397
403
 
398
404
  ```
399
405
  gravity agents
@@ -452,7 +458,7 @@ gravity logout
452
458
 
453
459
  ### `gravity whoami`
454
460
 
455
- Show the account you are signed in as, and whether you hold a gateway token and until when.
461
+ Show the account you are signed in as, and whether you hold a gateway token and until when (an expired one is a warning).
456
462
 
457
463
  ```
458
464
  gravity whoami
@@ -652,7 +658,7 @@ resources:
652
658
  triggers:
653
659
  manual: true # the consumer can start a run
654
660
  schedule:
655
- cron: "0 8 * * 1-5" # weekdays at 08:00, local time
661
+ cron: "0 8 * * 1-5" # weekdays at 08:00, Asia/Kolkata
656
662
  consumer_can_change: true
657
663
 
658
664
  state:
@@ -696,7 +702,7 @@ outputs: # extra formats the platform renders and deliv
696
702
  | `approvals.required_for` | no | none | Tools that pause until a person approves. Each must also be declared under `permissions.tools` or a slot adapter. |
697
703
  | `resources.memory_mb` | no | `512` | 128 to 4096. A request; the platform may lower it. |
698
704
  | `triggers.manual` | no | `true` | Whether the consumer can start a run. Something must start the agent: `manual`, a `schedule` or a `webhook`. |
699
- | `triggers.schedule` | no | none | `cron` (5 fields, local time, such as `"0 9 * * 1"`) and `consumer_can_change` (default `true`). |
705
+ | `triggers.schedule` | no | none | `cron` (5 fields, Asia/Kolkata, such as `"0 9 * * 1"`; every 5 minutes at the fastest; day of month or weekday, not both) and `consumer_can_change` (default `true`). |
700
706
  | `triggers.webhook` | no | none | A list of events; `github.push` is the only one today. |
701
707
  | `state.max_kb` | no | no memory | Include `state:` to keep memory between runs, in `state["memory"]`. 1 to 256 KB. |
702
708
  | `slots.<name>` | no | none | A swappable part: `label`, `default` adapter, and `adapters` mapping each adapter name to the tools it may call. Code lives in `src/adapters/<slot>/<adapter>.py`. Slot names cannot repeat an input's name. |
@@ -724,6 +730,8 @@ Files the CLI writes:
724
730
  | `.gravity/payload.json` | `run` | The inputs passed to the agent |
725
731
  | `.gravity/thread.json` | `run` | The last conversation of a chat agent, for `--reply` |
726
732
  | `.gravity/memory.json` | `run` | Memory kept between runs, when the manifest declares `state` |
733
+ | `.gravity/payload.json` | `run`, `dev` | The run's inputs for the runner; deleted when the run ends (`run.json` keeps them with secret inputs redacted) |
734
+ | `.gravity/tool_schemas.json` | `adapt` | Argument schemas of every tool the slot uses, read from the gateway |
727
735
 
728
736
  The template `.gitignore` excludes `build/`, `.gravity/` and `.env`.
729
737
 
@@ -771,7 +779,12 @@ Real tools act on a connected account. Connect one with `gravity-gateway connect
771
779
  | `Can't reach the gateway at …` | No gateway at that address. Set `GRAVITY_GATEWAY_URL` to your gateway's `/mcp` address. |
772
780
  | `The gateway refused this run` | The gateway rejected the token or a declared tool. Run `gravity whoami` to check the token, or `gravity login` for a fresh one. |
773
781
  | `You are not signed in` | A control-plane command without credentials. Run `gravity login`. |
774
- | `Can't reach Gravity at …` | The control plane did not answer. Check `GRAVITY_API_URL` in `~/.gravity/.env`. |
782
+ | `Can't reach Gravity at …` | The control plane did not answer. Check `GRAVITY_API_URL` in `~/.gravity/.env`, or `api_url` in `~/.gravity/config.json` (set by `gravity --api-url`). |
783
+ | `Gravity refused GRAVITY_API_TOKEN` | The dev token in `~/.gravity/.env` is wrong or revoked. Fix it there, or remove it and run `gravity login`. |
784
+ | `… did not answer with JSON` or `Gravity answered 5xx at …` | A proxy, VPN sign-in page or outage is answering instead of Gravity. Check `GRAVITY_API_URL`; for a 5xx wait a minute and retry, then tell Gravity if it persists. |
785
+ | `Another run is live` | Runs in one folder take turns. Wait for it, or stop it; if none is live, delete `.gravity/run.lock`. |
786
+ | `Manifest is not UTF-8 text` | Notepad saved it as "Unicode" (UTF-16). Re-save it with Encoding: UTF-8. |
787
+ | `could not load the agent: No module named …` only on Windows | Windows limits paths to 260 characters and some packages in `.venv` exceed it from a deep folder. Move the agent to a shorter path or enable long paths. |
775
788
  | `Missing required input: …` | Pass each required input with `--input name=value`, or give it a `default` in the manifest. |
776
789
  | `No build to push` or `The build is stale` | Run `gravity build` again after any change to `src/`, the manifest or the requirements. |
777
790
  | `✗ requirements.lock` in `gravity validate` | A dependency has no prebuilt package for the platform image (ARM64 Linux). Pick a version that publishes one, or a different package. |
@@ -779,7 +792,8 @@ Real tools act on a connected account. Connect one with `gravity-gateway connect
779
792
 
780
793
  ## What's new
781
794
 
782
- - **0.5.3** `gravity dev` opens a local dashboard for your agent: a form from your manifest, a live timeline, approvals, the result and run history; runs from `gravity run` in the same folder show up in it live, and the other way round. WhatsApp joins the catalog (51 tools; 1,374 across 12 apps). Agents can return generated images: put them in a corpus section's `images` and they land in the Word, PowerPoint and Markdown outputs. The agent standard now shows how to generate images and embeddings through the gateway. `NEEDS_RECONNECT` on the shared test account tells you to ask Gravity, not to connect your own account.
795
+ - **0.5.4** `gravity agents` shows each agent's review: a REVIEW column (not submitted, submitted, approved, rejected), and under "From the Gravity team" the reviewer's notes and what to do next. An agent the Gravity team turned off shows as `off` with the reason. UPDATED reads "3 min ago" instead of a timestamp, and status and review are coloured. Also, from a full audit of every command: `gravity --version`. Clear messages instead of tracebacks for: a proxy or sign-in page answering instead of Gravity, a refused `GRAVITY_API_TOKEN` or `GRAVITY_DEV_TOKEN`, an expired sign-in code, a non-UTF-8 manifest, an out-of-range `--port`, a missing agent folder, a malformed session file. `gravity validate` now refuses what the build would refuse: a `graph` that is not an async factory, `os.environ` or `asyncio.run` at import, relative imports, unquoted `python: 3.12`, schedules faster than every 5 minutes or with both day fields, out-of-range cron fields; it accepts a re-exported `graph`. `gravity agents` shows the review status and failure reasons; `gravity runs` never shortens an id; `gravity logs` never wraps. `gravity push` refuses a bundle over 30 MB or a manifest over 12 KB before uploading, and says why when a failed version is re-pushed. `gravity build` ships `tools_needed.yaml`, leaves dotfiles out, and refuses to build without `requirements.lock`. `gravity adapt` leaves the folder untouched when a model call fails. `gravity schedule` checks inputs before the first tick and runs on Asia/Kolkata like the hosted schedules. `gravity run` deletes `.gravity/payload.json` after the run, warns when no `.venv` is present, and keeps a plain agent's `result`. `gravity connect <app>` refuses on the shared test account instead of attaching your account to it. The builder guide and agent standard describe the real flow (init, validate, run or dev, build, push, review on /app).
796
+ - **0.5.3** `gravity dev` opens a local dashboard for your agent: a form from your manifest, a live timeline, approvals, the result and run history; runs from `gravity run` in the same folder show up in it live, and the other way round. WhatsApp joins the catalog (51 tools; 1,402 across 15 apps including the curated and built-in ones). Agents can return generated images: put them in a corpus section's `images` and they land in the Word, PowerPoint and Markdown outputs. The agent standard now shows how to generate images and embeddings through the gateway. `NEEDS_RECONNECT` on the shared test account tells you to ask Gravity, not to connect your own account.
783
797
  - **0.5.2** Jira, Confluence and Notion join the catalog: 1,351 tools across 11 apps. `get_store()` works in local runs, kept in `.gravity/store.json`. Renewing an expired sign-in keeps your gateway token, so `gravity run` keeps working after an hour. Commands start faster: the catalog loads in a quarter of the time.
784
798
  - **0.5.0** `gravity connect` links your own accounts. `web.search` and `web.fetch` join the catalog. `gravity validate` now refuses a tool that isn't in the catalog, an imported write tool without an approval, and code that names a write tool the manifest doesn't grant. `gravity tools` reads the gateway's list and needs no sign-in. On Windows the session stays in the keychain instead of a file.
785
799
  - **0.4.4** Same code as 0.4.3, released from this repository so the GitHub release, the tag and PyPI line up again. Adds `RELEASING.md`.
@@ -91,10 +91,10 @@ The same rules apply on your laptop and when the agent runs hosted, because both
91
91
 
92
92
  | | |
93
93
  |---|---|
94
- | Commands | 16, grouped into build, publish, inspect, account and help |
94
+ | Commands | 18, grouped into build an agent, publish, your agents, account and help |
95
95
  | Starter templates | 4: `minimal`, `structured`, `deepagent`, `chat` |
96
- | Tool catalog | 1,139 tools across 8 apps (Gmail, Slack, Linear, GitHub, Calendar, Drive, Sheets, LinkedIn), 25 of them curated |
97
- | Test suite | 285 tests, run on Ubuntu and Windows with Python 3.12 and 3.13 |
96
+ | Tool catalog | 1,402 tools across 15 apps (Gmail, Slack, Linear, GitHub, Calendar, Drive, Sheets, LinkedIn, Jira, Confluence, Notion, WhatsApp, web search, and two built-ins), 28 of them curated |
97
+ | Test suite | 466 tests (214 CLI, 252 schema), run on Ubuntu and Windows with Python 3.12 and 3.13 |
98
98
  | Runtime contract | 3 environment variables, no Gravity import in your code |
99
99
 
100
100
  ## Five things to know before you start
@@ -195,7 +195,7 @@ Run `gravity help` for this list in the terminal, and add `-h` to any command fo
195
195
  | **Help** | [`gravity guide`](#gravity-guide) | Read the builder guide: how to build a good agent |
196
196
  | | [`gravity help`](#gravity-help) | Show the list of commands |
197
197
 
198
- `gravity --api-url URL <command>` saves a different control-plane address to `~/.gravity/config.json`. Wherever a command takes `[path]`, it is the agent's folder and defaults to the current one.
198
+ `gravity --api-url URL <command>` saves a different control-plane address to `~/.gravity/config.json`; it must start with `http://` or `https://`. The flag applies to the command it is given with; on later commands a `GRAVITY_API_URL` in `~/.gravity/.env` or the shell still wins over `config.json`, and the CLI says so when that is the case. `gravity --version` (or `-V`) prints the installed version. Wherever a command takes `[path]`, it is the agent's folder and defaults to the current one.
199
199
 
200
200
  ### `gravity init`
201
201
 
@@ -244,6 +244,9 @@ What it checks:
244
244
  - every imported write tool is under `approvals.required_for`
245
245
  - your code never names a write tool the manifest does not grant
246
246
  - a warning, never a failure, for read tools your code names but the manifest does not declare
247
+ - `graph` is an `async def` (or a compiled graph), not a plain `def` or a bare value; a name bound by an import counts
248
+ - nothing under `src/` reads `os.environ` or calls `asyncio.run` at module level, and no import is relative
249
+ - a schedule runs at most every 5 minutes and restricts the day of the month or the weekday, not both
247
250
 
248
251
  ```bash
249
252
  gravity validate
@@ -301,7 +304,7 @@ gravity dev [path] [--port 4747] [--no-open] [--llm-url URL]
301
304
 
302
305
  ### `gravity schedule`
303
306
 
304
- Run the agent on its `triggers.schedule`, the way the platform will, until you press Ctrl+C. Each tick is an ordinary run with the same inputs.
307
+ Run the agent on its `triggers.schedule`, the way the platform will, until you press Ctrl+C. Ticks are timed in Asia/Kolkata, as the hosted schedules are. Each tick is an ordinary run with the same inputs; a missing required input is refused before the first tick.
305
308
 
306
309
  ```
307
310
  gravity schedule [path] [--input KEY=VALUE]... [--cron EXPR] [--now]
@@ -341,7 +344,7 @@ gravity adapt <slot> <name> --tools TOOLS [--hint TEXT] [--model MODEL] [--path
341
344
  gravity adapt deliver slack --tools slack.send,slack.list_channels --hint "post to #leads"
342
345
  ```
343
346
 
344
- Requires `tests/test_<slot>_adapters.py` in the project, `GRAVITY_DEV_TOKEN` and a reachable gateway. It makes up to 3 attempts, feeding each test failure back to the model.
347
+ Requires `tests/test_<slot>_adapters.py` in the project and a gateway token (`gravity login`, or `GRAVITY_DEV_TOKEN`). It makes up to 3 attempts, feeding each test failure back to the model.
345
348
 
346
349
  ### `gravity build`
347
350
 
@@ -351,7 +354,7 @@ Validate the agent, then bundle it into `build/agent.zip` with its hashes in `bu
351
354
  gravity build [path]
352
355
  ```
353
356
 
354
- The bundle holds `manifest.yaml`, `requirements.txt`, `requirements.lock`, `src/` and `tests/`. The same source always produces the same bytes.
357
+ The bundle holds `manifest.yaml`, `requirements.txt`, `requirements.lock`, `src/`, `tests/` and `tools_needed.yaml` when present; dotfiles under `src/` and `tests/` are left out. It refuses to build without `requirements.lock` (run `gravity validate` first). The same source always produces the same bytes.
355
358
 
356
359
  ```bash
357
360
  gravity build
@@ -369,6 +372,9 @@ gravity push [path]
369
372
  - Refuses if there is no build, or if the source changed since the build. Run `gravity build` again.
370
373
  - A published version can never be replaced. Raise `version` in the manifest to publish again.
371
374
  - Press Ctrl+C to stop waiting; the build carries on, and `gravity agents` shows its status.
375
+ - The bundle may be at most 30 MB; a larger `agent.zip` is refused before upload. Remove large files from `src/` or `tests/` and build again.
376
+ - The manifest travels in a request header, so `manifest.yaml` must stay under 12 KB once encoded; a larger one is refused before upload. Keep descriptions and help text short.
377
+ - Re-pushing a version whose build failed, without changing the source, is refused with the failure reason: fix it and raise `version` in the manifest.
372
378
 
373
379
  ```bash
374
380
  gravity build && gravity push
@@ -376,7 +382,7 @@ gravity build && gravity push
376
382
 
377
383
  ### `gravity agents`
378
384
 
379
- List your published agents with their version, status and last update.
385
+ List your published agents with their version, status, review and last update. Under the table it prints what the Gravity team told you: the reviewer's notes on an approved or rejected version, and the reason if an agent was turned off. A rejected version needs a fix and a new `version:` in `manifest.yaml` before you push again. A failed build prints its reason under the table.
380
386
 
381
387
  ```
382
388
  gravity agents
@@ -435,7 +441,7 @@ gravity logout
435
441
 
436
442
  ### `gravity whoami`
437
443
 
438
- Show the account you are signed in as, and whether you hold a gateway token and until when.
444
+ Show the account you are signed in as, and whether you hold a gateway token and until when (an expired one is a warning).
439
445
 
440
446
  ```
441
447
  gravity whoami
@@ -635,7 +641,7 @@ resources:
635
641
  triggers:
636
642
  manual: true # the consumer can start a run
637
643
  schedule:
638
- cron: "0 8 * * 1-5" # weekdays at 08:00, local time
644
+ cron: "0 8 * * 1-5" # weekdays at 08:00, Asia/Kolkata
639
645
  consumer_can_change: true
640
646
 
641
647
  state:
@@ -679,7 +685,7 @@ outputs: # extra formats the platform renders and deliv
679
685
  | `approvals.required_for` | no | none | Tools that pause until a person approves. Each must also be declared under `permissions.tools` or a slot adapter. |
680
686
  | `resources.memory_mb` | no | `512` | 128 to 4096. A request; the platform may lower it. |
681
687
  | `triggers.manual` | no | `true` | Whether the consumer can start a run. Something must start the agent: `manual`, a `schedule` or a `webhook`. |
682
- | `triggers.schedule` | no | none | `cron` (5 fields, local time, such as `"0 9 * * 1"`) and `consumer_can_change` (default `true`). |
688
+ | `triggers.schedule` | no | none | `cron` (5 fields, Asia/Kolkata, such as `"0 9 * * 1"`; every 5 minutes at the fastest; day of month or weekday, not both) and `consumer_can_change` (default `true`). |
683
689
  | `triggers.webhook` | no | none | A list of events; `github.push` is the only one today. |
684
690
  | `state.max_kb` | no | no memory | Include `state:` to keep memory between runs, in `state["memory"]`. 1 to 256 KB. |
685
691
  | `slots.<name>` | no | none | A swappable part: `label`, `default` adapter, and `adapters` mapping each adapter name to the tools it may call. Code lives in `src/adapters/<slot>/<adapter>.py`. Slot names cannot repeat an input's name. |
@@ -707,6 +713,8 @@ Files the CLI writes:
707
713
  | `.gravity/payload.json` | `run` | The inputs passed to the agent |
708
714
  | `.gravity/thread.json` | `run` | The last conversation of a chat agent, for `--reply` |
709
715
  | `.gravity/memory.json` | `run` | Memory kept between runs, when the manifest declares `state` |
716
+ | `.gravity/payload.json` | `run`, `dev` | The run's inputs for the runner; deleted when the run ends (`run.json` keeps them with secret inputs redacted) |
717
+ | `.gravity/tool_schemas.json` | `adapt` | Argument schemas of every tool the slot uses, read from the gateway |
710
718
 
711
719
  The template `.gitignore` excludes `build/`, `.gravity/` and `.env`.
712
720
 
@@ -754,7 +762,12 @@ Real tools act on a connected account. Connect one with `gravity-gateway connect
754
762
  | `Can't reach the gateway at …` | No gateway at that address. Set `GRAVITY_GATEWAY_URL` to your gateway's `/mcp` address. |
755
763
  | `The gateway refused this run` | The gateway rejected the token or a declared tool. Run `gravity whoami` to check the token, or `gravity login` for a fresh one. |
756
764
  | `You are not signed in` | A control-plane command without credentials. Run `gravity login`. |
757
- | `Can't reach Gravity at …` | The control plane did not answer. Check `GRAVITY_API_URL` in `~/.gravity/.env`. |
765
+ | `Can't reach Gravity at …` | The control plane did not answer. Check `GRAVITY_API_URL` in `~/.gravity/.env`, or `api_url` in `~/.gravity/config.json` (set by `gravity --api-url`). |
766
+ | `Gravity refused GRAVITY_API_TOKEN` | The dev token in `~/.gravity/.env` is wrong or revoked. Fix it there, or remove it and run `gravity login`. |
767
+ | `… did not answer with JSON` or `Gravity answered 5xx at …` | A proxy, VPN sign-in page or outage is answering instead of Gravity. Check `GRAVITY_API_URL`; for a 5xx wait a minute and retry, then tell Gravity if it persists. |
768
+ | `Another run is live` | Runs in one folder take turns. Wait for it, or stop it; if none is live, delete `.gravity/run.lock`. |
769
+ | `Manifest is not UTF-8 text` | Notepad saved it as "Unicode" (UTF-16). Re-save it with Encoding: UTF-8. |
770
+ | `could not load the agent: No module named …` only on Windows | Windows limits paths to 260 characters and some packages in `.venv` exceed it from a deep folder. Move the agent to a shorter path or enable long paths. |
758
771
  | `Missing required input: …` | Pass each required input with `--input name=value`, or give it a `default` in the manifest. |
759
772
  | `No build to push` or `The build is stale` | Run `gravity build` again after any change to `src/`, the manifest or the requirements. |
760
773
  | `✗ requirements.lock` in `gravity validate` | A dependency has no prebuilt package for the platform image (ARM64 Linux). Pick a version that publishes one, or a different package. |
@@ -762,7 +775,8 @@ Real tools act on a connected account. Connect one with `gravity-gateway connect
762
775
 
763
776
  ## What's new
764
777
 
765
- - **0.5.3** `gravity dev` opens a local dashboard for your agent: a form from your manifest, a live timeline, approvals, the result and run history; runs from `gravity run` in the same folder show up in it live, and the other way round. WhatsApp joins the catalog (51 tools; 1,374 across 12 apps). Agents can return generated images: put them in a corpus section's `images` and they land in the Word, PowerPoint and Markdown outputs. The agent standard now shows how to generate images and embeddings through the gateway. `NEEDS_RECONNECT` on the shared test account tells you to ask Gravity, not to connect your own account.
778
+ - **0.5.4** `gravity agents` shows each agent's review: a REVIEW column (not submitted, submitted, approved, rejected), and under "From the Gravity team" the reviewer's notes and what to do next. An agent the Gravity team turned off shows as `off` with the reason. UPDATED reads "3 min ago" instead of a timestamp, and status and review are coloured. Also, from a full audit of every command: `gravity --version`. Clear messages instead of tracebacks for: a proxy or sign-in page answering instead of Gravity, a refused `GRAVITY_API_TOKEN` or `GRAVITY_DEV_TOKEN`, an expired sign-in code, a non-UTF-8 manifest, an out-of-range `--port`, a missing agent folder, a malformed session file. `gravity validate` now refuses what the build would refuse: a `graph` that is not an async factory, `os.environ` or `asyncio.run` at import, relative imports, unquoted `python: 3.12`, schedules faster than every 5 minutes or with both day fields, out-of-range cron fields; it accepts a re-exported `graph`. `gravity agents` shows the review status and failure reasons; `gravity runs` never shortens an id; `gravity logs` never wraps. `gravity push` refuses a bundle over 30 MB or a manifest over 12 KB before uploading, and says why when a failed version is re-pushed. `gravity build` ships `tools_needed.yaml`, leaves dotfiles out, and refuses to build without `requirements.lock`. `gravity adapt` leaves the folder untouched when a model call fails. `gravity schedule` checks inputs before the first tick and runs on Asia/Kolkata like the hosted schedules. `gravity run` deletes `.gravity/payload.json` after the run, warns when no `.venv` is present, and keeps a plain agent's `result`. `gravity connect <app>` refuses on the shared test account instead of attaching your account to it. The builder guide and agent standard describe the real flow (init, validate, run or dev, build, push, review on /app).
779
+ - **0.5.3** `gravity dev` opens a local dashboard for your agent: a form from your manifest, a live timeline, approvals, the result and run history; runs from `gravity run` in the same folder show up in it live, and the other way round. WhatsApp joins the catalog (51 tools; 1,402 across 15 apps including the curated and built-in ones). Agents can return generated images: put them in a corpus section's `images` and they land in the Word, PowerPoint and Markdown outputs. The agent standard now shows how to generate images and embeddings through the gateway. `NEEDS_RECONNECT` on the shared test account tells you to ask Gravity, not to connect your own account.
766
780
  - **0.5.2** Jira, Confluence and Notion join the catalog: 1,351 tools across 11 apps. `get_store()` works in local runs, kept in `.gravity/store.json`. Renewing an expired sign-in keeps your gateway token, so `gravity run` keeps working after an hour. Commands start faster: the catalog loads in a quarter of the time.
767
781
  - **0.5.0** `gravity connect` links your own accounts. `web.search` and `web.fetch` join the catalog. `gravity validate` now refuses a tool that isn't in the catalog, an imported write tool without an approval, and code that names a write tool the manifest doesn't grant. `gravity tools` reads the gateway's list and needs no sign-in. On Windows the session stays in the keychain instead of a file.
768
782
  - **0.4.4** Same code as 0.4.3, released from this repository so the GitHub release, the tag and PyPI line up again. Adds `RELEASING.md`.
@@ -1,11 +1,16 @@
1
- """Standard 5-field cron, local time. Day-of-week 0 or 7 is Sunday; when both day
1
+ """Standard 5-field cron, in whatever timezone the `after` datetime carries (gravity
2
+ schedule passes Asia/Kolkata, like the hosted schedules). Day-of-week 0 or 7 is Sunday; when both day
2
3
  fields are restricted, either matching is enough (as in cron itself)."""
3
4
 
4
5
  from __future__ import annotations
5
6
 
7
+ import math
6
8
  from datetime import datetime, timedelta
9
+ from itertools import pairwise
7
10
 
8
11
  _BOUNDS = ((0, 59), (0, 23), (1, 31), (1, 12), (0, 7))
12
+ # Every tick is a paid run; the control plane's MIN_SCHEDULE_MINUTES.
13
+ MIN_GAP_MINUTES = 5
9
14
 
10
15
 
11
16
  def _values(field: str, low: int, high: int) -> set[int]:
@@ -22,6 +27,26 @@ def _values(field: str, low: int, high: int) -> set[int]:
22
27
  return out
23
28
 
24
29
 
30
+ def field_values(expr: str) -> list[set[int]]:
31
+ """Each field's values. ValueError when a field is malformed, empty or out of its range."""
32
+ out = [_values(f, lo, hi) for f, (lo, hi) in zip(expr.split(), _BOUNDS)]
33
+ for vals, (lo, hi) in zip(out, _BOUNDS):
34
+ if not vals or min(vals) < lo or max(vals) > hi:
35
+ raise ValueError(f"'{expr}' has a value out of range")
36
+ return out
37
+
38
+
39
+ def min_gap(expr: str) -> float:
40
+ """Minutes between the closest two fires, from the minute and hour fields alone (the day
41
+ fields can only widen it). Same arithmetic as the control plane's checkScheduleFloor."""
42
+ minutes, hours = field_values(expr)[:2]
43
+ mins = sorted(minutes)
44
+ gap = min((b - a for a, b in pairwise(mins)), default=math.inf)
45
+ if any((h + 1) % 24 in hours for h in hours):
46
+ gap = min(gap, 60 - mins[-1] + mins[0])
47
+ return gap
48
+
49
+
25
50
  def matches(expr: str, at: datetime) -> bool:
26
51
  fields = expr.split()
27
52
  minute, hour, dom, month, dow = (_values(f, *b) for f, b in zip(fields, _BOUNDS))
@@ -9,6 +9,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_valida
9
9
  from pydantic_core import PydanticCustomError
10
10
 
11
11
  from . import contract as C
12
+ from . import cron
12
13
  from .errors import ErrorCode
13
14
 
14
15
  # Unknown fields are rejected, so a newer-contract field is reported, not dropped.
@@ -99,6 +100,18 @@ class RuntimeSpec(BaseModel):
99
100
  )
100
101
  return v
101
102
 
103
+ @field_validator("python", mode="before")
104
+ @classmethod
105
+ def _check_python_is_text(cls, v: Any) -> Any:
106
+ # YAML reads an unquoted 3.12 as a float (and 3.10 as 3.1).
107
+ if isinstance(v, (int, float)):
108
+ raise PydanticCustomError(
109
+ ErrorCode.INVALID_PYTHON_VERSION.value,
110
+ "Python version {value} is not quoted text.",
111
+ {"value": v, "remediation": 'Put it in quotes: python: "3.12".'},
112
+ )
113
+ return v
114
+
102
115
  @field_validator("python")
103
116
  @classmethod
104
117
  def _check_python(cls, v: str) -> str:
@@ -375,6 +388,37 @@ class ScheduleSpec(BaseModel):
375
388
  ),
376
389
  },
377
390
  )
391
+ try:
392
+ cron.field_values(v)
393
+ except ValueError:
394
+ raise PydanticCustomError(
395
+ ErrorCode.INVALID_TRIGGER.value,
396
+ "Schedule '{value}' has a value out of range.",
397
+ {
398
+ "value": v,
399
+ "remediation": (
400
+ "Minutes are 0-59, hours 0-23, days 1-31, months 1-12 and "
401
+ "weekdays 0-7 (0 and 7 are Sunday)."
402
+ ),
403
+ },
404
+ ) from None
405
+ # The platform's scheduler cannot express cron's "either day matches".
406
+ if fields[2] != "*" and fields[4] != "*":
407
+ raise PydanticCustomError(
408
+ ErrorCode.INVALID_TRIGGER.value,
409
+ "Schedule '{value}' restricts both the day of the month and the weekday.",
410
+ {"value": v, "remediation": "Set one of them to *."},
411
+ )
412
+ if cron.min_gap(v) < cron.MIN_GAP_MINUTES:
413
+ raise PydanticCustomError(
414
+ ErrorCode.INVALID_TRIGGER.value,
415
+ "Schedules can run at most every {limit} minutes.",
416
+ {
417
+ "value": v,
418
+ "limit": cron.MIN_GAP_MINUTES,
419
+ "remediation": f"Use '*/{cron.MIN_GAP_MINUTES} * * * *' or something slower.",
420
+ },
421
+ )
378
422
  return v
379
423
 
380
424
 
@@ -527,6 +571,18 @@ class Manifest(BaseModel):
527
571
  )
528
572
  return v
529
573
 
574
+ @field_validator("version", mode="before")
575
+ @classmethod
576
+ def _check_version_is_text(cls, v: Any) -> Any:
577
+ # YAML reads an unquoted 1.0 as a float.
578
+ if isinstance(v, (int, float)):
579
+ raise PydanticCustomError(
580
+ ErrorCode.INVALID_AGENT_VERSION.value,
581
+ "Agent version {value} is not quoted text.",
582
+ {"value": v, "remediation": 'Put it in quotes: version: "1.0.0".'},
583
+ )
584
+ return v
585
+
530
586
  @field_validator("version")
531
587
  @classmethod
532
588
  def _check_version(cls, v: str) -> str:
@@ -156,7 +156,17 @@ def load_manifest_file(
156
156
  "Run `gravity init` to create one, or check the path.",
157
157
  str(p),
158
158
  )
159
- return load_manifest_text(p.read_text(encoding="utf-8"), catalog=catalog)
159
+ try:
160
+ text = p.read_text(encoding="utf-8")
161
+ except UnicodeDecodeError:
162
+ # Notepad's "Unicode" is UTF-16; a BOM-only UTF-8 file decodes fine and YAML skips the BOM.
163
+ raise _fail(
164
+ ErrorCode.MALFORMED_YAML,
165
+ "Manifest is not UTF-8 text.",
166
+ "Save the file as UTF-8 (in Notepad: Encoding, then UTF-8).",
167
+ str(p),
168
+ ) from None
169
+ return load_manifest_text(text, catalog=catalog)
160
170
 
161
171
 
162
172
  def _fail(code: ErrorCode, message: str, remediation: str, location: str = "") -> ManifestValidationError:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gravity-schema"
3
- version = "0.5.3"
3
+ version = "0.5.4"
4
4
  description = "Gravity agent manifest contract — models, validation, and JSON Schema export"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -1,7 +1,7 @@
1
1
  from datetime import datetime
2
2
 
3
3
  import pytest
4
- from gravity_schema.cron import matches, next_run
4
+ from gravity_schema.cron import field_values, matches, min_gap, next_run
5
5
 
6
6
  MON_0900 = datetime(2026, 9, 28, 9, 0) # a Monday
7
7
 
@@ -29,3 +29,18 @@ def test_next_run_is_strictly_after():
29
29
  assert next_run("0 9 * * *", MON_0900) == datetime(2026, 9, 29, 9, 0)
30
30
  with pytest.raises(ValueError):
31
31
  next_run("0 0 31 2 *", MON_0900) # 31 February
32
+
33
+
34
+ def test_field_values_checks_every_range():
35
+ assert field_values("0 9 1 * 7")[4] == {7}
36
+ for bad in ("60 * * * *", "0 24 * * *", "0 0 0 * *", "0 0 32 * *", "0 0 * 13 *", "0 0 * * 8", "5-3 * * * *", "*/0 * * * *"):
37
+ with pytest.raises(ValueError):
38
+ field_values(bad)
39
+
40
+
41
+ def test_min_gap_is_the_closest_two_fires():
42
+ assert min_gap("*/5 * * * *") == 5
43
+ assert min_gap("0,3 * * * *") == 3
44
+ assert min_gap("0,59 * * * *") == 1 # 10:59 then 11:00
45
+ assert min_gap("59 9 * * *") == float("inf") # once a day; the day fields don't matter
46
+ assert min_gap("58 9,10 * * *") == 60