bathos 0.3.0__tar.gz → 0.4.0__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 (102) hide show
  1. {bathos-0.3.0 → bathos-0.4.0}/CHANGELOG.md +10 -0
  2. {bathos-0.3.0 → bathos-0.4.0}/CLAUDE.md +43 -26
  3. {bathos-0.3.0 → bathos-0.4.0}/PKG-INFO +49 -3
  4. {bathos-0.3.0 → bathos-0.4.0}/README.md +48 -2
  5. {bathos-0.3.0 → bathos-0.4.0}/agent_assets/docs/claude-code-integration.md +7 -3
  6. {bathos-0.3.0 → bathos-0.4.0}/agent_assets/using_bathos/SKILL.md +2 -2
  7. {bathos-0.3.0 → bathos-0.4.0}/pyproject.toml +1 -1
  8. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/__init__.py +1 -1
  9. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/catalog.py +2 -2
  10. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/cli.py +24 -2
  11. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/compact.py +1 -1
  12. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/config.py +2 -0
  13. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/mcp.py +1 -0
  14. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/migrate.py +56 -1
  15. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/schema.py +3 -0
  16. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/sync.py +41 -6
  17. {bathos-0.3.0 → bathos-0.4.0}/tests/test_catalog.py +1 -1
  18. {bathos-0.3.0 → bathos-0.4.0}/tests/test_cli.py +2 -2
  19. {bathos-0.3.0 → bathos-0.4.0}/tests/test_cluster_workflow.py +3 -3
  20. {bathos-0.3.0 → bathos-0.4.0}/tests/test_integration.py +2 -2
  21. {bathos-0.3.0 → bathos-0.4.0}/tests/test_query.py +1 -1
  22. {bathos-0.3.0 → bathos-0.4.0}/tests/test_remote.py +1 -1
  23. {bathos-0.3.0 → bathos-0.4.0}/tests/test_sync.py +47 -2
  24. bathos-0.4.0/tests/test_sync_project_filter.py +212 -0
  25. {bathos-0.3.0 → bathos-0.4.0}/uv.lock +1 -1
  26. {bathos-0.3.0 → bathos-0.4.0}/.agent/workspace.id +0 -0
  27. {bathos-0.3.0 → bathos-0.4.0}/.bth.toml +0 -0
  28. {bathos-0.3.0 → bathos-0.4.0}/.coverage +0 -0
  29. {bathos-0.3.0 → bathos-0.4.0}/.github/SECRETS.md +0 -0
  30. {bathos-0.3.0 → bathos-0.4.0}/.github/workflows/publish.yml +0 -0
  31. {bathos-0.3.0 → bathos-0.4.0}/.github/workflows/test.yml +0 -0
  32. {bathos-0.3.0 → bathos-0.4.0}/.gitignore +0 -0
  33. {bathos-0.3.0 → bathos-0.4.0}/.python-version +0 -0
  34. {bathos-0.3.0 → bathos-0.4.0}/.readthedocs.yaml +0 -0
  35. {bathos-0.3.0 → bathos-0.4.0}/LICENSE +0 -0
  36. {bathos-0.3.0 → bathos-0.4.0}/agent_assets/docs/gemini-cli-integration.md +0 -0
  37. {bathos-0.3.0 → bathos-0.4.0}/docs/Makefile +0 -0
  38. {bathos-0.3.0 → bathos-0.4.0}/docs/source/api.rst +0 -0
  39. {bathos-0.3.0 → bathos-0.4.0}/docs/source/architecture.rst +0 -0
  40. {bathos-0.3.0 → bathos-0.4.0}/docs/source/conf.py +0 -0
  41. {bathos-0.3.0 → bathos-0.4.0}/docs/source/contributing.rst +0 -0
  42. {bathos-0.3.0 → bathos-0.4.0}/docs/source/index.rst +0 -0
  43. {bathos-0.3.0 → bathos-0.4.0}/docs/source/slurm-integration.rst +0 -0
  44. {bathos-0.3.0 → bathos-0.4.0}/docs/source/user-guide.rst +0 -0
  45. {bathos-0.3.0 → bathos-0.4.0}/docs/superpowers/plans/2026-05-15-bathos-v01-core.md +0 -0
  46. {bathos-0.3.0 → bathos-0.4.0}/docs/superpowers/plans/2026-05-19-agent-onboarding-export.md +0 -0
  47. {bathos-0.3.0 → bathos-0.4.0}/docs/superpowers/plans/2026-05-19-decorator.md +0 -0
  48. {bathos-0.3.0 → bathos-0.4.0}/docs/superpowers/plans/2026-05-19-historical-migration.md +0 -0
  49. {bathos-0.3.0 → bathos-0.4.0}/docs/superpowers/plans/2026-05-19-linter.md +0 -0
  50. {bathos-0.3.0 → bathos-0.4.0}/docs/superpowers/plans/2026-05-19-remote-management.md +0 -0
  51. {bathos-0.3.0 → bathos-0.4.0}/docs/superpowers/plans/2026-05-19-schema-versioning.md +0 -0
  52. {bathos-0.3.0 → bathos-0.4.0}/docs/superpowers/plans/2026-05-19-skill-and-export-mcp.md +0 -0
  53. {bathos-0.3.0 → bathos-0.4.0}/docs/superpowers/plans/2026-05-19-v02-feature-wiring.md +0 -0
  54. {bathos-0.3.0 → bathos-0.4.0}/docs/superpowers/plans/2026-05-20-agentic-integrity-design.md +0 -0
  55. {bathos-0.3.0 → bathos-0.4.0}/docs/superpowers/plans/2026-05-20-agentic-integrity-impl-plan.md +0 -0
  56. {bathos-0.3.0 → bathos-0.4.0}/docs/superpowers/specs/2026-05-15-bathos-design.md +0 -0
  57. {bathos-0.3.0 → bathos-0.4.0}/inventory.txt +0 -0
  58. {bathos-0.3.0 → bathos-0.4.0}/main.py +0 -0
  59. {bathos-0.3.0 → bathos-0.4.0}/praxia.toml +0 -0
  60. {bathos-0.3.0 → bathos-0.4.0}/scripts/benchmarks/.bth.toml +0 -0
  61. {bathos-0.3.0 → bathos-0.4.0}/scripts/experiments/.bth.toml +0 -0
  62. {bathos-0.3.0 → bathos-0.4.0}/scripts/slurm/_bth_env.sh +0 -0
  63. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/archive.py +0 -0
  64. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/campaigns.py +0 -0
  65. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/checker.py +0 -0
  66. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/decorators.py +0 -0
  67. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/export.py +0 -0
  68. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/git.py +0 -0
  69. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/init.py +0 -0
  70. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/linter.py +0 -0
  71. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/new_experiment.py +0 -0
  72. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/prereg.py +0 -0
  73. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/query.py +0 -0
  74. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/remote.py +0 -0
  75. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/runner.py +0 -0
  76. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/sidecar.py +0 -0
  77. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/sprint_audit.py +0 -0
  78. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/templates/_bth_env.sh +0 -0
  79. {bathos-0.3.0 → bathos-0.4.0}/src/bathos/validate.py +0 -0
  80. {bathos-0.3.0 → bathos-0.4.0}/tests/__init__.py +0 -0
  81. {bathos-0.3.0 → bathos-0.4.0}/tests/conftest.py +0 -0
  82. {bathos-0.3.0 → bathos-0.4.0}/tests/test_archive.py +0 -0
  83. {bathos-0.3.0 → bathos-0.4.0}/tests/test_campaigns.py +0 -0
  84. {bathos-0.3.0 → bathos-0.4.0}/tests/test_checker.py +0 -0
  85. {bathos-0.3.0 → bathos-0.4.0}/tests/test_compact.py +0 -0
  86. {bathos-0.3.0 → bathos-0.4.0}/tests/test_config.py +0 -0
  87. {bathos-0.3.0 → bathos-0.4.0}/tests/test_decorators.py +0 -0
  88. {bathos-0.3.0 → bathos-0.4.0}/tests/test_export.py +0 -0
  89. {bathos-0.3.0 → bathos-0.4.0}/tests/test_git.py +0 -0
  90. {bathos-0.3.0 → bathos-0.4.0}/tests/test_init.py +0 -0
  91. {bathos-0.3.0 → bathos-0.4.0}/tests/test_linter.py +0 -0
  92. {bathos-0.3.0 → bathos-0.4.0}/tests/test_mcp_integration.py +0 -0
  93. {bathos-0.3.0 → bathos-0.4.0}/tests/test_mcp_server.py +0 -0
  94. {bathos-0.3.0 → bathos-0.4.0}/tests/test_migrate.py +0 -0
  95. {bathos-0.3.0 → bathos-0.4.0}/tests/test_new_experiment.py +0 -0
  96. {bathos-0.3.0 → bathos-0.4.0}/tests/test_prereg.py +0 -0
  97. {bathos-0.3.0 → bathos-0.4.0}/tests/test_runner.py +0 -0
  98. {bathos-0.3.0 → bathos-0.4.0}/tests/test_schema.py +0 -0
  99. {bathos-0.3.0 → bathos-0.4.0}/tests/test_sidecar.py +0 -0
  100. {bathos-0.3.0 → bathos-0.4.0}/tests/test_slurm_integration.py +0 -0
  101. {bathos-0.3.0 → bathos-0.4.0}/tests/test_sprint_audit.py +0 -0
  102. {bathos-0.3.0 → bathos-0.4.0}/tests/test_validate.py +0 -0
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ### Fixed
11
+
12
+ - **`bth sync` indefinite hang** — `sync_catalog()` now passes SSH options (`ConnectTimeout=10`, `BatchMode=yes`) to rsync so unreachable hosts fail in ≤10 s instead of blocking forever; adds `subprocess.run(timeout=120)` as a safety net; raises `RuntimeError` with actionable message on timeout; drops silent `--progress` flag (output was captured, not displayed)
13
+
14
+ - **Parquet timestamp timezone-aware fix** — Ensure timestamps loaded from Parquet are timezone-aware (UTC) to avoid comparisons between naive and aware datetimes in query functions; prevents TypeError and incorrect filtering.
15
+
16
+ ---
17
+
8
18
  ## [0.3.0] - 2026-05-20
9
19
 
10
20
  ### Added
@@ -10,15 +10,21 @@ bathos is a standalone experiment tracking CLI for a single researcher across 10
10
10
 
11
11
  ---
12
12
 
13
- ## Current Status (as of 2026-05-15)
13
+ ## Current Status (as of 2026-05-21)
14
14
 
15
- **v0.1: complete and merged to main.** 44 tests passing.
15
+ **v0.4.0: complete and merged to main.** 333 tests passing.
16
16
 
17
17
  - Full design spec: `.praxia/specs/bathos-design.md`
18
18
  - v0.1 implementation plan: `.praxia/specs/bathos-v01-plan.md`
19
- - Backlog: items #124–139 in praxia DB (see below); #124–127 done
19
+ - Backlog: items #124–142 in praxia DB (see below); #124–141 done
20
20
 
21
- **Next sprint candidates (P2):** #128 (FastMCP), #130 (bth check) + #128 unblocked once 130 done, #138 (bth sync), #139 (bth compact / warm tier), #131 (SLURM integration), #132 (bth new-experiment).
21
+ **Shipped in v0.2:** FastMCP server (#128), `@bth.experiment` decorator (#129), `bth check` (#130), SLURM `_bth_env.sh` (#131), `bth new-experiment` (#132), `uv tool` packaging (#133), `bth lint` (#134), `bth migrate` (#135), `bth sync` (#138), schema versioning v2 (#140), `bth archive` (#141), `bth remote` subcommands, `bth export`, `bth catalog-version`.
22
+
23
+ **Shipped in v0.3:** Agentic integrity gate (`--agent-mode`, `--no-sidecar`, `--derived-from`), lineage tracking (`bth lineage`), campaigns (`bth campaign` subcommands + MCP tools), sprint audit (`bth sprint-audit`), Tier-2 lint checks, schema v3 with campaign/lineage/integrity fields, `--campaign` flag on `bth run`.
24
+
25
+ **Shipped in v0.4:** Per-project sync filtering — `bth sync` now pushes/pulls only the current project's runs (no cross-project contamination). Cool-tier layout changed to `runs/<slug>/run_<uuid>.parquet`. New command `bth migrate-to-project-subdirs` migrates flat catalogs. `sync_filter` config knob; `--no-project-filter` opt-out planned.
26
+
27
+ **Remaining backlog:** #136 (bth-migrate praxia workflow), #137 (global instruction portability), #142 (results management design).
22
28
 
23
29
  ---
24
30
 
@@ -30,7 +36,7 @@ bathos is a standalone experiment tracking CLI for a single researcher across 10
30
36
  SLURM job / bth run
31
37
  │
32
38
  ▼ atomic write-then-rename
33
- cool/ ~/.bth/catalog/runs/run_<uuid>.parquet ← minimal schema, SLURM-safe
39
+ cool/ ~/.bth/catalog/runs/<slug>/run_<uuid>.parquet ← minimal schema, SLURM-safe
34
40
  │
35
41
  bth compact (lazy on query or explicit)
36
42
  │
@@ -52,7 +58,7 @@ Cool schema is intentionally minimal (13 provenance fields). Warm adds `metadata
52
58
 
53
59
  ### Pre-Registration Schemas (per script directory)
54
60
 
55
- Every script in `scripts/experiments/` and `scripts/benchmarks/` must have a companion sidecar **`<script-stem>.bth.toml`** declaring its hypothesis, expected outcomes, and result schema before `bth run` will execute it.
61
+ Every script in `scripts/experiments/` and `scripts/benchmarks/` should have a companion sidecar **`<script-stem>.bth.toml`** declaring its hypothesis, expected outcomes, and result schema. In v0.2+, `bth run` enforces this by default; pass `--no-sidecar` to bypass.
56
62
 
57
63
  **Sidecar format (experiments):**
58
64
  ```toml
@@ -118,7 +124,7 @@ fix = "str"
118
124
  | Catalog storage | **DuckDB + Parquet** (not pyiceberg) | pyiceberg is for distributed data lakes; DuckDB+Parquet is simpler, no overhead |
119
125
  | FastMCP | Mirror CLI tool-for-tool | `cli.py` and `mcp.py` both thin layers over same core modules |
120
126
 
121
- ### Source Layout (planned)
127
+ ### Source Layout
122
128
 
123
129
  ```
124
130
  src/bathos/
@@ -133,8 +139,20 @@ src/bathos/
133
139
  init.py # bth init logic
134
140
  runner.py # bth run subprocess wrapper
135
141
  query.py # list_runs, get_run, find_runs, run_sql
136
- checker.py # bth check — git-drift validity (v0.2)
137
- sidecar.py # .bth.toml sidecar parse, content-hash, outcome eval (v0.2)
142
+ checker.py # bth check — git-drift freshness gate
143
+ sidecar.py # .bth.toml sidecar parse, content-hash, outcome eval
144
+ campaigns.py # bth campaign subcommands + Campaign dataclass
145
+ sprint_audit.py # bth sprint-audit cross-project audit
146
+ linter.py # bth lint — Tier-1 and Tier-2 checks
147
+ migrate.py # bth migrate — schema version upgrades
148
+ new_experiment.py # bth new-experiment scaffold
149
+ export.py # bth export — skill + MCP registration
150
+ validate.py # sidecar structural validation
151
+ remote.py # bth remote subcommands
152
+ sync.py # bth sync rsync wrapper
153
+ archive.py # bth archive warm→cold export
154
+ prereg.py # agentic integrity pre-registration gate
155
+ decorators.py # @bth.experiment provenance decorator
138
156
  templates/
139
157
  _bth_env.sh # SLURM env helper template
140
158
  experiment.py # bth new-experiment script skeleton
@@ -150,25 +168,24 @@ src/bathos/
150
168
  | 125 | `bth init` — dirs, .bth.toml, catalog bootstrap, SLURM config | P1 | 124 |
151
169
  | 126 | `bth run` — CLI wrapper, provenance capture | P1 | 124 |
152
170
  | 127 | `bth ls` / `show` / `find` / `sql` — query interface | P1 | 126 |
153
- | 128 | FastMCP server — mirrors CLI tool-for-tool | P1 | 126, 127, 130 |
154
- | 129 | `@bth.experiment` — provenance decorator for Typer scripts | P2 | 124 |
155
- | 130 | `bth check` — freshness/validity vs git HEAD | P2 | 126 |
156
- | 131 | SLURM `_bth_env.sh` integration | P2 | 125, 126 |
157
- | 132 | `bth new-experiment` — Typer + sidecar scaffold | P2 | 125 |
158
- | 133 | `uv tool` packaging + release | P3 | 126–130 |
159
- | 134 | Script convention linter | P3 | — |
160
- | 135 | `bth migrate` — Phase 1 mechanical (existing projects) | P2 | 125 |
161
- | 136 | `bth-migrate` praxia workflow — agentic classification + git mv plan | P2 | 135 |
162
- | 137 | Global instruction portability (separate design session needed) | P2 | — |
163
- | 138 | `bth sync` — rsync cool-tier catalog to/from cluster remote | P2 | 125, v0.1 done |
164
- | 139 | `bth compact` + warm-tier DuckDB (`bth sql` catalog queries) | P2 | v0.1 done |
165
- | 140 | Schema versioning + extended provenance (hostname, slurm_job_id, metadata JSON, migrations) | P2 | 139 |
166
- | 141 | `bth archive` — warm→cold partitioned Parquet export | P3 | 139, 140 |
167
- | 142 | Results management — output convention, file-count utilities, direct management interface design | P2 | 139 |
171
+ | 128 | FastMCP server — mirrors CLI tool-for-tool | P1 ✓ done (v0.2) | 126, 127, 130 |
172
+ | 129 | `@bth.experiment` — provenance decorator for Typer scripts | P2 ✓ done (v0.2) | 124 |
173
+ | 130 | `bth check` — freshness/validity vs git HEAD | P2 ✓ done (v0.1/v0.2) | 126 |
174
+ | 131 | SLURM `_bth_env.sh` integration | P2 ✓ done (v0.1) | 125, 126 |
175
+ | 132 | `bth new-experiment` — Typer + sidecar scaffold | P2 ✓ done (v0.2) | 125 |
176
+ | 133 | `uv tool` packaging + release | P3 ✓ done (v0.2) | 126–130 |
177
+ | 134 | Script convention linter | P3 ✓ done (v0.2) | — |
178
+ | 135 | `bth migrate` — Phase 1 mechanical (existing projects) | P2 ✓ done (v0.2) | 125 |
179
+ | 136 | `bth-migrate` praxia workflow — agentic classification + git mv plan | P2 (not started) | 135 |
180
+ | 137 | Global instruction portability (separate design session needed) | P2 (not started) | — |
181
+ | 138 | `bth sync` — rsync cool-tier catalog to/from cluster remote | P2 ✓ done (v0.2) | 125, v0.1 done |
182
+ | 139 | `bth compact` + warm-tier DuckDB (`bth sql` catalog queries) | P2 ✓ done (v0.1) | v0.1 done |
183
+ | 140 | Schema versioning + extended provenance (hostname, slurm_job_id, metadata JSON, migrations) | P2 ✓ done (v0.2) | 139 |
184
+ | 141 | `bth archive` — warm→cold partitioned Parquet export | P3 ✓ done (v0.2) | 139, 140 |
185
+ | 142 | Results management — output convention, file-count utilities, direct management interface design | P2 (not started) | 139 |
168
186
 
169
187
  **Not yet backlogged:**
170
- - Sidecar pre-registration enforcement in `bth run` (add as P2, depends on 126)
171
- - Outcome evaluation + `runs.outcome` column in warm DuckDB (add as P2, depends on 139)
188
+ - *(none — sidecar enforcement and outcome evaluation shipped in v0.2/v0.3)*
172
189
 
173
190
  ---
174
191
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bathos
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Local-first, zero-server experiment tracking for researchers
5
5
  Project-URL: Homepage, https://github.com/mariellerossi/bathos
6
6
  Project-URL: Documentation, https://bathos.readthedocs.io
@@ -66,13 +66,59 @@ bth run scripts/experiments/benchmark_tip3p.py -- --n-steps 1000 --out outputs/r
66
66
  bth ls --since 7d
67
67
  bth show <run-id>
68
68
 
69
- # Check if a result is still valid (compares recorded git hash to HEAD)
70
- bth check outputs/run1.parquet
69
+ # Check catalog runs for git-drift freshness
70
+ bth check [--status <status>] [--check-outputs]
71
71
 
72
72
  # Escape hatch: raw DuckDB SQL against the catalog
73
73
  bth sql "SELECT project_slug, count(*) FROM runs GROUP BY 1"
74
74
  ```
75
75
 
76
+ ### `bth run` flags
77
+
78
+ **v0.3+ flags:**
79
+ - `--agent-mode collaborative|autonomous` — declare whether this run is agent-driven
80
+ - `--derived-from <run-id>` — record parent run for lineage tracking
81
+ - `--campaign <campaign-id>` — associate this run with a campaign
82
+ - `--no-sidecar` — bypass sidecar enforcement (use for ad-hoc runs)
83
+
84
+ ## CLI Reference
85
+
86
+ **Core commands:**
87
+ - `bth init` — initialize bathos in a project
88
+ - `bth run` — execute and track an experiment
89
+ - `bth ls` — list recent runs
90
+ - `bth show <run-id>` — display run details
91
+ - `bth find` — search runs by criteria
92
+ - `bth sql` — raw DuckDB query against catalog
93
+ - `bth check` — check catalog runs for git-drift freshness
94
+ - `bth lineage <run-id>` — show ancestor chain following parent_run_id links
95
+ - `bth sprint-audit [--hours N]` — audit recent runs across all registered projects
96
+ - `bth lint [--project-root PATH]` — check scripts/ for naming conventions and missing sidecars
97
+ - `bth new-experiment <name>` — scaffold a new experiment script and sidecar
98
+ - `bth migrate` — upgrade cool-tier Parquet fragments to current schema version
99
+ - `bth migrate-to-project-subdirs [--dry-run]` — move flat catalog runs into `runs/<slug>/` subdirs (v0.4+)
100
+ - `bth export` — export the using-bathos skill and register MCP server
101
+
102
+ **`bth sync`** — Sync cool-tier catalog to/from cluster (v0.4+: per-project filtered)
103
+ - `bth sync [<remote>]` — push only this project's runs to the remote (filtered by `project_slug`)
104
+ - `bth sync [<remote>] --pull` — pull only this project's runs from the remote
105
+ - Output: `Pushed 47 runs (filtered 275 from other projects) to 'engaging' in 1.2s`
106
+ - Config: `sync_filter = "none"` in `.bth.toml` disables filtering (pushes all projects)
107
+
108
+ **`bth remote`** — Manage sync remotes
109
+ - `bth remote add <name> <url>` — add an SSH remote for catalog sync
110
+ - `bth remote list` — list configured remotes
111
+ - `bth remote remove <name>` — remove a remote
112
+ - `bth remote test <name>` — test SSH connectivity to a remote
113
+
114
+ **`bth campaign`** — Manage experiment campaigns (v0.3+)
115
+ - `bth campaign create <id> --hypothesis <text>` — create a new campaign
116
+ - `bth campaign add <run-id> --campaign <id>` — associate a run with a campaign
117
+ - `bth campaign ls` — list campaigns
118
+ - `bth campaign show <id>` — show campaign details and runs
119
+ - `bth campaign review <id>` — statistical summary and anomaly detection
120
+ - `bth campaign conclude <id> --outcome <label>` — close campaign with outcome
121
+
76
122
  ## Script conventions
77
123
 
78
124
  `bth init` scaffolds the following structure in your project:
@@ -30,13 +30,59 @@ bth run scripts/experiments/benchmark_tip3p.py -- --n-steps 1000 --out outputs/r
30
30
  bth ls --since 7d
31
31
  bth show <run-id>
32
32
 
33
- # Check if a result is still valid (compares recorded git hash to HEAD)
34
- bth check outputs/run1.parquet
33
+ # Check catalog runs for git-drift freshness
34
+ bth check [--status <status>] [--check-outputs]
35
35
 
36
36
  # Escape hatch: raw DuckDB SQL against the catalog
37
37
  bth sql "SELECT project_slug, count(*) FROM runs GROUP BY 1"
38
38
  ```
39
39
 
40
+ ### `bth run` flags
41
+
42
+ **v0.3+ flags:**
43
+ - `--agent-mode collaborative|autonomous` — declare whether this run is agent-driven
44
+ - `--derived-from <run-id>` — record parent run for lineage tracking
45
+ - `--campaign <campaign-id>` — associate this run with a campaign
46
+ - `--no-sidecar` — bypass sidecar enforcement (use for ad-hoc runs)
47
+
48
+ ## CLI Reference
49
+
50
+ **Core commands:**
51
+ - `bth init` — initialize bathos in a project
52
+ - `bth run` — execute and track an experiment
53
+ - `bth ls` — list recent runs
54
+ - `bth show <run-id>` — display run details
55
+ - `bth find` — search runs by criteria
56
+ - `bth sql` — raw DuckDB query against catalog
57
+ - `bth check` — check catalog runs for git-drift freshness
58
+ - `bth lineage <run-id>` — show ancestor chain following parent_run_id links
59
+ - `bth sprint-audit [--hours N]` — audit recent runs across all registered projects
60
+ - `bth lint [--project-root PATH]` — check scripts/ for naming conventions and missing sidecars
61
+ - `bth new-experiment <name>` — scaffold a new experiment script and sidecar
62
+ - `bth migrate` — upgrade cool-tier Parquet fragments to current schema version
63
+ - `bth migrate-to-project-subdirs [--dry-run]` — move flat catalog runs into `runs/<slug>/` subdirs (v0.4+)
64
+ - `bth export` — export the using-bathos skill and register MCP server
65
+
66
+ **`bth sync`** — Sync cool-tier catalog to/from cluster (v0.4+: per-project filtered)
67
+ - `bth sync [<remote>]` — push only this project's runs to the remote (filtered by `project_slug`)
68
+ - `bth sync [<remote>] --pull` — pull only this project's runs from the remote
69
+ - Output: `Pushed 47 runs (filtered 275 from other projects) to 'engaging' in 1.2s`
70
+ - Config: `sync_filter = "none"` in `.bth.toml` disables filtering (pushes all projects)
71
+
72
+ **`bth remote`** — Manage sync remotes
73
+ - `bth remote add <name> <url>` — add an SSH remote for catalog sync
74
+ - `bth remote list` — list configured remotes
75
+ - `bth remote remove <name>` — remove a remote
76
+ - `bth remote test <name>` — test SSH connectivity to a remote
77
+
78
+ **`bth campaign`** — Manage experiment campaigns (v0.3+)
79
+ - `bth campaign create <id> --hypothesis <text>` — create a new campaign
80
+ - `bth campaign add <run-id> --campaign <id>` — associate a run with a campaign
81
+ - `bth campaign ls` — list campaigns
82
+ - `bth campaign show <id>` — show campaign details and runs
83
+ - `bth campaign review <id>` — statistical summary and anomaly detection
84
+ - `bth campaign conclude <id> --outcome <label>` — close campaign with outcome
85
+
40
86
  ## Script conventions
41
87
 
42
88
  `bth init` scaffolds the following structure in your project:
@@ -38,14 +38,18 @@ The skill is loaded automatically at session start in Claude Code. In a new sess
38
38
 
39
39
  | MCP Tool | CLI equivalent | Status |
40
40
  |----------|---------------|--------|
41
- | `run_script` | `bth run` | ✅ Available |
41
+ | `run` | `bth run` | ✅ Available |
42
42
  | `list_runs` | `bth ls` | ✅ Available |
43
43
  | `get_run` | `bth show <id>` | ✅ Available |
44
44
  | `find_runs` | `bth find` | ✅ Available |
45
45
  | `run_sql` | `bth sql` | ✅ Available |
46
46
  | `compact_catalog` | `bth compact` | ✅ Available |
47
- | `check_runs` | `bth check` | 🔜 Coming in v0.2 |
48
- | `archive_runs` | `bth archive` | 🔜 Coming in v0.2 |
47
+ | `check` | `bth check` | ✅ Available |
48
+ | `archive` | `bth archive` | ✅ Available |
49
+ | `campaign_create` | `bth campaign create` | ✅ Available |
50
+ | `campaign_list` | `bth campaign ls` | ✅ Available |
51
+ | `campaign_review` | `bth campaign review` | ✅ Available |
52
+ | `campaign_conclude` | `bth campaign conclude` | ✅ Available |
49
53
 
50
54
  ## Update
51
55
 
@@ -13,7 +13,7 @@ description: "Use when working in any bathos-tracked research project — experi
13
13
 
14
14
  **Scope:** Single researcher, 10+ projects, SLURM cluster integration.
15
15
 
16
- **Status:** ✅ v0.2 shipped — 207 tests passing. Full CLI + MCP server available.
16
+ **Status:** ✅ v0.4.0 shipped — 333 tests passing. Full CLI + MCP server available, including campaigns, agentic integrity gates, and per-project sync filtering.
17
17
 
18
18
  ---
19
19
 
@@ -34,7 +34,7 @@ description: "Use when working in any bathos-tracked research project — experi
34
34
  | Command | Arguments | Status | Notes |
35
35
  |---------|-----------|--------|-------|
36
36
  | bth init | --slug, --remote, --slurm-partition | ✅ | Project initialization |
37
- | bth run | <script> [-- args], --tag, --out, --campaign, --agent-mode, --no-sidecar | ✅ | Execute + provenance capture |
37
+ | bth run | <script> [-- args], --tag, --out, --campaign, --agent-mode, --derived-from, --no-sidecar | ✅ | Execute + provenance capture; v0.3: `--agent-mode` (collaborative/autonomous), `--derived-from` (lineage), `--campaign` (campaign association), `--no-sidecar` (bypass enforcement) |
38
38
  | bth ls | --since, --status, --limit, --project | ✅ | List recent runs + OUTCOME column |
39
39
  | bth show | <run-id> | ✅ | Full run details + git state |
40
40
  | bth find | --project, --since, --status, --tag, --output-file | ✅ | Flexible filtered query |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bathos"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Local-first, zero-server experiment tracking for researchers"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -1,4 +1,4 @@
1
- __version__ = "0.3.0"
1
+ __version__ = "0.4.0"
2
2
 
3
3
  from bathos.decorators import experiment
4
4
 
@@ -15,7 +15,7 @@ def init_catalog(catalog_dir: Path) -> None:
15
15
 
16
16
  def write_run(run: Run, catalog_dir: Path) -> None:
17
17
  """Write (or overwrite) a run record atomically."""
18
- runs_dir = catalog_dir / "runs"
18
+ runs_dir = catalog_dir / "runs" / run.project_slug
19
19
  runs_dir.mkdir(parents=True, exist_ok=True)
20
20
  target = runs_dir / f"run_{run.id}.parquet"
21
21
  tmp = runs_dir / f"run_{run.id}.tmp.parquet"
@@ -28,7 +28,7 @@ def read_runs(catalog_dir: Path) -> list[Run]:
28
28
  runs_dir = catalog_dir / "runs"
29
29
  if not runs_dir.exists():
30
30
  return []
31
- parquet_files = list(runs_dir.glob("run_*.parquet"))
31
+ parquet_files = list(runs_dir.rglob("run_*.parquet"))
32
32
  if not parquet_files:
33
33
  return []
34
34
  tables = [pq.read_table(f) for f in parquet_files]
@@ -595,9 +595,10 @@ def sync(
595
595
 
596
596
  try:
597
597
  result = sync_catalog(remote, config, catalog_dir, pull=pull)
598
- direction = "pulled" if pull else "pushed"
598
+ direction = "Pulled" if pull else "Pushed"
599
+ filter_msg = f" (filtered {result.filtered} from other projects)" if result.filtered > 0 else ""
599
600
  typer.echo(
600
- f"{direction.capitalize()} {result.transferred} runs to/from '{result.remote}' in {result.duration_s:.1f}s"
601
+ f"{direction} {result.transferred} runs{filter_msg} to/from '{result.remote}' in {result.duration_s:.1f}s"
601
602
  )
602
603
  except ValueError as e:
603
604
  typer.echo(str(e), err=True)
@@ -624,6 +625,27 @@ def migrate(
624
625
  typer.echo(" Nothing to migrate.")
625
626
 
626
627
 
628
+ @app.command("migrate-to-project-subdirs")
629
+ def migrate_to_subdirs_cmd(
630
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be moved without writing"),
631
+ ):
632
+ """Move flat cool-tier run parquets into per-project subdirectories.
633
+
634
+ Reads each run's project_slug and moves it to runs/<slug>/run_<uuid>.parquet.
635
+ Run this on both local and remote before using per-project sync filtering.
636
+ """
637
+ from bathos.migrate import migrate_to_project_subdirs
638
+
639
+ result = migrate_to_project_subdirs(_catalog_dir(), dry_run=dry_run)
640
+ action = "Would move" if dry_run else "Moved"
641
+ typer.echo(f"{action} {result.moved} run(s) into per-project subdirectories.")
642
+ if result.skipped:
643
+ typer.echo(f" {result.skipped} already in place (skipped).")
644
+ if result.by_slug:
645
+ for slug, count in sorted(result.by_slug.items()):
646
+ typer.echo(f" {slug}: {count}")
647
+
648
+
627
649
  @app.command("sprint-audit")
628
650
  def sprint_audit_cmd(
629
651
  hours: int = typer.Option(24, "--hours", help="Lookback window in hours"),
@@ -180,7 +180,7 @@ def _fragment_count(catalog_dir: Path) -> int:
180
180
  runs_dir = catalog_dir / "runs"
181
181
  if not runs_dir.exists():
182
182
  return 0
183
- return len(list(runs_dir.glob("run_*.parquet")))
183
+ return len(list(runs_dir.rglob("run_*.parquet")))
184
184
 
185
185
 
186
186
  def should_compact(catalog_dir: Path) -> bool:
@@ -14,6 +14,7 @@ class ProjectConfig:
14
14
  catalog_dir: Path = field(default_factory=lambda: Path.home() / ".bth" / "catalog")
15
15
  remotes: dict[str, dict] = field(default_factory=dict)
16
16
  slurm: dict = field(default_factory=dict)
17
+ sync_filter: str = "project_slug"
17
18
 
18
19
 
19
20
  def default_catalog_dir() -> Path:
@@ -38,6 +39,7 @@ def load_project_config(path: Path) -> ProjectConfig:
38
39
  catalog_dir=Path(project["catalog_dir"]) if "catalog_dir" in project else default_catalog_dir(),
39
40
  remotes=data.get("remotes", {}),
40
41
  slurm=data.get("slurm", {}),
42
+ sync_filter=project.get("sync_filter", "project_slug"),
41
43
  )
42
44
 
43
45
 
@@ -316,6 +316,7 @@ def sync_tool(
316
316
  "transferred": result.transferred,
317
317
  "duration_s": result.duration_s,
318
318
  "remote": result.remote,
319
+ "filtered": result.filtered,
319
320
  }
320
321
  return json.dumps(result_dict, indent=2)
321
322
  except Exception as e:
@@ -24,7 +24,7 @@ def migrate_catalog(catalog_dir: Path, dry_run: bool = False) -> MigrateResult:
24
24
  if not runs_dir.exists():
25
25
  return MigrateResult(scanned=0, already_current=0, migrated=0, dry_run=dry_run)
26
26
 
27
- fragments = list(runs_dir.glob("run_*.parquet"))
27
+ fragments = list(runs_dir.rglob("run_*.parquet"))
28
28
  current_names = {f.name for f in COOL_SCHEMA}
29
29
  migrated = 0
30
30
 
@@ -62,6 +62,61 @@ def migrate_catalog(catalog_dir: Path, dry_run: bool = False) -> MigrateResult:
62
62
  )
63
63
 
64
64
 
65
+ @dataclass
66
+ class MigrateToSubdirsResult:
67
+ moved: int
68
+ skipped: int
69
+ by_slug: dict
70
+ dry_run: bool
71
+
72
+
73
+ def migrate_to_project_subdirs(catalog_dir: Path, dry_run: bool = False) -> MigrateToSubdirsResult:
74
+ """Move flat cool-tier run parquets into per-project subdirectories.
75
+
76
+ Reads project_slug from each parquet and moves it to runs/<slug>/run_<uuid>.parquet.
77
+ Files already in a subdir are skipped. Idempotent.
78
+ Falls back to 'default' for parquets without a readable project_slug.
79
+ """
80
+ from collections import Counter
81
+
82
+ runs_dir = catalog_dir / "runs"
83
+ if not runs_dir.exists():
84
+ return MigrateToSubdirsResult(moved=0, skipped=0, by_slug={}, dry_run=dry_run)
85
+
86
+ # Only consider directly-nested parquets (flat layout), not already-subdir'd ones
87
+ flat_files = [f for f in runs_dir.glob("run_*.parquet")]
88
+
89
+ moves = []
90
+ for parquet in flat_files:
91
+ slug = _read_project_slug(parquet) or "default"
92
+ dst = runs_dir / slug / parquet.name
93
+ moves.append((parquet, dst))
94
+
95
+ if not dry_run:
96
+ for src, dst in moves:
97
+ dst.parent.mkdir(parents=True, exist_ok=True)
98
+ src.rename(dst)
99
+
100
+ by_slug = dict(Counter(dst.parent.name for _, dst in moves))
101
+ return MigrateToSubdirsResult(
102
+ moved=len(moves),
103
+ skipped=len(flat_files) - len(moves),
104
+ by_slug=by_slug,
105
+ dry_run=dry_run,
106
+ )
107
+
108
+
109
+ def _read_project_slug(parquet: Path) -> str | None:
110
+ """Read project_slug from the first row of a parquet file."""
111
+ try:
112
+ tbl = pq.read_table(parquet, columns=["project_slug"])
113
+ if tbl.num_rows > 0:
114
+ return tbl["project_slug"][0].as_py()
115
+ except Exception:
116
+ pass
117
+ return None
118
+
119
+
65
120
  def _default_array(arrow_type: pa.DataType, n: int) -> pa.Array:
66
121
  if pa.types.is_string(arrow_type) or pa.types.is_large_string(arrow_type):
67
122
  return pa.array([""] * n, type=arrow_type)
@@ -137,6 +137,9 @@ class Run:
137
137
  ts = pydict["timestamp"][i]
138
138
  if not isinstance(ts, datetime):
139
139
  ts = ts.as_py()
140
+ # Ensure timestamp is timezone-aware in UTC for internal comparisons
141
+ if ts.tzinfo is None:
142
+ ts = ts.replace(tzinfo=UTC)
140
143
  return cls(
141
144
  id=pydict["id"][i],
142
145
  project_slug=pydict["project_slug"][i],
@@ -7,6 +7,8 @@ from pathlib import Path
7
7
 
8
8
  from bathos.config import ProjectConfig
9
9
 
10
+ _SYNC_TIMEOUT_S = 120 # rsync kill switch: fail if no completion within 2 minutes
11
+
10
12
 
11
13
  @dataclass
12
14
  class SyncResult:
@@ -15,6 +17,7 @@ class SyncResult:
15
17
  transferred: int
16
18
  duration_s: float
17
19
  remote: str
20
+ filtered: int = 0
18
21
 
19
22
 
20
23
  def sync_catalog(
@@ -43,8 +46,27 @@ def sync_catalog(
43
46
  host = remote_config["host"]
44
47
  remote_root = remote_config["remote_root"]
45
48
 
46
- local_runs = catalog_dir / "runs"
47
- remote_runs = f"{host}:{remote_root}/.bth/catalog/runs/"
49
+ project_slug = config.slug
50
+ sync_filter = getattr(config, "sync_filter", "project_slug")
51
+
52
+ if sync_filter == "project_slug":
53
+ local_runs = catalog_dir / "runs" / project_slug
54
+ remote_runs = f"{host}:{remote_root}/.bth/catalog/runs/{project_slug}/"
55
+ # Count filtered runs (total in catalog minus this project's runs)
56
+ runs_root = catalog_dir / "runs"
57
+ if not pull and runs_root.exists():
58
+ total = len(list(runs_root.rglob("run_*.parquet")))
59
+ project_count = len(list(local_runs.glob("run_*.parquet"))) if local_runs.exists() else 0
60
+ filtered = total - project_count
61
+ else:
62
+ filtered = 0
63
+ else:
64
+ local_runs = catalog_dir / "runs"
65
+ remote_runs = f"{host}:{remote_root}/.bth/catalog/runs/"
66
+ filtered = 0
67
+
68
+ if not pull:
69
+ local_runs.mkdir(parents=True, exist_ok=True)
48
70
 
49
71
  if pull:
50
72
  src = remote_runs
@@ -53,17 +75,30 @@ def sync_catalog(
53
75
  src = str(local_runs) + "/"
54
76
  dst = remote_runs
55
77
 
56
- # Construct rsync command
78
+ # ConnectTimeout=10: fail in 10s if host is unreachable rather than hanging
79
+ # BatchMode=yes: never prompt for a password — fail immediately instead
80
+ ssh_opts = "ssh -o ConnectTimeout=10 -o BatchMode=yes"
81
+
57
82
  cmd = [
58
83
  "rsync",
59
- "-azP",
84
+ "-az",
85
+ "--partial",
86
+ f"-e{ssh_opts}",
60
87
  "--ignore-existing",
61
88
  src,
62
89
  dst,
63
90
  ]
64
91
 
65
92
  start_time = time.time()
66
- result = subprocess.run(cmd, capture_output=True, text=True)
93
+ try:
94
+ result = subprocess.run(
95
+ cmd, capture_output=True, text=True, timeout=_SYNC_TIMEOUT_S
96
+ )
97
+ except subprocess.TimeoutExpired:
98
+ raise RuntimeError(
99
+ f"bth sync timed out after {_SYNC_TIMEOUT_S}s — "
100
+ "check that the remote host is reachable and SSH keys are configured"
101
+ )
67
102
  duration_s = time.time() - start_time
68
103
 
69
104
  if result.returncode != 0:
@@ -73,7 +108,7 @@ def sync_catalog(
73
108
  # rsync outputs something like "Number of regular files transferred: 42"
74
109
  transferred = _parse_transferred_count(result.stdout)
75
110
 
76
- return SyncResult(transferred=transferred, duration_s=duration_s, remote=remote_name)
111
+ return SyncResult(transferred=transferred, duration_s=duration_s, remote=remote_name, filtered=filtered)
77
112
 
78
113
 
79
114
  def _parse_transferred_count(rsync_output: str) -> int:
@@ -24,7 +24,7 @@ def test_write_and_read_single_run(tmp_catalog: Path, sample_run: Run):
24
24
  def test_write_creates_parquet_file(tmp_catalog: Path, sample_run: Run):
25
25
  init_catalog(tmp_catalog)
26
26
  write_run(sample_run, tmp_catalog)
27
- files = list((tmp_catalog / "runs").glob("*.parquet"))
27
+ files = list((tmp_catalog / "runs").rglob("*.parquet"))
28
28
  assert len(files) == 1
29
29
  assert sample_run.id in files[0].name
30
30
 
@@ -186,8 +186,8 @@ def test_sql_allows_arbitrary_queries(tmp_path: Path, monkeypatch):
186
186
  # Create a run to have Parquet files
187
187
  runner.invoke(app, ["run", sys.executable, "--", "-c", "pass"])
188
188
 
189
- # Query using read_parquet should work without warm DB
190
- glob = str(catalog / "runs" / "run_*.parquet")
189
+ # Query using read_parquet should work without warm DB (runs are in per-project subdir)
190
+ glob = str(catalog / "runs" / "testproj" / "run_*.parquet")
191
191
  result = runner.invoke(app, ["sql", f"SELECT COUNT(*) FROM read_parquet('{glob}')"])
192
192
  assert result.exit_code == 0
193
193
  assert "1" in result.output
@@ -119,7 +119,7 @@ remote_root = "~/projects/testproj"
119
119
  write_run(run2, catalog)
120
120
 
121
121
  # Verify we have 2 cool fragments
122
- run_files = list((catalog / "runs").glob("run_*.parquet"))
122
+ run_files = list((catalog / "runs").rglob("run_*.parquet"))
123
123
  assert len(run_files) == 2, f"Expected 2 run files, got {len(run_files)}"
124
124
 
125
125
  # Step 3: Compact cool fragments → warm DuckDB
@@ -177,7 +177,7 @@ remote_root = "~/projects/testproj"
177
177
 
178
178
  # Verify rsync command structure
179
179
  assert cmd[0] == "rsync"
180
- assert "-azP" in cmd
180
+ assert "-az" in cmd
181
181
  assert "--ignore-existing" in cmd
182
182
  # Should have source and destination with runs/ directories
183
183
  assert any("runs/" in str(arg) for arg in cmd)
@@ -238,7 +238,7 @@ def test_cluster_workflow_with_slurm_job_id(git_repo: Path, monkeypatch):
238
238
  write_run(run, catalog)
239
239
 
240
240
  # Verify SLURM_JOB_ID is in the written fragment
241
- run_files = list((catalog / "runs").glob("run_*.parquet"))
241
+ run_files = list((catalog / "runs").rglob("run_*.parquet"))
242
242
  assert len(run_files) == 1
243
243
 
244
244
  # Compact and verify slurm_job_id is preserved in warm tier