sql-harness 0.2.1__tar.gz → 0.2.3__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 (95) hide show
  1. {sql_harness-0.2.1 → sql_harness-0.2.3}/PKG-INFO +4 -3
  2. {sql_harness-0.2.1 → sql_harness-0.2.3}/README.md +3 -2
  3. {sql_harness-0.2.1 → sql_harness-0.2.3}/SKILL.md +5 -7
  4. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/docker-via-ssh.md +1 -2
  5. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/ssh.md +1 -1
  6. {sql_harness-0.2.1 → sql_harness-0.2.3}/pyproject.toml +2 -2
  7. {sql_harness-0.2.1 → sql_harness-0.2.3}/src/sql_harness/__init__.py +6 -1
  8. {sql_harness-0.2.1 → sql_harness-0.2.3}/src/sql_harness/cli.py +28 -22
  9. {sql_harness-0.2.1 → sql_harness-0.2.3}/src/sql_harness/helpers.py +17 -18
  10. {sql_harness-0.2.1 → sql_harness-0.2.3}/src/sql_harness/paths.py +1 -1
  11. {sql_harness-0.2.1 → sql_harness-0.2.3}/tests/unit/test_cli.py +1 -1
  12. {sql_harness-0.2.1 → sql_harness-0.2.3}/tests/unit/test_manager.py +4 -0
  13. {sql_harness-0.2.1 → sql_harness-0.2.3}/uv.lock +1 -1
  14. sql_harness-0.2.1/agent-workspace/skills/README.md +0 -26
  15. sql_harness-0.2.1/agent-workspace/skills/docker-deploy.md +0 -85
  16. sql_harness-0.2.1/agent-workspace/skills/ssh.md +0 -70
  17. {sql_harness-0.2.1 → sql_harness-0.2.3}/.claude-plugin/marketplace.json +0 -0
  18. {sql_harness-0.2.1 → sql_harness-0.2.3}/.claude-plugin/plugin.json +0 -0
  19. {sql_harness-0.2.1 → sql_harness-0.2.3}/.env.example +0 -0
  20. {sql_harness-0.2.1 → sql_harness-0.2.3}/.github/ISSUE_TEMPLATE/bug-report.yml +0 -0
  21. {sql_harness-0.2.1 → sql_harness-0.2.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  22. {sql_harness-0.2.1 → sql_harness-0.2.3}/.github/ISSUE_TEMPLATE/feature-request.yml +0 -0
  23. {sql_harness-0.2.1 → sql_harness-0.2.3}/.gitignore +0 -0
  24. {sql_harness-0.2.1 → sql_harness-0.2.3}/AGENTS.md +0 -0
  25. {sql_harness-0.2.1 → sql_harness-0.2.3}/agent-workspace/README.md +0 -0
  26. {sql_harness-0.2.1 → sql_harness-0.2.3}/agent-workspace/agent_helpers.example.py +0 -0
  27. {sql_harness-0.2.1 → sql_harness-0.2.3}/agent-workspace/agent_helpers.py +0 -0
  28. {sql_harness-0.2.1 → sql_harness-0.2.3}/agent-workspace/zones/example_pg/scripts/sh_demo_crud.py +0 -0
  29. {sql_harness-0.2.1 → sql_harness-0.2.3}/agent-workspace/zones/example_pg/skills/sh_demo-schema.md +0 -0
  30. {sql_harness-0.2.1/agent-workspace → sql_harness-0.2.3/agent-workspace/zones/meta}/skills/pool.md +0 -0
  31. {sql_harness-0.2.1/agent-workspace → sql_harness-0.2.3/agent-workspace/zones/meta}/skills/workspace.md +0 -0
  32. {sql_harness-0.2.1 → sql_harness-0.2.3}/docker-services/coturn/README.md +0 -0
  33. {sql_harness-0.2.1 → sql_harness-0.2.3}/docker-services/coturn/docker-compose.yml +0 -0
  34. {sql_harness-0.2.1 → sql_harness-0.2.3}/docker-services/coturn/turnserver.conf +0 -0
  35. {sql_harness-0.2.1 → sql_harness-0.2.3}/docker-services/mysql/docker-compose.yml +0 -0
  36. {sql_harness-0.2.1 → sql_harness-0.2.3}/docker-services/redis/docker-compose.yml +0 -0
  37. {sql_harness-0.2.1 → sql_harness-0.2.3}/docs/connections-toml-example.toml +0 -0
  38. {sql_harness-0.2.1 → sql_harness-0.2.3}/docs/headless-notes.md +0 -0
  39. {sql_harness-0.2.1 → sql_harness-0.2.3}/docs/maintainer.md +0 -0
  40. {sql_harness-0.2.1 → sql_harness-0.2.3}/docs/standalone-install.md +0 -0
  41. {sql_harness-0.2.1 → sql_harness-0.2.3}/install.md +0 -0
  42. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/aggregations.md +0 -0
  43. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/encoding-and-charset.md +0 -0
  44. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/indexes-and-explain.md +0 -0
  45. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/joins.md +0 -0
  46. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/json-columns.md +0 -0
  47. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/large-result-sets.md +0 -0
  48. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/migrations.md +0 -0
  49. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/pooling.md +0 -0
  50. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/postgres/btree-indexes.md +0 -0
  51. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/postgres/plan-reading.md +0 -0
  52. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/postgres/slow-queries-joins.md +0 -0
  53. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/postgres/specialized-indexes.md +0 -0
  54. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/postgres/table-optimization.md +0 -0
  55. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/recursive-ctes.md +0 -0
  56. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/references/aggregations-advanced.md +0 -0
  57. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/references/joins-advanced.md +0 -0
  58. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/references/recursive-ctes-patterns.md +0 -0
  59. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/references/window-functions-catalog.md +0 -0
  60. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/save-run-cycle.md +0 -0
  61. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/schema-introspection.md +0 -0
  62. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/timeouts-and-cancellation.md +0 -0
  63. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/transactions.md +0 -0
  64. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/window-functions.md +0 -0
  65. {sql_harness-0.2.1 → sql_harness-0.2.3}/interaction-skills/zone-skill-auto-surface.md +0 -0
  66. {sql_harness-0.2.1 → sql_harness-0.2.3}/practice/pgexercises/README.md +0 -0
  67. {sql_harness-0.2.1 → sql_harness-0.2.3}/skills/sql-harness/README.txt +0 -0
  68. {sql_harness-0.2.1 → sql_harness-0.2.3}/skills/sql-harness/SKILL.md +0 -0
  69. {sql_harness-0.2.1 → sql_harness-0.2.3}/skills/sql-harness/references/install.md +0 -0
  70. {sql_harness-0.2.1 → sql_harness-0.2.3}/sql-harness +0 -0
  71. {sql_harness-0.2.1 → sql_harness-0.2.3}/src/sql_harness/SKILL.md +0 -0
  72. {sql_harness-0.2.1 → sql_harness-0.2.3}/src/sql_harness/agent_loader.py +0 -0
  73. {sql_harness-0.2.1 → sql_harness-0.2.3}/src/sql_harness/config.py +0 -0
  74. {sql_harness-0.2.1 → sql_harness-0.2.3}/src/sql_harness/drivers/__init__.py +0 -0
  75. {sql_harness-0.2.1 → sql_harness-0.2.3}/src/sql_harness/drivers/mysql.py +0 -0
  76. {sql_harness-0.2.1 → sql_harness-0.2.3}/src/sql_harness/drivers/postgres.py +0 -0
  77. {sql_harness-0.2.1 → sql_harness-0.2.3}/src/sql_harness/drivers/redis.py +0 -0
  78. {sql_harness-0.2.1 → sql_harness-0.2.3}/src/sql_harness/drivers/sqlite.py +0 -0
  79. {sql_harness-0.2.1 → sql_harness-0.2.3}/src/sql_harness/drivers/ssh.py +0 -0
  80. {sql_harness-0.2.1 → sql_harness-0.2.3}/src/sql_harness/manager.py +0 -0
  81. {sql_harness-0.2.1 → sql_harness-0.2.3}/src/sql_harness/run.py +0 -0
  82. {sql_harness-0.2.1 → sql_harness-0.2.3}/tests/__init__.py +0 -0
  83. {sql_harness-0.2.1 → sql_harness-0.2.3}/tests/conftest.py +0 -0
  84. {sql_harness-0.2.1 → sql_harness-0.2.3}/tests/integration/__init__.py +0 -0
  85. {sql_harness-0.2.1 → sql_harness-0.2.3}/tests/integration/test_mysql.py +0 -0
  86. {sql_harness-0.2.1 → sql_harness-0.2.3}/tests/integration/test_pg_optimization.py +0 -0
  87. {sql_harness-0.2.1 → sql_harness-0.2.3}/tests/integration/test_pgexercises_practice.py +0 -0
  88. {sql_harness-0.2.1 → sql_harness-0.2.3}/tests/integration/test_postgres.py +0 -0
  89. {sql_harness-0.2.1 → sql_harness-0.2.3}/tests/integration/test_ssh.py +0 -0
  90. {sql_harness-0.2.1 → sql_harness-0.2.3}/tests/unit/__init__.py +0 -0
  91. {sql_harness-0.2.1 → sql_harness-0.2.3}/tests/unit/test_agent_loader.py +0 -0
  92. {sql_harness-0.2.1 → sql_harness-0.2.3}/tests/unit/test_config.py +0 -0
  93. {sql_harness-0.2.1 → sql_harness-0.2.3}/tests/unit/test_helpers.py +0 -0
  94. {sql_harness-0.2.1 → sql_harness-0.2.3}/tests/unit/test_paths.py +0 -0
  95. {sql_harness-0.2.1 → sql_harness-0.2.3}/tests/unit/test_ssh.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sql-harness
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Single-process SQL + SSH CLI for LLM agents. PostgreSQL/MySQL/SSH via SQLAlchemy + paramiko. Plaintext credentials in one TOML file. Helpers auto-injected into the heredoc namespace.
5
5
  Project-URL: Source, https://github.com/zhaoliuxue/much_bigpy/tree/master/lab/sql_harness
6
6
  Project-URL: Issues, https://github.com/zhaoliuxue/much_bigpy/issues
@@ -58,7 +58,8 @@ PY
58
58
  - `lab/sql_harness/src/sql_harness/` — protected core package
59
59
  - `${XDG_CONFIG_HOME:-~/.config}/sql-harness/connections.toml` — plaintext credentials in ONE place
60
60
  - `${XDG_CONFIG_HOME:-~/.config}/sql-harness/agent-workspace/agent_helpers.py` — task-specific helpers
61
- - `${XDG_CONFIG_HOME:-~/.config}/sql-harness/agent-workspace/skills/` — per-task/per-table skills
61
+ - `${XDG_CONFIG_HOME:-~/.config}/sql-harness/agent-workspace/zones/<conn>/skills/` — per-DSN skills (one zone per connection)
62
+ - `${XDG_CONFIG_HOME:-~/.config}/sql-harness/agent-workspace/zones/meta/skills/` — cross-DSN strategy skills (passive fallback for `apply_skill`)
62
63
 
63
64
  ## Why mirror browser-harness?
64
65
 
@@ -78,7 +79,7 @@ PY
78
79
 
79
80
  PRs and improvements welcome. See `AGENTS.md` for code priorities.
80
81
 
81
- - **Skills are written by the harness, not by you.** When you figure out a non-obvious SQL flow (a weird schema, a slow query, a JSON column trick), file a skill in `agent-workspace/skills/<name>.md`. Future sessions will read it before re-discovering it.
82
+ - **Skills are written by the harness, not by you.** When you figure out a non-obvious SQL flow (a weird schema, a slow query, a JSON column trick), file a skill in `agent-workspace/zones/<conn>/skills/<name>.md` for that DSN, or `agent-workspace/zones/meta/skills/<name>.md` if it generalizes. Future sessions will read it before re-discovering it.
82
83
  - Bug fixes, new drivers, helper additions all welcome.
83
84
 
84
85
  ## License
@@ -35,7 +35,8 @@ PY
35
35
  - `lab/sql_harness/src/sql_harness/` — protected core package
36
36
  - `${XDG_CONFIG_HOME:-~/.config}/sql-harness/connections.toml` — plaintext credentials in ONE place
37
37
  - `${XDG_CONFIG_HOME:-~/.config}/sql-harness/agent-workspace/agent_helpers.py` — task-specific helpers
38
- - `${XDG_CONFIG_HOME:-~/.config}/sql-harness/agent-workspace/skills/` — per-task/per-table skills
38
+ - `${XDG_CONFIG_HOME:-~/.config}/sql-harness/agent-workspace/zones/<conn>/skills/` — per-DSN skills (one zone per connection)
39
+ - `${XDG_CONFIG_HOME:-~/.config}/sql-harness/agent-workspace/zones/meta/skills/` — cross-DSN strategy skills (passive fallback for `apply_skill`)
39
40
 
40
41
  ## Why mirror browser-harness?
41
42
 
@@ -55,7 +56,7 @@ PY
55
56
 
56
57
  PRs and improvements welcome. See `AGENTS.md` for code priorities.
57
58
 
58
- - **Skills are written by the harness, not by you.** When you figure out a non-obvious SQL flow (a weird schema, a slow query, a JSON column trick), file a skill in `agent-workspace/skills/<name>.md`. Future sessions will read it before re-discovering it.
59
+ - **Skills are written by the harness, not by you.** When you figure out a non-obvious SQL flow (a weird schema, a slow query, a JSON column trick), file a skill in `agent-workspace/zones/<conn>/skills/<name>.md` for that DSN, or `agent-workspace/zones/meta/skills/<name>.md` if it generalizes. Future sessions will read it before re-discovering it.
59
60
  - Bug fixes, new drivers, helper additions all welcome.
60
61
 
61
62
  ## License
@@ -58,14 +58,12 @@ PG optimization depth — `interaction-skills/postgres/`:
58
58
  - `interaction-skills/postgres/specialized-indexes.md`
59
59
  - `interaction-skills/postgres/table-optimization.md`
60
60
 
61
- ## Workspace / strategy skills
61
+ ## Cross-DSN strategy skills
62
62
 
63
- Read these when working across multiple connections, tuning pool behavior, or operating remote hosts:
63
+ Read these when working across multiple connections, tuning pool behavior, or operating remote hosts. Reached via `apply_skill("pool")` etc. from any active zone — `zones/meta/skills/` is the passive fallback layer (the legacy global `agent-workspace/skills/` was collapsed into a regular zone so `apply_skill()` has a single shape: active zone or meta).
64
64
 
65
- - `agent-workspace/skills/pool.md` — pool sizing, `pre_ping`, idle reuse.
66
- - `agent-workspace/skills/workspace.md` — workspace isolation, multi-DB workflows.
67
- - `agent-workspace/skills/ssh.md` — strategy for hitting remote DB hosts over SSH.
68
- - `agent-workspace/skills/docker-deploy.md` — strategy: when to use docker-via-ssh, the `~/sql-harness/<svc>/` cohesion convention, service catalog (coturn/mysql/redis/...).
65
+ - `agent-workspace/zones/meta/skills/pool.md` — pool sizing, `pre_ping`, idle reuse.
66
+ - `agent-workspace/zones/meta/skills/workspace.md` — workspace isolation, multi-DB workflows.
69
67
 
70
68
  ## What actually works (field-tested)
71
69
 
@@ -98,4 +96,4 @@ Read these when working across multiple connections, tuning pool behavior, or op
98
96
 
99
97
  ## Domain / table skills
100
98
 
101
- PG optimization practice: `practice/pgexercises/` (71 pgexercises problems, see its README). Per-table or per-schema skills: drop a markdown file into `agent-workspace/skills/<name>.md`, read with `apply_skill(name)`.
99
+ PG optimization practice: `practice/pgexercises/` (71 pgexercises problems, see its README). Per-table or per-schema skills: drop a markdown file into `agent-workspace/zones/<conn>/skills/<name>.md`, read with `apply_skill(name)`. Cross-DSN strategy goes to `zones/meta/skills/` instead.
@@ -130,8 +130,7 @@ Those live in `docker-services/coturn/README.md` — read before deploying cotur
130
130
 
131
131
  ## See also
132
132
 
133
- - `agent-workspace/skills/docker-deploy.md` strategy (when to use this pattern,
134
- service catalog, why `~/sql-harness/<svc>/` convention)
133
+ - This file's "Detection" / "Approach" sections (decision P: the legacy strategy stub in `agent-workspace/zones/meta/skills/docker-deploy.md` was collapsed into the mechanic; no separate doc).
135
134
  - `interaction-skills/ssh.md` — the raw ssh_exec/upload/download helpers
136
135
  - `docker-services/<svc>/` — shipped templates (coturn, mysql, redis, ...)
137
136
  - `docker-services/<svc>/README.md` — service-specific gotchas (e.g. coturn)
@@ -1,6 +1,6 @@
1
1
  # SSH mechanic — how to talk to an SSH workspace
2
2
 
3
- > **Mechanic guide.** For the strategy (when to use it, auth choice), see `agent-workspace/skills/ssh.md`.
3
+ > **Mechanic guide.** Strategy notes (when to reach for the SSH driver, auth choice) are folded into this file's "Detection" / "Approach" sections below (decision P: the legacy strategy stub was collapsed into the mechanic).
4
4
 
5
5
  ## Activation
6
6
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sql-harness"
3
- version = "0.2.1"
3
+ version = "0.2.3"
4
4
  description = "Single-process SQL + SSH CLI for LLM agents. PostgreSQL/MySQL/SSH via SQLAlchemy + paramiko. Plaintext credentials in one TOML file. Helpers auto-injected into the heredoc namespace."
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -52,7 +52,7 @@ artifacts = ["src/sql_harness/SKILL.md"]
52
52
  # Paths are relative to this pyproject.toml (lab/sql_harness/).
53
53
  "SKILL.md" = "sql_harness/_skills/SKILL.md"
54
54
  "interaction-skills" = "sql_harness/_skills/interaction-skills"
55
- "agent-workspace/skills" = "sql_harness/_skills/agent-workspace-skills"
55
+ "agent-workspace/zones/meta/skills" = "sql_harness/_skills/zones-meta-skills"
56
56
  # Docker service templates (coturn/mysql/redis/...) — the docker-via-ssh mechanic
57
57
  # uploads these to ~/sql-harness/<svc>/ on a remote host.
58
58
  "docker-services" = "sql_harness/_skills/docker-services"
@@ -7,11 +7,16 @@ agent-editable workspace. Connections live in plaintext in one TOML file
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ try:
11
+ from importlib.metadata import version as _pkg_version
12
+ except Exception:
13
+ _pkg_version = None # type: ignore
14
+
10
15
  from .config import ConnectionConfig, ConnectionsConfig, PoolConfig, load, save
11
16
  from .drivers import Driver, get_driver
12
17
  from .manager import SqlHarness, Workspace
13
18
 
14
- __version__ = "0.1.0"
19
+ __version__ = _pkg_version("sql-harness") if _pkg_version else "0.0.0+local"
15
20
 
16
21
  __all__ = [
17
22
  "ConnectionConfig",
@@ -289,29 +289,27 @@ def cmd_skill(args, _harness: SqlHarness) -> int:
289
289
  if code:
290
290
  return code
291
291
  if sub == "list":
292
- d = zone_skills_dir(conn)
293
- zone = d.glob("*.md") if d.is_dir() else []
294
- # Also include global skills (shipped examples) as fallback.
295
- global_d = workspace_dir() / "skills"
296
- global_ = global_d.glob("*.md") if global_d.is_dir() else []
297
- seen = set()
298
- names = []
299
- for p in sorted(zone) + sorted(global_):
300
- if p.stem not in seen:
301
- seen.add(p.stem)
302
- names.append(p.stem)
292
+ seen: set[str] = set()
293
+ names: list[str] = []
294
+ # Two-layer lookup: active zone wins, zones/meta supplies cross-DSN fallback.
295
+ for d in (zone_skills_dir(conn), zone_skills_dir("meta")):
296
+ if d.is_dir():
297
+ for p in sorted(d.glob("*.md")):
298
+ if p.stem not in seen:
299
+ seen.add(p.stem)
300
+ names.append(p.stem)
303
301
  _emit({"connection": conn, "skills": names})
304
302
  return 0
305
303
  if sub == "show":
306
304
  zone_path = zone_skills_dir(conn) / f"{args.name}.md"
307
- global_path = workspace_dir() / "skills" / f"{args.name}.md"
305
+ meta_path = zone_skills_dir("meta") / f"{args.name}.md"
308
306
  if zone_path.is_file():
309
307
  print(zone_path.read_text(encoding="utf-8"))
310
- elif global_path.is_file():
311
- print(global_path.read_text(encoding="utf-8"))
308
+ elif meta_path.is_file():
309
+ print(meta_path.read_text(encoding="utf-8"))
312
310
  else:
313
311
  print(
314
- f"error: no skill named {args.name!r} in zone {conn!r} or global",
312
+ f"error: no skill named {args.name!r} in zone {conn!r} or zones/meta",
315
313
  file=sys.stderr,
316
314
  )
317
315
  return 2
@@ -331,8 +329,13 @@ def _package_root_dir() -> Path:
331
329
 
332
330
 
333
331
  def _shipped_skills_dir() -> Path:
334
- """Shipped preset skills: <root>/agent-workspace/skills/."""
335
- return _package_root_dir() / "agent-workspace" / "skills"
332
+ """Shipped preset (cross-DSN) skills source: zones/meta/skills/.
333
+
334
+ `sql-harness init` copies every .md under this dir into the runtime
335
+ workspace's zones/meta/skills/ so `apply_skill()` can find them as
336
+ fallback from any active zone.
337
+ """
338
+ return _package_root_dir() / "agent-workspace" / "zones" / "meta" / "skills"
336
339
 
337
340
 
338
341
  def _packaged_skill_body() -> str | None:
@@ -487,9 +490,12 @@ def cmd_init(_args, _harness: SqlHarness) -> int:
487
490
  else:
488
491
  _emit({"path": str(path), "existed": True})
489
492
 
490
- target_skills = workspace_dir() / "skills"
491
- ensure_private_dir(target_skills)
492
- copied = _copy_shipped_skills(target_skills)
493
+ # Shipped cross-DSN strategy skills land in zones/meta/skills/ (decision P:
494
+ # collapsed the legacy global layer into a regular zone so apply_skill()
495
+ # has a single shape: zone or meta).
496
+ target_meta = zone_skills_dir("meta")
497
+ ensure_private_dir(target_meta)
498
+ copied = _copy_shipped_skills(target_meta)
493
499
  if copied:
494
500
  print(
495
501
  f"installed {len(copied)} preset skills: {', '.join(copied)}",
@@ -655,7 +661,7 @@ def cmd_paths(args, harness: SqlHarness) -> int:
655
661
  _path_record("config_dir", P.config_dir()),
656
662
  _path_record("config_file", P.config_file()),
657
663
  _path_record("agent_workspace", P.workspace_dir()),
658
- _path_record("global_skills", P.workspace_dir() / "skills"),
664
+ _path_record("meta_skills", P.zone_skills_dir("meta")),
659
665
  _path_record("runtime_dir", P.runtime_dir()),
660
666
  _path_record("tmp_dir", P.tmp_dir()),
661
667
  _path_record("package_root", _package_root_dir()),
@@ -697,7 +703,7 @@ def _resolve_open_path(target: str, conn: str | None) -> Path:
697
703
  "home": P.home_dir(),
698
704
  "config": P.config_dir(),
699
705
  "workspace": P.workspace_dir(),
700
- "skills": P.workspace_dir() / "skills",
706
+ "skills": P.zone_skills_dir("meta"),
701
707
  "runtime": P.runtime_dir(),
702
708
  "tmp": P.tmp_dir(),
703
709
  "package": _package_root_dir(),
@@ -311,35 +311,34 @@ def with_transaction():
311
311
  # --- Skill / script helpers (per-DSN zone) ---------------------------------
312
312
 
313
313
  def apply_skill(name: str) -> str:
314
- """Read a skill markdown from the ACTIVE connection's zone (or global fallback).
314
+ """Read a skill markdown from the ACTIVE zone, with zones/meta as fallback.
315
315
 
316
- Mirrors browser-harness's per-domain skill lookup. Zone wins if present;
317
- falls back to the global agent-workspace/skills/ directory so shipped
318
- skills (pool.md, workspace.md) remain reachable.
316
+ Two-layer fallback (decision P): the active zone wins if it has the skill;
317
+ otherwise zones/meta/skills/ (cross-DSN strategy knowledge, populated by
318
+ `sql-harness init`) supplies the answer. This collapses the legacy
319
+ global `agent-workspace/skills/` layer into a regular zone so the lookup
320
+ is single-shape: zone or meta.
319
321
  """
320
322
  conn = _require_connection()
321
323
  zone_path = paths.zone_skills_dir(conn) / f"{name}.md"
322
324
  if zone_path.is_file():
323
325
  return zone_path.read_text(encoding="utf-8")
324
- # Fallback to global skills dir (shipped examples).
325
- global_path = paths.workspace_dir() / "skills" / f"{name}.md"
326
- if global_path.is_file():
327
- return global_path.read_text(encoding="utf-8")
326
+ meta_path = paths.zone_skills_dir("meta") / f"{name}.md"
327
+ if meta_path.is_file():
328
+ return meta_path.read_text(encoding="utf-8")
328
329
  raise FileNotFoundError(
329
- f"no skill named {name!r} in zone {conn!r} or global {paths.workspace_dir() / 'skills'}"
330
+ f"no skill named {name!r} in zone {conn!r} or zones/meta"
330
331
  )
331
332
 
332
333
 
333
334
  def list_skills() -> list[str]:
334
- """List skills in the ACTIVE connection's zone UNION the global skills dir."""
335
- zone = set()
336
- d = paths.zone_skills_dir(_require_connection())
337
- if d.is_dir():
338
- zone.update(p.stem for p in d.glob("*.md"))
339
- global_d = paths.workspace_dir() / "skills"
340
- if global_d.is_dir():
341
- zone.update(p.stem for p in global_d.glob("*.md"))
342
- return sorted(zone)
335
+ """List skills in the ACTIVE zone UNION zones/meta (cross-DSN fallback)."""
336
+ conn = _require_connection()
337
+ seen: set[str] = set()
338
+ for d in (paths.zone_skills_dir(conn), paths.zone_skills_dir("meta")):
339
+ if d.is_dir():
340
+ seen.update(p.stem for p in d.glob("*.md"))
341
+ return sorted(seen)
343
342
 
344
343
 
345
344
  def list_scripts() -> list[str]:
@@ -47,8 +47,8 @@ def workspace_dir() -> Path:
47
47
  - helpers.py per-connection helpers (merged over the shared base)
48
48
  - scripts/<name>.py saved heredocs for this connection
49
49
  - skills/<name>.md per-connection skill knowledge
50
+ - zones/meta/skills/ cross-DSN strategy skills (apply_skill fallback)
50
51
  - scripts/ (legacy global scripts, pre-zone)
51
- - skills/ (legacy global skills, pre-zone)
52
52
  """
53
53
  override = os.environ.get("BH_SQL_AGENT_WORKSPACE")
54
54
  return Path(override).expanduser() if override else home_dir() / "agent-workspace"
@@ -190,7 +190,7 @@ def test_paths_lists_global_dirs(tmp_path: Path) -> None:
190
190
  data = json.loads(p.stdout)
191
191
  names = {r["name"] for r in data["global"]}
192
192
  # The key folders a user wants to open must all be present.
193
- assert {"home", "config_file", "agent_workspace", "global_skills",
193
+ assert {"home", "config_file", "agent_workspace", "meta_skills",
194
194
  "package_root", "package_source"} <= names
195
195
  # config_file should point inside the isolated BH_SQL_HOME.
196
196
  cf = next(r for r in data["global"] if r["name"] == "config_file")
@@ -63,6 +63,10 @@ def test_close_workspace_unknown_is_noop() -> None:
63
63
 
64
64
 
65
65
  def test_skills_no_dir(tmp_path, monkeypatch) -> None:
66
+ """SqlHarness.apply_skill/list_skills is the legacy class-level API
67
+ (decision C in the P-plan): it still reads workspace_dir()/skills/ unchanged.
68
+ The canonical entry is the module-level helpers.py:apply_skill, which uses
69
+ the zones/meta fallback. See test_helpers.py for the new behavior."""
66
70
  monkeypatch.setenv("BH_SQL_AGENT_WORKSPACE", str(tmp_path / "isolated_ws"))
67
71
  h = SqlHarness(_sqlite_config())
68
72
  assert h.list_skills() == []
@@ -446,7 +446,7 @@ wheels = [
446
446
 
447
447
  [[package]]
448
448
  name = "sql-harness"
449
- version = "0.2.0"
449
+ version = "0.2.2"
450
450
  source = { editable = "." }
451
451
  dependencies = [
452
452
  { name = "paramiko" },
@@ -1,26 +0,0 @@
1
- # agent-workspace/skills — strategy layer
2
-
3
- Strategy-altitude notes for sql-harness: **when** and **why** to reach for a
4
- pattern. The **how** (helpers, commands, gotchas) lives one level down in
5
- `interaction-skills/` — these docs point there, they don't duplicate it.
6
-
7
- ## Files in this folder
8
-
9
- - `pool.md` — ships the `[pool_defaults]` block + recycle-timing rule; points at the pooling mechanic for sizing
10
- - `workspace.md` — one connection = one workspace; isolation, lifecycle, multi-DB, cross-workspace-transaction warning
11
- - `ssh.md` — when to reach for `driver = "ssh"` vs a DB workspace; auth reference; host-key-trust decision
12
- - `docker-deploy.md` — when to deploy docker-compose over SSH; service catalog; `~/sql-harness/<svc>/` convention; lifecycle decisions
13
-
14
- ## When to read which
15
-
16
- - Queries stall / connections go stale → `pool.md` (then the pooling mechanic)
17
- - Juggling >1 database, or "can I share a connection?" → `workspace.md`
18
- - Need to run shell / move files on a remote box → `ssh.md`
19
- - Standing up mysql / redis / coturn / nginx on a remote host → `docker-deploy.md`
20
-
21
- ## When to skip
22
-
23
- Most pool / workspace / ssh / docker tuning is **mechanic work** — read
24
- `interaction-skills/` first (`pooling.md`, `ssh.md`, `docker-via-ssh.md`,
25
- `transactions.md`). Come here only when a mechanic doc points at strategy, or
26
- when you're deciding *whether* to use a pattern rather than *how* to run it.
@@ -1,85 +0,0 @@
1
- # Docker-via-SSH deployment — strategy
2
-
3
- > **Strategy layer** for deploying docker-compose services to remote hosts via
4
- > sql-harness SSH. For the **mechanic** (upload + `docker compose up` + verify),
5
- > see `interaction-skills/docker-via-ssh.md`. This doc is about **when**, **why**,
6
- > and **which service** — the convention, the catalog, the lifecycle decisions.
7
-
8
- ## When to use this pattern
9
-
10
- Use `docker-via-ssh` when:
11
-
12
- - You have **1–N remote hosts** (Aliyun ECS, a VPS, a home lab box) reachable by SSH
13
- - You want **standard services** (mysql / postgres / redis / coturn / nginx / grafana) running, self-contained
14
- - You want **re-deployable / scriptable** deploys (not manual `docker run` you'll forget)
15
- - You **don't** need k8s-scale orchestration (no auto-scaling, no multi-node clustering)
16
-
17
- Don't use it when:
18
-
19
- - You need **multi-node clustering** (Hadoop / HBase across N hosts) → k8s / Nomad
20
- - The service needs **bare-metal perf** (GPU passthrough, hugepages) → systemd unit
21
- - You're on a **managed platform** (Aliyun RDS, Redis Cloud) → use the managed one
22
-
23
- ## The convention: `~/sql-harness/<service>/` on the remote
24
-
25
- Every deployed service lives in its own dir under `~/sql-harness/<service>/` **on
26
- the remote host** — compose + config + data + logs, all with relative mounts
27
- (`./data`, not absolute paths). The mechanic doc shows the layout tree and the
28
- relative-mount rationale in full; the strategy payoff is:
29
-
30
- | Property | How the convention delivers it |
31
- |---|---|
32
- | **Cohesion** | A service's compose + config + data + logs are all in one dir |
33
- | **Backup** | `tar czf coturn.tgz ~/sql-harness/coturn/` — one command, complete |
34
- | **Teardown** | `docker compose down && rm -rf ~/sql-harness/<svc>` — clean |
35
- | **Upgrade** | re-upload compose → `docker compose up -d` (rolling, keeps data) |
36
- | **Discovery** | `ls ~/sql-harness/` shows what's deployed |
37
- | **Isolation** | services don't share mounts/networks unless explicitly composed together |
38
- | **Portability** | relative mounts follow the dir — move or migrate hosts without editing paths |
39
-
40
- ## Service catalog (shipped templates in `docker-services/`)
41
-
42
- | Service | Image | Ports | Use case |
43
- |---|---|---|---|
44
- | `coturn` | `coturn/coturn` | 3478, 5349, 49152-65535/udp | WebRTC TURN/STUN (P2P hole-punching) |
45
- | `mysql` | `mysql:8.0` | 3306 | Relational DB (sql-harness `mysql` driver connects here) |
46
- | `postgresql` | `postgres:16` | 5432 | Relational DB (sql-harness `postgres` driver connects here) |
47
- | `redis` | `redis:7-alpine` | 6379 | Cache / KV (sql-harness `redis` driver — when implemented) |
48
- | `nginx` | `nginx:alpine` | 80, 443 | Reverse proxy / static hosting |
49
- | `rabbitmq` | `rabbitmq:3-management` | 5672, 15672 | Message queue |
50
- | `cloud-grafana` | `grafana/grafana` + `prom/prometheus` | 3000, 9090 | Monitoring stack |
51
-
52
- Each template in `docker-services/<svc>/` ships a `docker-compose.yml` + config
53
- files, all with relative mounts. Pick one → the mechanic's 4-step loop uploads
54
- and starts it. Pair a deployed DB service with a matching **DB workspace**
55
- (`driver = "mysql"`/`postgres`) to run SQL against it — SSH-level container
56
- management + DB-level queries on the same host is the typical diagnostic loop.
57
-
58
- ## Lifecycle decisions
59
-
60
- | Question | Answer |
61
- |---|---|
62
- | New service? | pick template → upload → `up -d` (4-step loop in mechanic doc) |
63
- | Image update? | edit compose tag → re-upload → `up -d` (keeps data) |
64
- | Config change? | edit conf → re-upload → `restart` (or `up -d` if compose changed) |
65
- | Host migration? | `tar` the service dir → scp to new host → `up -d` |
66
- | Out of disk? | `docker system prune -af` (careful) + check service `data/` growth |
67
- | Service flapping? | `docker logs -f` → `docker compose ps` → healthcheck output |
68
- | Stop paying for it? | `down -v` + `rm -rf ~/sql-harness/<svc>` + snapshot data first |
69
-
70
- ## Gotchas (strategy-level)
71
-
72
- - **One host = one `~/sql-harness/` root**. Multi-host deploys loop the deploy per
73
- host — one sql-harness SSH workspace per host.
74
- - **Don't mount the SAME host port from two services** — mysql + postgres coexist
75
- fine on their defaults, but two mysql instances need port remapping.
76
- - **Healthy ≠ reachable by the DB workspace**: if a container isn't healthy,
77
- `sql-harness test <db-workspace>` fails confusingly — check `docker compose ps`
78
- before assuming a sql-harness bug. (Per-service startup gotchas live with the
79
- mechanic and `docker-services/<svc>/README.md`.)
80
-
81
- ## See also
82
-
83
- - `interaction-skills/docker-via-ssh.md` — the mechanic (layout tree, relative-mount
84
- rationale, 4-step deploy loop, per-op lifecycle commands, deploy gotchas)
85
- - `docker-services/<svc>/` — shipped templates (and `<svc>/README.md` service gotchas)
@@ -1,70 +0,0 @@
1
- # SSH workspaces — when to use the SSH driver
2
-
3
- > **Strategy guide.** For the mechanics (helper calls, CLI subcommands, SFTP /
4
- > timeout / quoting gotchas), see `interaction-skills/ssh.md`.
5
-
6
- ## When to reach for `driver = "ssh"`
7
-
8
- Use an SSH workspace to **operate on a remote host** (not a database). It reuses
9
- the same `use_workspace("name")` abstraction as DB workspaces, but opens a
10
- paramiko `SSHClient` + SFTP instead of a SQLAlchemy engine — so you get shell +
11
- file ops with the same ergonomics.
12
-
13
- Reach for it to: run ad-hoc shell commands (`ps`, `systemctl`, `cat /var/log/...`);
14
- do a one-shot deploy / restart / log inspection without a full Ansible playbook;
15
- upload-then-run a script (`ssh_run_script`); pull a log/artifact down for analysis;
16
- or pair with a DB workspace (shell check, then `query(...)`).
17
-
18
- Don't use it for: heavy interactive shells (no TUI/scrollback/resize — use
19
- `term-cli` from `lab.subprocess`); long-running daemons (the channel is
20
- short-lived — use systemd / supervisord); or anything that must survive the
21
- sql-harness process exit (the connection closes on exit).
22
-
23
- ## Auth reference
24
-
25
- Configure in `connections.toml` (`driver = "ssh"`, `url = "ssh://user@host:port"`).
26
- Pick an auth strategy:
27
-
28
- | Strategy | When | Example URL |
29
- |---|---|---|
30
- | SSH key (preferred) | CI / dev boxes with `~/.ssh/id_*` | `ssh://user@host` (auto-resolves key) |
31
- | Explicit key path | Specific key per connection | `ssh://user@host?key=/opt/keys/prod.pem` |
32
- | Password | Quick lab use; NOT for prod | `ssh://user:pw@host` (works but key is better) |
33
- | `ssh+password://` | Force password (no key lookup) | `ssh+password://user:pw@host` |
34
- | `ssh+key://` | Force key-only auth (no password fallback) | `ssh+key://user@host?key=/path` |
35
-
36
- Key resolution order (first hit wins): (1) `?key=...` in the URL, (2) `$BH_SSH_KEY`
37
- env var, (3) `~/.ssh/id_ed25519` → `id_rsa` → `id_ecdsa`, (4) password from URL or
38
- `$BH_SSH_PASSWORD`.
39
-
40
- ## Workspace lifecycle parity
41
-
42
- SSH workspaces follow the **same** lazy-open / cached / dispose pattern as DB
43
- workspaces: `use_workspace("prod-app")` opens TCP+SSH+shell+SFTP and caches it;
44
- re-opening returns the cached handle for free; process exit / `dispose()` /
45
- `close_workspace(name)` tears it down.
46
-
47
- ## Combining with DB workspaces
48
-
49
- SSH and DB workspaces are independent — keep one of each open at once. The typical
50
- diagnostic flow: shell-level process check, then in-process DB check.
51
-
52
- ```python
53
- ssh_use_workspace("prod-app")
54
- print(ssh_exec("ps aux | grep postgres | head -3")["stdout"])
55
-
56
- use_workspace("prod_pg")
57
- print(query("SELECT count(*) FROM pg_stat_activity")["count"])
58
- ```
59
-
60
- ## Safety — the host-key-trust decision
61
-
62
- The driver trusts host keys on first connect (`AutoAddPolicy()`, like
63
- `StrictHostKeyChecking=accept-new`). Fine for trusted networks; for hostile ones,
64
- harden this in a subclass. Also prefer a non-root user, and set an explicit
65
- `timeout=` for long commands. (Connection pooling, shell-vs-exec, path handling,
66
- non-ASCII decoding and other operational gotchas live in the mechanic doc.)
67
-
68
- ## See also
69
-
70
- - `interaction-skills/ssh.md` — the mechanic (helpers, CLI, SFTP/timeout/quoting gotchas)
File without changes
File without changes
File without changes
File without changes
File without changes