From cc20de8cacdab6db32ffd16ba15e306864740313 Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Wed, 17 Jun 2026 19:49:01 -0600 Subject: [PATCH] =?UTF-8?q?feat(think):=20synthesis=20access-tier=20?= =?UTF-8?q?=E2=80=94=20sol-surface-only=20cogitate=20(no=20raw-fs=20reads)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a fourth locked cogitate access tier `synthesis` (sol=True, reads=False, submit=False): the journal is reached only through `sol` domain commands, with no raw-filesystem read tier (read_file / list_directory / glob / grep_search) and no outbound submit. It fills the last meaningful corner of the (reads, submit) capability space — `outbound` was the only reads=False tier but it drags submit=True along, which is wrong for a pure synthesis talent. This is the mechanism half of the weekly_reflection fs-fallback fix; 23d6498a shipped the prose ("gather only through sol; do not walk the raw tree"), and a 2026-06-17 weekly run still logged 12 fs-fallback + 4 repeat-flail of 29 actions because the read tools were merely discouraged, not withheld. The tier removes them at registration, so the constraint the prose asks for is now enforced. Set weekly_reflection + partner to `synthesis`. weekly_reflection needed no body change (prose already forbids raw reads). partner had a live Step-1 `read_file identity/partner.md` dependency, swapped to `journal identity partner` (the settled sol-surface read form, policy-allowed at reads=False). The locked-contract RuntimeError guards keep COGITATE_ACCESS_TIERS in sync with the capability map; all caps/display/validation consumers are contract-driven and auto-adapt (openhands tool registration, cogitate_policy, talent_cli inventory + tier audit). Updated the contract vocabulary-lock test, added the capability row, made the finalization-harness tool-surface assertion tier-aware (weekly_reflection is now sol-only), and documented the tier in docs/COGITATE.md. --- docs/COGITATE.md | 1 + solstone/talent/partner.md | 4 +++- solstone/talent/weekly_reflection.md | 1 + solstone/think/cogitate_contract.py | 7 ++++++- tests/test_cogitate_contract.py | 3 ++- tests/test_cogitate_contract_harness.py | 6 +++++- 6 files changed, 18 insertions(+), 4 deletions(-) diff --git a/docs/COGITATE.md b/docs/COGITATE.md index 4adb6e16b..d65444c45 100644 --- a/docs/COGITATE.md +++ b/docs/COGITATE.md @@ -126,6 +126,7 @@ enforcement are layered on top of it. | `normal` | default cogitate talents | the `sol` tool (`sol` / `sol call`), the bounded raw-read tier, a finalization tool | | `system-read` | diagnostics boundary for scoped operational evidence | no cogitate talent claims it today (steward was demoted to a deterministic renderer + `lite` generate); the tier remains the declared diagnostics boundary and extension point, with scoped evidence arriving through a talent pre-hook rather than an extra model read tool | | `outbound` | comms-like talents that may submit something that leaves the machine (e.g. `support`) | the `sol` tool (`sol` / `sol call`) and a finalization tool, plus submit-capable support commands gated on per-send owner approval supplied only by a human-initiated chat launch; no raw-read tier — drafts and evidence go through `sol` domain commands | +| `synthesis` | pure sol-surface synthesis talents (e.g. `weekly_reflection`, `partner`) whose source of record is `sol call journal` / `sol call activities`, not the raw journal tree | the `sol` tool (`sol` / `sol call`) and a finalization tool; **no raw-read tier and no submit** — same as `outbound` minus the outbound submit capability. Removing the raw-read tools keeps a synthesis talent from spelunking `chronicle/` / `talents/` / `facets/` and burning its budget instead of reaching the journal through `sol` | Policy denies support send verbs (`create`, `reply`, `attach`, `feedback`) for `normal` / `system-read` runs. `outbound` runs may use those verbs only when the diff --git a/solstone/talent/partner.md b/solstone/talent/partner.md index 210fe2157..437660d70 100644 --- a/solstone/talent/partner.md +++ b/solstone/talent/partner.md @@ -1,5 +1,6 @@ { "type": "cogitate", + "access_tier": "synthesis", "title": "Partner Profile", "description": "Weekly observation of the journal owner's behavioral patterns — work style, communication, priorities, decision-making, expertise", @@ -19,7 +20,8 @@ This is not a conversation. Gather data, observe patterns, update the profile, t ## Step 1: Read current state -Read `identity/partner.md` with the `read_file` tool. +Read the current profile with `journal identity partner` — the settled +`sol`-surface read form for `identity/partner.md`. Note which sections have real observations vs `[observing]` placeholders. diff --git a/solstone/talent/weekly_reflection.md b/solstone/talent/weekly_reflection.md index 2faa39d0f..5f0cc7197 100644 --- a/solstone/talent/weekly_reflection.md +++ b/solstone/talent/weekly_reflection.md @@ -1,5 +1,6 @@ { "type": "cogitate", + "access_tier": "synthesis", "title": "Weekly Reflection", "description": "Sunday-start weekly reflection synthesized from the journal", "schedule": "weekly", diff --git a/solstone/think/cogitate_contract.py b/solstone/think/cogitate_contract.py index de2093239..385f72c51 100644 --- a/solstone/think/cogitate_contract.py +++ b/solstone/think/cogitate_contract.py @@ -27,7 +27,7 @@ You are a solstone cogitate talent running inside the live system. This runtime # Locked cogitate access-tier vocabulary (the C1 contract). Downstream milestones # key per-talent assignment, enforcement, redesign, and lint off these names. -COGITATE_ACCESS_TIERS = ("normal", "system-read", "outbound") +COGITATE_ACCESS_TIERS = ("normal", "system-read", "outbound", "synthesis") # `code-agent` is a documented FUTURE tier — NOT part of the current cogitate # runtime (it needs write access, broad tools, and a repo cwd, deliberately out of @@ -54,6 +54,11 @@ _ACCESS_TIER_CAPABILITIES: dict[str, AccessCapabilities] = { "normal": AccessCapabilities(sol=True, reads=True, submit=False), "system-read": AccessCapabilities(sol=True, reads=True, submit=False), "outbound": AccessCapabilities(sol=True, reads=False, submit=True), + # Pure sol-surface synthesis: the journal is reached only through `sol` + # domain commands, with no raw-filesystem read tier and no outbound submit. + # For synthesis talents (weekly_reflection, partner) whose source of record + # is `sol call journal` / `sol call activities`, not the raw journal tree. + "synthesis": AccessCapabilities(sol=True, reads=False, submit=False), } _missing_access_tiers = set(COGITATE_ACCESS_TIERS) - set(_ACCESS_TIER_CAPABILITIES) diff --git a/tests/test_cogitate_contract.py b/tests/test_cogitate_contract.py index 4bc4219bd..0cfbfe218 100644 --- a/tests/test_cogitate_contract.py +++ b/tests/test_cogitate_contract.py @@ -66,7 +66,7 @@ def test_prompt_body_unchanged_under_cogitate_injection(): def test_cogitate_vocabulary_lock(): - assert COGITATE_ACCESS_TIERS == ("normal", "system-read", "outbound") + assert COGITATE_ACCESS_TIERS == ("normal", "system-read", "outbound", "synthesis") assert COGITATE_READ_TOOL_NAMES == ( "read_file", "list_directory", @@ -107,6 +107,7 @@ def test_expects_emit_final(config, expected): ("normal", (True, True, False)), ("system-read", (True, True, False)), ("outbound", (True, False, True)), + ("synthesis", (True, False, False)), ], ) def test_capabilities_for_access_tier_real_tiers(tier, expected): diff --git a/tests/test_cogitate_contract_harness.py b/tests/test_cogitate_contract_harness.py index 3ad4319aa..fa0574956 100644 --- a/tests/test_cogitate_contract_harness.py +++ b/tests/test_cogitate_contract_harness.py @@ -63,7 +63,11 @@ def test_cogitate_finalization_class_assembles_on_contract( *(["sol"] if caps.sol else []), *(COGITATE_READ_TOOL_NAMES if caps.reads else ()), ] - assert tool_surface == ["sol", *COGITATE_READ_TOOL_NAMES] + # Every real cogitate tier exposes the `sol` surface; the bounded raw-read + # tools are present iff the talent's access tier grants reads. The + # `synthesis` tier (e.g. weekly_reflection) is sol-only by design. + expected_surface = ["sol", *COGITATE_READ_TOOL_NAMES] if caps.reads else ["sol"] + assert tool_surface == expected_surface body, system = assemble_prompt(config, sol_tool_name="sol") assert isinstance(body, str) -- 2.51.2