ph-core 0.1.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 (226) hide show
  1. ph_core-0.1.0/.gitignore +28 -0
  2. ph_core-0.1.0/LICENSE +21 -0
  3. ph_core-0.1.0/PKG-INFO +239 -0
  4. ph_core-0.1.0/README.md +205 -0
  5. ph_core-0.1.0/pyproject.toml +146 -0
  6. ph_core-0.1.0/src/ph/__init__.py +9 -0
  7. ph_core-0.1.0/src/ph/agent/__init__.py +33 -0
  8. ph_core-0.1.0/src/ph/agent/inbox.py +215 -0
  9. ph_core-0.1.0/src/ph/agent/registry.py +186 -0
  10. ph_core-0.1.0/src/ph/agent/types.py +214 -0
  11. ph_core-0.1.0/src/ph/agent_loop/__init__.py +35 -0
  12. ph_core-0.1.0/src/ph/agent_loop/driver.py +624 -0
  13. ph_core-0.1.0/src/ph/agent_loop/invariant.py +83 -0
  14. ph_core-0.1.0/src/ph/bundles/__init__.py +63 -0
  15. ph_core-0.1.0/src/ph/bundles/base.yaml +335 -0
  16. ph_core-0.1.0/src/ph/bundles/headless.yaml +10 -0
  17. ph_core-0.1.0/src/ph/cancel.py +96 -0
  18. ph_core-0.1.0/src/ph/commands/__init__.py +15 -0
  19. ph_core-0.1.0/src/ph/commands/autonomous.py +231 -0
  20. ph_core-0.1.0/src/ph/commands/revert.py +192 -0
  21. ph_core-0.1.0/src/ph/commands/sandbox.py +359 -0
  22. ph_core-0.1.0/src/ph/commands/workspaces.py +371 -0
  23. ph_core-0.1.0/src/ph/cordis/__init__.py +114 -0
  24. ph_core-0.1.0/src/ph/cordis/catalog.py +170 -0
  25. ph_core-0.1.0/src/ph/cordis/context.py +1716 -0
  26. ph_core-0.1.0/src/ph/cordis/errors.py +69 -0
  27. ph_core-0.1.0/src/ph/cordis/events.py +122 -0
  28. ph_core-0.1.0/src/ph/cordis/key.py +57 -0
  29. ph_core-0.1.0/src/ph/cordis/loader.py +775 -0
  30. ph_core-0.1.0/src/ph/cordis/plugin.py +144 -0
  31. ph_core-0.1.0/src/ph/json.py +379 -0
  32. ph_core-0.1.0/src/ph/keys.py +163 -0
  33. ph_core-0.1.0/src/ph/lingering.py +404 -0
  34. ph_core-0.1.0/src/ph/llm/__init__.py +105 -0
  35. ph_core-0.1.0/src/ph/llm/adapter.py +314 -0
  36. ph_core-0.1.0/src/ph/llm/assembler.py +206 -0
  37. ph_core-0.1.0/src/ph/llm/dimensions.py +163 -0
  38. ph_core-0.1.0/src/ph/llm/fake.py +124 -0
  39. ph_core-0.1.0/src/ph/llm/media.py +317 -0
  40. ph_core-0.1.0/src/ph/llm/replay.py +188 -0
  41. ph_core-0.1.0/src/ph/llm/retry.py +124 -0
  42. ph_core-0.1.0/src/ph/llm/structured.py +249 -0
  43. ph_core-0.1.0/src/ph/llm/types.py +689 -0
  44. ph_core-0.1.0/src/ph/orphans.py +309 -0
  45. ph_core-0.1.0/src/ph/paths.py +350 -0
  46. ph_core-0.1.0/src/ph/persistence/__init__.py +37 -0
  47. ph_core-0.1.0/src/ph/persistence/checkpoint_policy.py +83 -0
  48. ph_core-0.1.0/src/ph/persistence/families.py +86 -0
  49. ph_core-0.1.0/src/ph/persistence/jsonl.py +455 -0
  50. ph_core-0.1.0/src/ph/persistence/lease.py +78 -0
  51. ph_core-0.1.0/src/ph/persistence/lineage.py +285 -0
  52. ph_core-0.1.0/src/ph/persistence/protocol.py +299 -0
  53. ph_core-0.1.0/src/ph/persistence/repair.py +260 -0
  54. ph_core-0.1.0/src/ph/persistence/turso.py +414 -0
  55. ph_core-0.1.0/src/ph/py.typed +0 -0
  56. ph_core-0.1.0/src/ph/resources.py +184 -0
  57. ph_core-0.1.0/src/ph/seams/__init__.py +5 -0
  58. ph_core-0.1.0/src/ph/seams/_names.py +51 -0
  59. ph_core-0.1.0/src/ph/seams/_registry.py +222 -0
  60. ph_core-0.1.0/src/ph/seams/_restriction.py +42 -0
  61. ph_core-0.1.0/src/ph/seams/approval.py +475 -0
  62. ph_core-0.1.0/src/ph/seams/attachments.py +596 -0
  63. ph_core-0.1.0/src/ph/seams/changes.py +418 -0
  64. ph_core-0.1.0/src/ph/seams/code_runtime.py +324 -0
  65. ph_core-0.1.0/src/ph/seams/code_runtime_stub.py +89 -0
  66. ph_core-0.1.0/src/ph/seams/commands.py +282 -0
  67. ph_core-0.1.0/src/ph/seams/compaction.py +292 -0
  68. ph_core-0.1.0/src/ph/seams/containment.py +316 -0
  69. ph_core-0.1.0/src/ph/seams/credentials.py +112 -0
  70. ph_core-0.1.0/src/ph/seams/diagnostics.py +160 -0
  71. ph_core-0.1.0/src/ph/seams/fs.py +1093 -0
  72. ph_core-0.1.0/src/ph/seams/goals.py +365 -0
  73. ph_core-0.1.0/src/ph/seams/invariants.py +284 -0
  74. ph_core-0.1.0/src/ph/seams/jobs.py +405 -0
  75. ph_core-0.1.0/src/ph/seams/permission_presets.py +184 -0
  76. ph_core-0.1.0/src/ph/seams/sandbox.py +870 -0
  77. ph_core-0.1.0/src/ph/seams/sandbox_allow.py +82 -0
  78. ph_core-0.1.0/src/ph/seams/sandbox_egress.py +467 -0
  79. ph_core-0.1.0/src/ph/seams/sandbox_local.py +886 -0
  80. ph_core-0.1.0/src/ph/seams/schedule.py +470 -0
  81. ph_core-0.1.0/src/ph/seams/schedule_index.py +174 -0
  82. ph_core-0.1.0/src/ph/seams/scope_invariant.py +157 -0
  83. ph_core-0.1.0/src/ph/seams/settings.py +95 -0
  84. ph_core-0.1.0/src/ph/seams/shell.py +161 -0
  85. ph_core-0.1.0/src/ph/seams/skills.py +978 -0
  86. ph_core-0.1.0/src/ph/seams/skills_invariant.py +59 -0
  87. ph_core-0.1.0/src/ph/seams/spill.py +263 -0
  88. ph_core-0.1.0/src/ph/seams/subagents.py +1492 -0
  89. ph_core-0.1.0/src/ph/seams/subprocess.py +592 -0
  90. ph_core-0.1.0/src/ph/seams/telemetry.py +249 -0
  91. ph_core-0.1.0/src/ph/seams/telemetry_otel.py +173 -0
  92. ph_core-0.1.0/src/ph/seams/token_meter.py +347 -0
  93. ph_core-0.1.0/src/ph/seams/topology.py +44 -0
  94. ph_core-0.1.0/src/ph/seams/tui_screens.py +242 -0
  95. ph_core-0.1.0/src/ph/seams/tui_status.py +204 -0
  96. ph_core-0.1.0/src/ph/seams/uploads.py +345 -0
  97. ph_core-0.1.0/src/ph/seams/user_questions.py +266 -0
  98. ph_core-0.1.0/src/ph/seams/workspace.py +2161 -0
  99. ph_core-0.1.0/src/ph/seams/workspace_agentfs.py +721 -0
  100. ph_core-0.1.0/src/ph/seams/workspace_git.py +917 -0
  101. ph_core-0.1.0/src/ph/seams/workspace_jj.py +1045 -0
  102. ph_core-0.1.0/src/ph/seams/workspace_provision.py +345 -0
  103. ph_core-0.1.0/src/ph/seams/workspace_scratch.py +131 -0
  104. ph_core-0.1.0/src/ph/selectors.py +168 -0
  105. ph_core-0.1.0/src/ph/session/__init__.py +92 -0
  106. ph_core-0.1.0/src/ph/session/derive.py +67 -0
  107. ph_core-0.1.0/src/ph/session/events.py +224 -0
  108. ph_core-0.1.0/src/ph/session/folds.py +147 -0
  109. ph_core-0.1.0/src/ph/session/invariant.py +46 -0
  110. ph_core-0.1.0/src/ph/session/json.py +198 -0
  111. ph_core-0.1.0/src/ph/session/known_event_types.py +370 -0
  112. ph_core-0.1.0/src/ph/session/request_header.py +141 -0
  113. ph_core-0.1.0/src/ph/session/session.py +672 -0
  114. ph_core-0.1.0/src/ph/session/store.py +434 -0
  115. ph_core-0.1.0/src/ph/session/surface.py +349 -0
  116. ph_core-0.1.0/src/ph/system_prompt/__init__.py +25 -0
  117. ph_core-0.1.0/src/ph/system_prompt/assembly.py +383 -0
  118. ph_core-0.1.0/src/ph/system_prompt/memory.py +201 -0
  119. ph_core-0.1.0/src/ph/testing/__init__.py +159 -0
  120. ph_core-0.1.0/src/ph/testing/anthropic_wire.py +66 -0
  121. ph_core-0.1.0/src/ph/testing/builders.py +596 -0
  122. ph_core-0.1.0/src/ph/testing/diagnostics.py +23 -0
  123. ph_core-0.1.0/src/ph/testing/folds.py +346 -0
  124. ph_core-0.1.0/src/ph/testing/git.py +91 -0
  125. ph_core-0.1.0/src/ph/testing/jj.py +82 -0
  126. ph_core-0.1.0/src/ph/testing/skills.py +58 -0
  127. ph_core-0.1.0/src/ph/testing/stub_sandbox.py +38 -0
  128. ph_core-0.1.0/src/ph/testing/stub_subagent.py +96 -0
  129. ph_core-0.1.0/src/ph/testing/stub_workspace.py +156 -0
  130. ph_core-0.1.0/src/ph/text.py +123 -0
  131. ph_core-0.1.0/src/ph/tools/__init__.py +90 -0
  132. ph_core-0.1.0/src/ph/tools/batch.py +294 -0
  133. ph_core-0.1.0/src/ph/tools/builtin/__init__.py +5 -0
  134. ph_core-0.1.0/src/ph/tools/builtin/ask_user.py +128 -0
  135. ph_core-0.1.0/src/ph/tools/builtin/attach_tool.py +197 -0
  136. ph_core-0.1.0/src/ph/tools/builtin/bash_tool.py +127 -0
  137. ph_core-0.1.0/src/ph/tools/builtin/fs_tools.py +279 -0
  138. ph_core-0.1.0/src/ph/tools/builtin/subagent_task.py +224 -0
  139. ph_core-0.1.0/src/ph/tools/code_mode.py +554 -0
  140. ph_core-0.1.0/src/ph/tools/definition.py +739 -0
  141. ph_core-0.1.0/src/ph/tools/errors.py +125 -0
  142. ph_core-0.1.0/src/ph/tools/invariant.py +58 -0
  143. ph_core-0.1.0/src/ph/tools/json_schema.py +296 -0
  144. ph_core-0.1.0/src/ph/tools/presentation.py +199 -0
  145. ph_core-0.1.0/src/ph/tools/prompt.py +29 -0
  146. ph_core-0.1.0/src/ph/tools/registry.py +1167 -0
  147. ph_core-0.1.0/src/ph/tools/sdk.py +133 -0
  148. ph_core-0.1.0/src/ph/tools/timeout.py +57 -0
  149. ph_core-0.1.0/src/ph/wire.py +236 -0
  150. ph_core-0.1.0/tests/test_agent_loop.py +425 -0
  151. ph_core-0.1.0/tests/test_ask_user.py +281 -0
  152. ph_core-0.1.0/tests/test_attach_tool.py +251 -0
  153. ph_core-0.1.0/tests/test_attachments.py +243 -0
  154. ph_core-0.1.0/tests/test_attachments_gc.py +292 -0
  155. ph_core-0.1.0/tests/test_catalog.py +130 -0
  156. ph_core-0.1.0/tests/test_changes.py +444 -0
  157. ph_core-0.1.0/tests/test_code_mode.py +676 -0
  158. ph_core-0.1.0/tests/test_commands_sandbox.py +185 -0
  159. ph_core-0.1.0/tests/test_commands_workspaces.py +333 -0
  160. ph_core-0.1.0/tests/test_containment.py +195 -0
  161. ph_core-0.1.0/tests/test_containment_ladder.py +188 -0
  162. ph_core-0.1.0/tests/test_cordis_context.py +555 -0
  163. ph_core-0.1.0/tests/test_cordis_dispatch.py +270 -0
  164. ph_core-0.1.0/tests/test_cordis_events.py +98 -0
  165. ph_core-0.1.0/tests/test_cordis_loader.py +610 -0
  166. ph_core-0.1.0/tests/test_cordis_plugin.py +219 -0
  167. ph_core-0.1.0/tests/test_derive.py +141 -0
  168. ph_core-0.1.0/tests/test_diagnostics.py +277 -0
  169. ph_core-0.1.0/tests/test_dimensions.py +215 -0
  170. ph_core-0.1.0/tests/test_docs_seams.py +197 -0
  171. ph_core-0.1.0/tests/test_exhaustive_dispatch.py +147 -0
  172. ph_core-0.1.0/tests/test_fold_laws.py +318 -0
  173. ph_core-0.1.0/tests/test_fork.py +323 -0
  174. ph_core-0.1.0/tests/test_fs.py +736 -0
  175. ph_core-0.1.0/tests/test_goals.py +297 -0
  176. ph_core-0.1.0/tests/test_invariants.py +631 -0
  177. ph_core-0.1.0/tests/test_json.py +285 -0
  178. ph_core-0.1.0/tests/test_json_schema.py +352 -0
  179. ph_core-0.1.0/tests/test_keys.py +144 -0
  180. ph_core-0.1.0/tests/test_layering.py +70 -0
  181. ph_core-0.1.0/tests/test_lingering.py +253 -0
  182. ph_core-0.1.0/tests/test_llm.py +198 -0
  183. ph_core-0.1.0/tests/test_loop_policies.py +289 -0
  184. ph_core-0.1.0/tests/test_media_degrade.py +283 -0
  185. ph_core-0.1.0/tests/test_memory.py +231 -0
  186. ph_core-0.1.0/tests/test_orphans.py +248 -0
  187. ph_core-0.1.0/tests/test_paths.py +378 -0
  188. ph_core-0.1.0/tests/test_persistence.py +344 -0
  189. ph_core-0.1.0/tests/test_persistence_backends.py +1062 -0
  190. ph_core-0.1.0/tests/test_prefix_stability.py +243 -0
  191. ph_core-0.1.0/tests/test_registration_ownership.py +1679 -0
  192. ph_core-0.1.0/tests/test_repair.py +485 -0
  193. ph_core-0.1.0/tests/test_resources.py +378 -0
  194. ph_core-0.1.0/tests/test_sandbox_allow.py +426 -0
  195. ph_core-0.1.0/tests/test_sandbox_egress.py +400 -0
  196. ph_core-0.1.0/tests/test_sandbox_local.py +659 -0
  197. ph_core-0.1.0/tests/test_schedule.py +398 -0
  198. ph_core-0.1.0/tests/test_sdk.py +247 -0
  199. ph_core-0.1.0/tests/test_seams.py +1336 -0
  200. ph_core-0.1.0/tests/test_selectors.py +295 -0
  201. ph_core-0.1.0/tests/test_session_admit.py +126 -0
  202. ph_core-0.1.0/tests/test_session_append.py +355 -0
  203. ph_core-0.1.0/tests/test_session_event.py +125 -0
  204. ph_core-0.1.0/tests/test_skills_progressive.py +603 -0
  205. ph_core-0.1.0/tests/test_spill_sweep.py +150 -0
  206. ph_core-0.1.0/tests/test_structured.py +264 -0
  207. ph_core-0.1.0/tests/test_subagent_grant.py +665 -0
  208. ph_core-0.1.0/tests/test_subagent_task.py +164 -0
  209. ph_core-0.1.0/tests/test_surface.py +257 -0
  210. ph_core-0.1.0/tests/test_telemetry.py +107 -0
  211. ph_core-0.1.0/tests/test_telemetry_otel.py +133 -0
  212. ph_core-0.1.0/tests/test_text.py +74 -0
  213. ph_core-0.1.0/tests/test_tools_batch.py +277 -0
  214. ph_core-0.1.0/tests/test_tools_pipeline.py +466 -0
  215. ph_core-0.1.0/tests/test_tools_registry.py +195 -0
  216. ph_core-0.1.0/tests/test_wire_forms.py +178 -0
  217. ph_core-0.1.0/tests/test_workspace.py +625 -0
  218. ph_core-0.1.0/tests/test_workspace_agentfs.py +631 -0
  219. ph_core-0.1.0/tests/test_workspace_checkpoint.py +477 -0
  220. ph_core-0.1.0/tests/test_workspace_git.py +931 -0
  221. ph_core-0.1.0/tests/test_workspace_jj.py +1180 -0
  222. ph_core-0.1.0/tests/test_workspace_lifecycle.py +404 -0
  223. ph_core-0.1.0/tests/test_workspace_provision.py +351 -0
  224. ph_core-0.1.0/tests/test_workspace_reconcile.py +256 -0
  225. ph_core-0.1.0/tests/test_workspace_retention.py +510 -0
  226. ph_core-0.1.0/tests/test_workspace_scratch.py +242 -0
@@ -0,0 +1,28 @@
1
+ # Build and environment
2
+ .venv/
3
+ dist/
4
+ build/
5
+ *.egg-info/
6
+ __pycache__/
7
+ *.py[cod]
8
+ jjt/
9
+ w2/
10
+
11
+ # Tooling caches
12
+ .pytest_cache/
13
+ .mypy_cache/
14
+ .ruff_cache/
15
+ .coverage
16
+ htmlcov/
17
+ # Dropped at the repo root by pytest-textual-snapshot when a snapshot test
18
+ # fails. The reference snapshots under `__snapshots__/` are the committed
19
+ # expectation; this is the diff viewer for a run that did not match one.
20
+ snapshot_report.html
21
+
22
+ # Reference checkouts of the upstream projects this port reads from. Vendored
23
+ # locally so the plans' citations are verifiable; never part of this repo.
24
+ sources/
25
+
26
+ # Local scratch
27
+ .ph/
28
+ *.local.yaml
ph_core-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Charles Tabor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ph_core-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,239 @@
1
+ Metadata-Version: 2.5
2
+ Name: ph-core
3
+ Version: 0.1.0
4
+ Summary: pH core: the cordis plugin subset, the session log, the LLM vocabulary, and the agent loop.
5
+ Project-URL: Homepage, https://github.com/chastabor/pH
6
+ Project-URL: Repository, https://github.com/chastabor/pH
7
+ Project-URL: Documentation, https://github.com/chastabor/pH/blob/main/docs/README.md
8
+ Project-URL: Issues, https://github.com/chastabor/pH/issues
9
+ Author: Charles Tabor
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent,harness,llm,plugin,react,session-log
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Software Development
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.12
24
+ Requires-Dist: anyio<5,>=4.4
25
+ Requires-Dist: croniter>=3.0
26
+ Requires-Dist: filelock>=3.15
27
+ Requires-Dist: pydantic>=2.9
28
+ Requires-Dist: pyturso>=0.7
29
+ Requires-Dist: pyyaml>=6.0
30
+ Provides-Extra: otel
31
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27; extra == 'otel'
32
+ Requires-Dist: opentelemetry-sdk>=1.27; extra == 'otel'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # ph-core
36
+
37
+ *Everything a pH profile is made of: the plugin framework, the append-only log,
38
+ the ReAct loop, the capability seams — and the seventy-four rows that provide
39
+ them.*
40
+
41
+ There is no privileged core to patch (invariant I1). What this distribution
42
+ ships is the *vocabulary* a profile is written in — a context that holds
43
+ services, an event bus, scopes that unwind what they created — plus one row per
44
+ thing pH can do, each addressable by id from a YAML document. The agent loop is
45
+ a row. The session store is a row. The filesystem is a row. So is the tool
46
+ registry that decides whether a tool call is even allowed to happen.
47
+
48
+ ```bash
49
+ phern --profile headless -p "hello" # ph-base + the fake adapter
50
+ phern config --profile base # every knob every row accepts
51
+ phern doctor --profile base # what actually activated
52
+ ```
53
+
54
+ `ph-core` is a dependency of every other package in this workspace and depends
55
+ on none of them.
56
+
57
+ ## What is in here
58
+
59
+ | module | what it owns |
60
+ |---|---|
61
+ | `ph.cordis` | the plugin meta-framework subset (D1): `Context`, service keys, the four event dispatch modes, scopes whose disposal unwinds every registration (I2), and the YAML profile loader |
62
+ | `ph.session` | the append-only event log, the derived model surface (`derive_messages`), the human `transcript()`, and the folds over both |
63
+ | `ph.llm` | the provider-neutral vocabulary, the stream assembler, the `ctx.llm` seam, plus the `fake` and `replay` adapters |
64
+ | `ph.agent` / `ph.agent_loop` | the agent handle and its inbox; the ReAct driver, mounted as a row like anything else |
65
+ | `ph.tools` | the registry, the governed pipeline (`tools/pre-execute`, `tools/post-execute`, guards), the batch scheduler, and Code Mode's transport and generated SDK |
66
+ | `ph.seams` | one module per capability seam: the Protocol, the local provider, and what it refuses |
67
+ | `ph.system_prompt` | prompt assembly — cached `section`s, post-cache `context()` snapshots, and `AGENTS.md` discovery |
68
+ | `ph.persistence` | JSONL and Turso backends, checkpoints, leases, lineage and crash repair |
69
+ | `ph.commands` | the slash commands ph-core itself owns |
70
+ | `ph.bundles` | `base.yaml`, `headless.yaml`, and the `ph.bundles` entry-point group other distributions register into |
71
+ | `ph.paths` | the three roots (`$PH_HOME`, `$PH_CACHE`, `$PH_RUNTIME`) and their resolution rules |
72
+ | `ph.testing` | builders, stubs and fixtures a test stands a profile up with. Nothing shipped imports it |
73
+
74
+ ## The two bundles it ships
75
+
76
+ **`ph-base`** (`src/ph/bundles/base.yaml`) is the shared core of every pH
77
+ profile: the log, the loop, the tool registry, every capability seam with its
78
+ local provider, the built-in tools, durability, resilience and the runtime
79
+ invariant rows. **`ph-headless`** adds one row — the scripted `llm-fake`
80
+ adapter — so a one-shot or a scenario test can script a conversation without
81
+ touching code.
82
+
83
+ Both are *paths*, not entry points, because `ph-app` can import them directly.
84
+ Every other bundle in this workspace is **discovered** through the `ph.bundles`
85
+ entry-point group, which is what lets `ph-app` compose the `rlm` profile without
86
+ depending on `ph-rlm`:
87
+
88
+ ```toml
89
+ [project.entry-points."ph.bundles"]
90
+ rlm = "ph_rlm:BUNDLE"
91
+ ```
92
+
93
+ ## The tools the model gets
94
+
95
+ Registered by rows, so a profile decides which of them exist at all.
96
+
97
+ | row | tools | note |
98
+ |---|---|---|
99
+ | `tool-fs` | `read`, `write`, `edit`, `glob`, `grep` | thin shells over `ctx.fs`, so the policy gates and the workspace root apply to any second editing tool too |
100
+ | `tool-bash` | `bash` | goes through `ctx.shell`, which is what `sandbox-local` confines |
101
+ | `tool-attach` | `attach` | puts an image, audio file, video or PDF from the workspace in front of the model's own eyes. **Registers nothing without an attachment store** |
102
+ | `tool-ask-user` | `ask_user` | **ships disabled**: a question with nobody to answer it spends a turn on nothing. `tui.yaml` arms it |
103
+ | `subagent-task` | `task` | blocking delegation. **Registers nothing until a `ctx.subagents` provider is mounted** — a tool named in every prompt and refused on every call teaches the model a capability the deployment does not have |
104
+ | `skills-progressive` | `skill` | the catalog goes in the prompt; a body only when the model asks for it by name (G9) |
105
+
106
+ ## The commands a person can type
107
+
108
+ These are **commands, not tools**: a person asks the harness directly, it costs
109
+ no model turn, and the log records `command/run`/`command/done` rather than the
110
+ model having decided something the user decided.
111
+
112
+ | command | row | what it does |
113
+ |---|---|---|
114
+ | `/sandbox` | `sandbox-commands` | show what confined commands may reach, and change it without a restart |
115
+ | `/workspaces` | `workspace-commands` | list, export, merge or remove the branches agents left behind in this session |
116
+ | `/revert` | `workspace-revert` | restore this agent's workspace to a per-run checkpoint |
117
+ | `/autonomous` | `autonomous` | work toward a goal until its gates pass or a budget stops it |
118
+
119
+ ## Adjusting it from a profile
120
+
121
+ Three layers, applied in this order — and a patch replaces the targeted row's
122
+ **whole** config rather than merging into it, so a row's effective value is
123
+ always one layer's and readable in one place:
124
+
125
+ 1. the shipped documents — `ph-base`, then whatever the profile layers;
126
+ 2. your overlay, `$PH_HOME/profiles/<name>.yaml`;
127
+ 3. drop-ins pH wrote on your behalf, `$PH_HOME/profiles/<name>.d/*.yaml`, in
128
+ name order (this is where `/sandbox allow …` keeps its decisions);
129
+ 4. `--patch`, this run only, same grammar as a profile document.
130
+
131
+ A patch entry is `{id: …, config: {…}}` to reconfigure, `{id: …, disabled:
132
+ false}` to arm a row a bundle ships off, `{id: …, remove: true}` to drop one, or
133
+ `{insert: [...]}` to add one. **A list in row config is replaced, not merged** —
134
+ an overlay restating one field of a route must restate the whole route entry.
135
+
136
+ ```bash
137
+ phern --patch '{id: fs, config: {root: /tmp/scratch}}' --profile tui --mode tui
138
+ phern --patch '{id: tool-ask-user, disabled: false}' --profile headless -p "..."
139
+ phern --dump-config --profile llama # the composition, before anything runs
140
+ phern config --row containment --profile tui # one row's knobs, defaults and what the profile set
141
+ ```
142
+
143
+ ### The knobs most deployments touch
144
+
145
+ | row | config | default |
146
+ |---|---|---|
147
+ | `agent-loop` | `maxParallelToolCalls` | `10` |
148
+ | `tools` | `mode: native \| code` | `native` |
149
+ | `tools-code-mode` | `maxDispatchesPerRun`, `maxSubagentSpawnsPerRun`, `maxParallelSubCalls` | `256`, `32`, `10` |
150
+ | `fs-local` | `root`, `ignore` | the session cwd |
151
+ | `sandbox-policy` | `defaultMode: read-only \| workspace-write` | `read-only` (`tui.yaml` sets `workspace-write`) |
152
+ | `sandbox-allow` | `paths`, `network.{mode,hosts}` | an allowlist over the package indexes and documentation hosts in `DEFAULT_HOSTS` |
153
+ | `containment` | `tier`, `childTier`, `strict` | **unset** — which is not `advisory`: unset means nobody chose, so a tier provider a profile layered is used |
154
+ | `workspace-lifecycle` | `access`, `provision` | `write` |
155
+ | `jobs-local` | `concurrency` (per job kind) | unset |
156
+ | `llm-retry` | `maxAttempts`, `baseDelayMs`, `maxDelayMs` | `3`, `500`, `20000` |
157
+ | `session-telemetry` | `enabled`, `path` | `enabled: false` in `ph-base` |
158
+ | `subagent-presets` | `presets` | empty — a menu, never a grant: selecting a preset never widens what the parent itself holds |
159
+ | `skills-progressive` | `paths` | **empty on purpose** — scanning a well-known directory would make "install a skill" mean "drop a file somewhere" (I7) |
160
+ | `autonomous` | `maxContinuations`, `maxTurns`, `maxTokens`, `timeoutMs` | `3`, `12`, `80000`, `1800000` |
161
+ | `subprocess-local` | `scrub`, `keep`, `maxOutputBytes` | `8388608` |
162
+ | `tool-attach` | `maxBytes` | `33554432` |
163
+
164
+ **An entry carrying both `id:` and `name:` is a row, not a patch** — that is the
165
+ rule that decides which of the two a document line is. So swapping a *provider*
166
+ is a removal and an insertion, not a rename; consumers never learn which one
167
+ answered either way (I5):
168
+
169
+ ```yaml
170
+ # $PH_HOME/profiles/tui.yaml — keep the log in Turso instead of JSONL
171
+ - id: session-persistence
172
+ remove: true
173
+ - insert:
174
+ - id: session-persistence
175
+ name: session-persistence-turso
176
+ ```
177
+
178
+ Giving the existing id a new `name:` in place looks like it should work and does
179
+ not: it appends a *second* row, and the mount then refuses with
180
+ `service "session_persistence" is already provided`.
181
+
182
+ ## Seams that ship with no provider, deliberately
183
+
184
+ `ph-base` mounts the *definition* of several seams and no backend, because these
185
+ have genuinely different answers per deployment and a harness that shipped one
186
+ would have made the choice for you:
187
+
188
+ - **`code_runtime`** — nothing runs model-written code until `ph-rlm` mounts
189
+ `code-runtime-python` (or a profile mounts `code-runtime-stub`).
190
+ - **`subagents`** — no child-agent provider, which is why `subagent-task`
191
+ registers no tool in `ph-base`.
192
+ - **`compaction`** — the seam records and replaces; *when* and *what to say* are
193
+ `ph-stabilize`'s `compaction-summarize`. Every profile `ph-app` offers layers
194
+ that bundle when it is installed, so a *profile* compacts; `ph-base` composed
195
+ on its own still does not.
196
+ - **`uploads`** — mounted with no uploader; each adapter row registers its own,
197
+ so a profile with no file API sends every byte inline.
198
+
199
+ A profile that layers nothing here simply never compacts, never delegates and
200
+ never runs code. That is the plain harness, and it is a supported posture.
201
+
202
+ ## Limitations, and things that are deliberate
203
+
204
+ - **No Textual, Rich, Typer, `ph_app`, aiohttp, textual-serve or jinja2
205
+ imports** — `tests/test_layering.py` fails the build on any of them. A front
206
+ end is a consumer of this package, never the other way round.
207
+ - **No real model wire ships here.** `llm-fake` and `llm-replay` are for wiring
208
+ work and tests; Anthropic, Google and the OpenAI-compatible route are rows in
209
+ `ph-app`, so a headless deployment that wants a real provider layers one of
210
+ its profiles.
211
+ - **`sandbox-local` is Linux (bwrap) and macOS (Seatbelt).** It probes both
212
+ claims at mount — a write outside the workspace must fail, and a `CONNECT`
213
+ through the egress shim must reach the proxy and be refused — and on a host
214
+ that cannot confine it says so in `phern doctor` and declines rather than
215
+ pretending.
216
+ - **`permissions-fs`-style path rules are not here** — they are `ph-stabilize`,
217
+ and they bound *seam-mediated* access only. A model-authored `open(path, "w")`
218
+ inside a code cell never fires an intent; what bounds that is the sandbox rung
219
+ below the rules (E9, N1).
220
+ - **Nothing watches the filesystem.** `AGENTS.md` is re-read as a post-cache
221
+ snapshot each turn, which is what makes an edit take effect in the turn after
222
+ it; nothing else polls.
223
+ - **Row order carries no load semantics.** Activation is service-availability
224
+ driven, so a row waits for whatever it injects regardless of where it sits.
225
+ The grouping in `base.yaml` is for readers.
226
+
227
+ ## Tests
228
+
229
+ `tests/` — 75 modules, the largest suite in the workspace. Three worth knowing
230
+ about, because they enforce rules rather than behaviour:
231
+
232
+ - `test_layering.py` — the forbidden-import rule above;
233
+ - `test_keys.py` — every `ctx.provide(...)` in this package has a typed key in
234
+ `ph.keys`, and every key has a provider, held against each other in both
235
+ directions;
236
+ - the invariant rows (`agent-loop-invariant`, `session-invariant`,
237
+ `tools-invariant`, `skills-invariant`, `scope-invariant`) are checked *at
238
+ runtime*, not only in CI — a harness that can only prove these in a test
239
+ cannot prove them about your session, and `phern doctor` reports which hold.
@@ -0,0 +1,205 @@
1
+ # ph-core
2
+
3
+ *Everything a pH profile is made of: the plugin framework, the append-only log,
4
+ the ReAct loop, the capability seams — and the seventy-four rows that provide
5
+ them.*
6
+
7
+ There is no privileged core to patch (invariant I1). What this distribution
8
+ ships is the *vocabulary* a profile is written in — a context that holds
9
+ services, an event bus, scopes that unwind what they created — plus one row per
10
+ thing pH can do, each addressable by id from a YAML document. The agent loop is
11
+ a row. The session store is a row. The filesystem is a row. So is the tool
12
+ registry that decides whether a tool call is even allowed to happen.
13
+
14
+ ```bash
15
+ phern --profile headless -p "hello" # ph-base + the fake adapter
16
+ phern config --profile base # every knob every row accepts
17
+ phern doctor --profile base # what actually activated
18
+ ```
19
+
20
+ `ph-core` is a dependency of every other package in this workspace and depends
21
+ on none of them.
22
+
23
+ ## What is in here
24
+
25
+ | module | what it owns |
26
+ |---|---|
27
+ | `ph.cordis` | the plugin meta-framework subset (D1): `Context`, service keys, the four event dispatch modes, scopes whose disposal unwinds every registration (I2), and the YAML profile loader |
28
+ | `ph.session` | the append-only event log, the derived model surface (`derive_messages`), the human `transcript()`, and the folds over both |
29
+ | `ph.llm` | the provider-neutral vocabulary, the stream assembler, the `ctx.llm` seam, plus the `fake` and `replay` adapters |
30
+ | `ph.agent` / `ph.agent_loop` | the agent handle and its inbox; the ReAct driver, mounted as a row like anything else |
31
+ | `ph.tools` | the registry, the governed pipeline (`tools/pre-execute`, `tools/post-execute`, guards), the batch scheduler, and Code Mode's transport and generated SDK |
32
+ | `ph.seams` | one module per capability seam: the Protocol, the local provider, and what it refuses |
33
+ | `ph.system_prompt` | prompt assembly — cached `section`s, post-cache `context()` snapshots, and `AGENTS.md` discovery |
34
+ | `ph.persistence` | JSONL and Turso backends, checkpoints, leases, lineage and crash repair |
35
+ | `ph.commands` | the slash commands ph-core itself owns |
36
+ | `ph.bundles` | `base.yaml`, `headless.yaml`, and the `ph.bundles` entry-point group other distributions register into |
37
+ | `ph.paths` | the three roots (`$PH_HOME`, `$PH_CACHE`, `$PH_RUNTIME`) and their resolution rules |
38
+ | `ph.testing` | builders, stubs and fixtures a test stands a profile up with. Nothing shipped imports it |
39
+
40
+ ## The two bundles it ships
41
+
42
+ **`ph-base`** (`src/ph/bundles/base.yaml`) is the shared core of every pH
43
+ profile: the log, the loop, the tool registry, every capability seam with its
44
+ local provider, the built-in tools, durability, resilience and the runtime
45
+ invariant rows. **`ph-headless`** adds one row — the scripted `llm-fake`
46
+ adapter — so a one-shot or a scenario test can script a conversation without
47
+ touching code.
48
+
49
+ Both are *paths*, not entry points, because `ph-app` can import them directly.
50
+ Every other bundle in this workspace is **discovered** through the `ph.bundles`
51
+ entry-point group, which is what lets `ph-app` compose the `rlm` profile without
52
+ depending on `ph-rlm`:
53
+
54
+ ```toml
55
+ [project.entry-points."ph.bundles"]
56
+ rlm = "ph_rlm:BUNDLE"
57
+ ```
58
+
59
+ ## The tools the model gets
60
+
61
+ Registered by rows, so a profile decides which of them exist at all.
62
+
63
+ | row | tools | note |
64
+ |---|---|---|
65
+ | `tool-fs` | `read`, `write`, `edit`, `glob`, `grep` | thin shells over `ctx.fs`, so the policy gates and the workspace root apply to any second editing tool too |
66
+ | `tool-bash` | `bash` | goes through `ctx.shell`, which is what `sandbox-local` confines |
67
+ | `tool-attach` | `attach` | puts an image, audio file, video or PDF from the workspace in front of the model's own eyes. **Registers nothing without an attachment store** |
68
+ | `tool-ask-user` | `ask_user` | **ships disabled**: a question with nobody to answer it spends a turn on nothing. `tui.yaml` arms it |
69
+ | `subagent-task` | `task` | blocking delegation. **Registers nothing until a `ctx.subagents` provider is mounted** — a tool named in every prompt and refused on every call teaches the model a capability the deployment does not have |
70
+ | `skills-progressive` | `skill` | the catalog goes in the prompt; a body only when the model asks for it by name (G9) |
71
+
72
+ ## The commands a person can type
73
+
74
+ These are **commands, not tools**: a person asks the harness directly, it costs
75
+ no model turn, and the log records `command/run`/`command/done` rather than the
76
+ model having decided something the user decided.
77
+
78
+ | command | row | what it does |
79
+ |---|---|---|
80
+ | `/sandbox` | `sandbox-commands` | show what confined commands may reach, and change it without a restart |
81
+ | `/workspaces` | `workspace-commands` | list, export, merge or remove the branches agents left behind in this session |
82
+ | `/revert` | `workspace-revert` | restore this agent's workspace to a per-run checkpoint |
83
+ | `/autonomous` | `autonomous` | work toward a goal until its gates pass or a budget stops it |
84
+
85
+ ## Adjusting it from a profile
86
+
87
+ Three layers, applied in this order — and a patch replaces the targeted row's
88
+ **whole** config rather than merging into it, so a row's effective value is
89
+ always one layer's and readable in one place:
90
+
91
+ 1. the shipped documents — `ph-base`, then whatever the profile layers;
92
+ 2. your overlay, `$PH_HOME/profiles/<name>.yaml`;
93
+ 3. drop-ins pH wrote on your behalf, `$PH_HOME/profiles/<name>.d/*.yaml`, in
94
+ name order (this is where `/sandbox allow …` keeps its decisions);
95
+ 4. `--patch`, this run only, same grammar as a profile document.
96
+
97
+ A patch entry is `{id: …, config: {…}}` to reconfigure, `{id: …, disabled:
98
+ false}` to arm a row a bundle ships off, `{id: …, remove: true}` to drop one, or
99
+ `{insert: [...]}` to add one. **A list in row config is replaced, not merged** —
100
+ an overlay restating one field of a route must restate the whole route entry.
101
+
102
+ ```bash
103
+ phern --patch '{id: fs, config: {root: /tmp/scratch}}' --profile tui --mode tui
104
+ phern --patch '{id: tool-ask-user, disabled: false}' --profile headless -p "..."
105
+ phern --dump-config --profile llama # the composition, before anything runs
106
+ phern config --row containment --profile tui # one row's knobs, defaults and what the profile set
107
+ ```
108
+
109
+ ### The knobs most deployments touch
110
+
111
+ | row | config | default |
112
+ |---|---|---|
113
+ | `agent-loop` | `maxParallelToolCalls` | `10` |
114
+ | `tools` | `mode: native \| code` | `native` |
115
+ | `tools-code-mode` | `maxDispatchesPerRun`, `maxSubagentSpawnsPerRun`, `maxParallelSubCalls` | `256`, `32`, `10` |
116
+ | `fs-local` | `root`, `ignore` | the session cwd |
117
+ | `sandbox-policy` | `defaultMode: read-only \| workspace-write` | `read-only` (`tui.yaml` sets `workspace-write`) |
118
+ | `sandbox-allow` | `paths`, `network.{mode,hosts}` | an allowlist over the package indexes and documentation hosts in `DEFAULT_HOSTS` |
119
+ | `containment` | `tier`, `childTier`, `strict` | **unset** — which is not `advisory`: unset means nobody chose, so a tier provider a profile layered is used |
120
+ | `workspace-lifecycle` | `access`, `provision` | `write` |
121
+ | `jobs-local` | `concurrency` (per job kind) | unset |
122
+ | `llm-retry` | `maxAttempts`, `baseDelayMs`, `maxDelayMs` | `3`, `500`, `20000` |
123
+ | `session-telemetry` | `enabled`, `path` | `enabled: false` in `ph-base` |
124
+ | `subagent-presets` | `presets` | empty — a menu, never a grant: selecting a preset never widens what the parent itself holds |
125
+ | `skills-progressive` | `paths` | **empty on purpose** — scanning a well-known directory would make "install a skill" mean "drop a file somewhere" (I7) |
126
+ | `autonomous` | `maxContinuations`, `maxTurns`, `maxTokens`, `timeoutMs` | `3`, `12`, `80000`, `1800000` |
127
+ | `subprocess-local` | `scrub`, `keep`, `maxOutputBytes` | `8388608` |
128
+ | `tool-attach` | `maxBytes` | `33554432` |
129
+
130
+ **An entry carrying both `id:` and `name:` is a row, not a patch** — that is the
131
+ rule that decides which of the two a document line is. So swapping a *provider*
132
+ is a removal and an insertion, not a rename; consumers never learn which one
133
+ answered either way (I5):
134
+
135
+ ```yaml
136
+ # $PH_HOME/profiles/tui.yaml — keep the log in Turso instead of JSONL
137
+ - id: session-persistence
138
+ remove: true
139
+ - insert:
140
+ - id: session-persistence
141
+ name: session-persistence-turso
142
+ ```
143
+
144
+ Giving the existing id a new `name:` in place looks like it should work and does
145
+ not: it appends a *second* row, and the mount then refuses with
146
+ `service "session_persistence" is already provided`.
147
+
148
+ ## Seams that ship with no provider, deliberately
149
+
150
+ `ph-base` mounts the *definition* of several seams and no backend, because these
151
+ have genuinely different answers per deployment and a harness that shipped one
152
+ would have made the choice for you:
153
+
154
+ - **`code_runtime`** — nothing runs model-written code until `ph-rlm` mounts
155
+ `code-runtime-python` (or a profile mounts `code-runtime-stub`).
156
+ - **`subagents`** — no child-agent provider, which is why `subagent-task`
157
+ registers no tool in `ph-base`.
158
+ - **`compaction`** — the seam records and replaces; *when* and *what to say* are
159
+ `ph-stabilize`'s `compaction-summarize`. Every profile `ph-app` offers layers
160
+ that bundle when it is installed, so a *profile* compacts; `ph-base` composed
161
+ on its own still does not.
162
+ - **`uploads`** — mounted with no uploader; each adapter row registers its own,
163
+ so a profile with no file API sends every byte inline.
164
+
165
+ A profile that layers nothing here simply never compacts, never delegates and
166
+ never runs code. That is the plain harness, and it is a supported posture.
167
+
168
+ ## Limitations, and things that are deliberate
169
+
170
+ - **No Textual, Rich, Typer, `ph_app`, aiohttp, textual-serve or jinja2
171
+ imports** — `tests/test_layering.py` fails the build on any of them. A front
172
+ end is a consumer of this package, never the other way round.
173
+ - **No real model wire ships here.** `llm-fake` and `llm-replay` are for wiring
174
+ work and tests; Anthropic, Google and the OpenAI-compatible route are rows in
175
+ `ph-app`, so a headless deployment that wants a real provider layers one of
176
+ its profiles.
177
+ - **`sandbox-local` is Linux (bwrap) and macOS (Seatbelt).** It probes both
178
+ claims at mount — a write outside the workspace must fail, and a `CONNECT`
179
+ through the egress shim must reach the proxy and be refused — and on a host
180
+ that cannot confine it says so in `phern doctor` and declines rather than
181
+ pretending.
182
+ - **`permissions-fs`-style path rules are not here** — they are `ph-stabilize`,
183
+ and they bound *seam-mediated* access only. A model-authored `open(path, "w")`
184
+ inside a code cell never fires an intent; what bounds that is the sandbox rung
185
+ below the rules (E9, N1).
186
+ - **Nothing watches the filesystem.** `AGENTS.md` is re-read as a post-cache
187
+ snapshot each turn, which is what makes an edit take effect in the turn after
188
+ it; nothing else polls.
189
+ - **Row order carries no load semantics.** Activation is service-availability
190
+ driven, so a row waits for whatever it injects regardless of where it sits.
191
+ The grouping in `base.yaml` is for readers.
192
+
193
+ ## Tests
194
+
195
+ `tests/` — 75 modules, the largest suite in the workspace. Three worth knowing
196
+ about, because they enforce rules rather than behaviour:
197
+
198
+ - `test_layering.py` — the forbidden-import rule above;
199
+ - `test_keys.py` — every `ctx.provide(...)` in this package has a typed key in
200
+ `ph.keys`, and every key has a provider, held against each other in both
201
+ directions;
202
+ - the invariant rows (`agent-loop-invariant`, `session-invariant`,
203
+ `tools-invariant`, `skills-invariant`, `scope-invariant`) are checked *at
204
+ runtime*, not only in CI — a harness that can only prove these in a test
205
+ cannot prove them about your session, and `phern doctor` reports which hold.
@@ -0,0 +1,146 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ph-core"
7
+ version = "0.1.0"
8
+ description = "pH core: the cordis plugin subset, the session log, the LLM vocabulary, and the agent loop."
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Charles Tabor" }]
14
+ keywords = ["agent", "llm", "harness", "plugin", "session-log", "react"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Operating System :: POSIX :: Linux",
22
+ "Operating System :: MacOS",
23
+ "Topic :: Software Development",
24
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = [
28
+ # Ceilinged, and the ceiling is load-bearing. `ph_app.daemon.cancelsafe`
29
+ # is keyed to how anyio registers a socket readiness wait — a bound
30
+ # `Future.set_result` handed to `loop.add_reader` — and `ph_rlm.kernel`
31
+ # depends on the *hardened* free `anyio.wait_readable` staying hardened.
32
+ # Neither is public API, so a major release that changes either should be
33
+ # a decision someone makes, not one a resolver makes for them.
34
+ "anyio>=4.4,<5",
35
+ "croniter>=3.0",
36
+ "filelock>=3.15",
37
+ "pydantic>=2.9",
38
+ "pyturso>=0.7",
39
+ "pyyaml>=6.0",
40
+ ]
41
+
42
+ # Exporting to a collector is opt-in; `session-telemetry-otel` refuses with a
43
+ # message naming this extra rather than failing to import. Both distributions,
44
+ # because the row needs the provider *and* a transport and refuses without
45
+ # either — half of the extra is a sink that ships nowhere.
46
+ [project.urls]
47
+ Homepage = "https://github.com/chastabor/pH"
48
+ Repository = "https://github.com/chastabor/pH"
49
+ Documentation = "https://github.com/chastabor/pH/blob/main/docs/README.md"
50
+ Issues = "https://github.com/chastabor/pH/issues"
51
+
52
+ [project.optional-dependencies]
53
+ otel = ["opentelemetry-sdk>=1.27", "opentelemetry-exporter-otlp-proto-http>=1.27"]
54
+
55
+ # The plugin discovery group. Reserved in Phase 0 (P0-01) so a third-party
56
+ # wheel can mount a row by name from its first release onward.
57
+ [project.entry-points."ph.plugins"]
58
+ # core
59
+ llm = "ph.llm.adapter:apply"
60
+ session = "ph.session.store:apply"
61
+ agent = "ph.agent.registry:apply"
62
+ agent-loop = "ph.agent_loop:apply"
63
+ system-prompt = "ph.system_prompt.assembly:apply"
64
+ memory-agents-md = "ph.system_prompt.memory:apply"
65
+ tools = "ph.tools.registry:apply"
66
+ tools-prompt = "ph.tools.prompt:apply"
67
+ tools-timeout = "ph.tools.timeout:apply"
68
+ # capability seams
69
+ approval = "ph.seams.approval:apply"
70
+ attachments-local = "ph.seams.attachments:apply"
71
+ uploads-local = "ph.seams.uploads:apply"
72
+ code-runtime = "ph.seams.code_runtime:apply"
73
+ compaction = "ph.seams.compaction:apply"
74
+ commands = "ph.seams.commands:apply"
75
+ credentials-env = "ph.seams.credentials:apply"
76
+ fs-local = "ph.seams.fs:apply"
77
+ fs-read-before-edit = "ph.seams.fs:read_before_edit"
78
+ jobs-local = "ph.seams.jobs:apply"
79
+ permission-presets = "ph.seams.permission_presets:apply"
80
+ goals = "ph.seams.goals:apply"
81
+ autonomous = "ph.commands.autonomous:apply"
82
+ schedule = "ph.seams.schedule:apply"
83
+ sandbox-local = "ph.seams.sandbox_local:apply"
84
+ sandbox-policy = "ph.seams.sandbox:apply"
85
+ sandbox-allow = "ph.seams.sandbox_allow:apply"
86
+ sandbox-commands = "ph.commands.sandbox:apply"
87
+ settings-local = "ph.seams.settings:apply"
88
+ shell-local = "ph.seams.shell:apply"
89
+ skills = "ph.seams.skills:apply"
90
+ skills-progressive = "ph.seams.skills:progressive"
91
+ subagents = "ph.seams.subagents:apply"
92
+ subagent-presets = "ph.seams.subagents:presets"
93
+ spill-local = "ph.seams.spill:apply"
94
+ subprocess-local = "ph.seams.subprocess:apply"
95
+ tui-screens = "ph.seams.tui_screens:apply"
96
+ tui-status = "ph.seams.tui_status:apply"
97
+ token-meter = "ph.seams.token_meter:apply"
98
+ user-questions = "ph.seams.user_questions:apply"
99
+ containment = "ph.seams.containment:apply"
100
+ diagnostics = "ph.seams.diagnostics:apply"
101
+ topology = "ph.seams.topology:apply"
102
+ invariants = "ph.seams.invariants:apply"
103
+ workspace-shared = "ph.seams.workspace:apply"
104
+ workspace-agentfs = "ph.seams.workspace_agentfs:apply"
105
+ workspace-git-worktree = "ph.seams.workspace_git:apply"
106
+ workspace-jj = "ph.seams.workspace_jj:apply"
107
+ workspace-readonly-scratch = "ph.seams.workspace_scratch:apply"
108
+ workspace-commands = "ph.commands.workspaces:apply"
109
+ workspace-checkpoint = "ph.seams.workspace:checkpoint_policy"
110
+ workspace-revert = "ph.commands.revert:apply"
111
+ workspace-lifecycle = "ph.seams.workspace:lifecycle"
112
+ workspace-reconcile = "ph.seams.workspace:reconcile"
113
+ session-telemetry = "ph.seams.telemetry:apply"
114
+ # model-facing tools
115
+ tool-fs = "ph.tools.builtin.fs_tools:apply"
116
+ tool-bash = "ph.tools.builtin.bash_tool:apply"
117
+ tool-ask-user = "ph.tools.builtin.ask_user:apply"
118
+ tool-attach = "ph.tools.builtin.attach_tool:apply"
119
+ subagent-task = "ph.tools.builtin.subagent_task:apply"
120
+ # durability and resilience
121
+ session-telemetry-otel = "ph.seams.telemetry_otel:apply"
122
+ session-persistence-jsonl = "ph.persistence.jsonl:apply"
123
+ session-persistence-turso = "ph.persistence.turso:apply"
124
+ session-checkpoint-policy = "ph.persistence.checkpoint_policy:apply"
125
+ llm-retry = "ph.llm.retry:apply"
126
+ media-degrade = "ph.llm.media:apply"
127
+ # testing and invariants
128
+ #
129
+ # The first three are test-facing *rows* whose modules are not, and the paths
130
+ # say so: a row is mountable, so it lives with the seam it implements, while
131
+ # `ph.testing` is scaffolding nothing shipped may import. `llm-fake` is the one
132
+ # that made the rule — `headless`, the default profile, mounts it, so an install
133
+ # with no pytest could not compose its own default while this pointed into the
134
+ # test package.
135
+ llm-fake = "ph.llm.fake:apply"
136
+ code-runtime-stub = "ph.seams.code_runtime_stub:apply"
137
+ llm-replay = "ph.llm.replay:apply"
138
+ tools-code-mode = "ph.tools.code_mode:apply"
139
+ agent-loop-invariant = "ph.agent_loop.invariant:apply"
140
+ session-invariant = "ph.session.invariant:apply"
141
+ tools-invariant = "ph.tools.invariant:apply"
142
+ skills-invariant = "ph.seams.skills_invariant:apply"
143
+ scope-invariant = "ph.seams.scope_invariant:apply"
144
+
145
+ [tool.hatch.build.targets.wheel]
146
+ packages = ["src/ph"]
@@ -0,0 +1,9 @@
1
+ """pH — a Python agent harness ported from DeepSeek Harness.
2
+
3
+ The core distribution. See `docs/dev-notes/phase-0.md` for what Phase 0 landed
4
+ and `plans/Implementation_Plan.md` for the work breakdown.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ __version__ = "0.1.0"