personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(cogitate): one approved journal-command vocabulary

Define {identity, health, talent} once in cogitate_contract.py and render it
into the policy, the prompt lint, the inventory scan, the runtime preamble,
both model-visible tool descriptions, and the sol-tool hint. The preamble no
longer bans bare `journal ...` while policy allows three families; it now
states the positive rule — normal access is `sol` / `sol call`, the three
approved host families run directly as `journal <family> ...` and must not be
prefixed with `sol call`.

Refuse the two forms seen in the stuck production run before execution, each
with the working repair: `sol call journal <family> ...` for any of the three
families, and the bare `journal search` / `journal facet` retries.

+354 -56
+2 -2
core/fixtures/cogitate_contract.json
··· 52 52 ], 53 53 "runtime_preamble": { 54 54 "algorithm": "sha256", 55 - "byte_length": 1745, 56 - "digest": "80fb4b8b98ee800489ca92728b0a49ffd5dfdd35f1af7cc129a7831273db0568", 55 + "byte_length": 1989, 56 + "digest": "6614e3fd060f29e7c3cb0ed063e43e370a7efefd2579974080fc8caec65ca591", 57 57 "encoding": "utf-8" 58 58 } 59 59 }
+28 -22
docs/COGITATE.md
··· 61 61 ## The `sol` CLI is the authoritative talent-to-journal contract 62 62 63 63 A talent reaches journal functionality by emitting `sol` / `sol call ...` 64 - command lines. The runtime parses each tool call as one command-line invocation 65 - and executes that argv directly; it is not an arbitrary shell. The `sol` CLI 66 - handlers are the **single authoritative translation layer** between a talent and 67 - the journal: they turn CLI syntax into convey API calls. A talent therefore: 64 + command lines. The approved host command families `journal identity ...`, 65 + `journal health ...`, and `journal talent ...` also run directly and must not be 66 + prefixed with `sol` or `sol call`. The runtime parses each tool call as one 67 + command-line invocation and executes that argv directly; it is not an arbitrary 68 + shell. The CLI handlers are the **single authoritative translation layer** 69 + between a talent and the journal: they turn CLI syntax into journal operations. 70 + A talent therefore: 68 71 69 72 - **never** talks to the convey HTTP API directly, and 70 73 - **never** assumes a database, a socket, or any journal access other than the 71 - `sol` CLI and the bounded raw-read tools below. 74 + documented command forms and the bounded raw-read tools below. 72 75 73 - The tool call shape is `sol(command="sol call activities list")`. The command 74 - policy (below) constrains what may run and rejects shell composition such as 75 - pipes, redirects, chaining, and command substitution. 76 + The tool call shape is `sol(command="sol call activities list")`, or 77 + `sol(command="journal identity partner")` for an approved host command family. 78 + The command policy (below) constrains what may run and rejects shell composition 79 + such as pipes, redirects, chaining, and command substitution. 76 80 77 81 ## Reads: domain reads vs raw evidence reads 78 82 ··· 103 107 > access tier. Check the run's actual tool schema 104 108 > (`journal talent show <name> --prompt`) for what a specific talent receives. 105 109 106 - ## Writes: only through `sol` domain commands 110 + ## Writes: only through approved journal commands 107 111 108 - A talent **writes journal state only through `sol` domain commands** (the 112 + A talent **writes journal state only through approved journal commands** (the 109 113 `sol call ...` verbs for the domain it is updating, e.g. 110 - `sol call entities update ...`). There is **no general-purpose write tool** and no 111 - raw-write tier — write-class tools are denied by policy. The mechanic is 112 - centralized in the domain command (which routes through the convey API to the 113 - single write-owner for that data); a talent gets the *capability* to update a 114 - domain, never an arbitrary file-write path. Persistence that does not go through a 115 - `sol` domain command does not happen. 114 + `sol call entities update ...`, plus approved direct host commands when a prompt 115 + names one). There is **no general-purpose write tool** and no raw-write tier — 116 + write-class tools are denied by policy. The mechanic is centralized in the owning 117 + command; a talent gets the *capability* to update a domain, never an arbitrary 118 + file-write path. Persistence that does not go through an approved journal command 119 + does not happen. 116 120 117 121 ## Access tiers 118 122 ··· 126 130 | `normal` | default cogitate talents | the `sol` tool (`sol` / `sol call`), the bounded raw-read tier, a finalization tool | 127 131 | `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 | 128 132 | `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 | 129 - | `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` | 133 + | `synthesis` | pure command-surface synthesis talents (e.g. `weekly_reflection`, `partner`) whose source of record is a documented command form, not the raw journal tree | the `sol` tool (`sol` / `sol call`, plus approved direct `journal` families when a prompt names one) 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 using documented commands | 130 134 131 135 Policy denies support send verbs (`create`, `reply`, `attach`, `feedback`) for 132 136 `normal` / `system-read` runs. `outbound` runs may use those verbs only when the ··· 157 161 |---|---|---| 158 162 | `emit_final` | scheduled / output talents (an `output_path` is set, or the schedule is `daily` / `weekly` / `activity`) | the run is accepted **only** from the `emit_final` tool; `FinishTool` is disabled; a finish with no emitted final is an error | 159 163 | `FinishTool` | manual / no-output talents | the run signals completion through OpenHands' built-in finish tool | 160 - | `quiet` | side-effect-only talents | the talent has already persisted its work through `sol` domain commands and finishes with no output | 164 + | `quiet` | side-effect-only talents | the talent has already persisted its work through approved journal commands and finishes with no output | 161 165 162 166 A manual talent's prompt should state which of these it expects so the behavior is 163 167 explicit rather than inferred. ··· 167 171 A talent prompt must not assume any of the following — they are named here so 168 172 prompts can be checked against them: 169 173 170 - - **bare `journal ...` commands** (e.g. `journal identity`, `journal health`, 171 - `journal navigate`) — reach the journal through `sol` / `sol call`; 174 + - **other bare `journal ...` families** (e.g. `journal search`, `journal facet`, 175 + `journal navigate`) — reach the journal through the documented `sol` / 176 + `sol call` form; 172 177 - **raw `cat` / `ls` / arbitrary shell file reads** — use the raw-read tools when 173 178 present, not shell file access; 174 179 - **auto-loaded skills, `AGENTS.md`, `CLAUDE.md`, or `GEMINI.md`** — these are ··· 191 196 You are a solstone cogitate talent running inside the live system. This runtime contract is authoritative; do not assume capabilities beyond it. 192 197 193 198 - Reach the journal through the `sol` command line: emit `sol` / `sol call ...` command lines, e.g. sol(command="sol call activities list"). The runtime runs each call as a single parsed command-line invocation, not an arbitrary shell. The `sol` CLI is the one authoritative path between you and the journal; never assume direct database, socket, or HTTP access. 194 - - Write journal state only through `sol` domain commands (the `sol call ...` verbs for the data you own). There is no general-purpose write tool; persistence that does not go through a `sol` domain command will not happen. 199 + - The approved host command families are identity, health, talent; run them directly as `journal <family> ...` through the same tool, never prefixed with `sol` or `sol call`. 200 + - Write journal state only through approved journal commands (`sol call ...` verbs for the data you own, plus approved direct host commands when a prompt names one). There is no general-purpose write tool; persistence that does not go through an approved journal command will not happen. 195 201 - Raw evidence reads use the provided read tools (`read_file`, `list_directory`, `glob`, `grep_search`), bounded to the journal root: a denylist (`.git`, caches, credentials, virtualenvs, `node_modules`) and per-call / per-run caps apply. Prefer `sol call` reads; use raw reads only for evidence that has no `sol` command. 196 202 - Finalize as your run is configured: call `emit_final` when an `emit_final` tool is present; otherwise finish through the built-in finish tool; a side-effect-only talent that has already persisted its work finishes quietly with no output. 197 - - Do not assume tools or context you were not given: no bare `journal ...` commands, no raw `cat` / `ls` / shell file reads, no shell composition (pipes, redirects, chaining, or command substitution; one command per call), no auto-loaded skills or AGENTS.md / CLAUDE.md, no browser or web access, no MCP tools, and no delegating to sub-agents. Any guidance file is a normal journal file with no special status; this contract is your source of truth. 203 + - Do not assume tools or context you were not given: no other bare `journal ...` family, no raw `cat` / `ls` / shell file reads, no shell composition (pipes, redirects, chaining, or command substitution; one command per call), no auto-loaded skills or AGENTS.md / CLAUDE.md, no browser or web access, no MCP tools, and no delegating to sub-agents. Any guidance file is a normal journal file with no special status; this contract is your source of truth. 198 204 ``` 199 205 200 206 ## Where this is wired (for maintainers)
+8 -6
scripts/check_cogitate_prompts.py
··· 37 37 import frontmatter 38 38 39 39 import solstone.think.talent as talent 40 + from solstone.think.cogitate_contract import ( 41 + COGITATE_JOURNAL_COMMANDS, 42 + cogitate_journal_command_list, 43 + ) 40 44 41 45 ROOT = Path(__file__).resolve().parent.parent 42 - 43 - # Must equal solstone/think/cogitate_policy.py:_JOURNAL_COMMANDS 44 - # (cogitate_policy.py:21). Duplicated here intentionally; no shared import. 45 - ALLOWED_JOURNAL_COMMANDS = frozenset({"identity", "health", "talent"}) 46 46 47 47 # Must equal solstone/think/cogitate_policy.py:_READ_TOOLS 48 48 # (cogitate_policy.py:24). Duplicated here intentionally; no shared import. ··· 87 87 ALLOWLIST: dict[tuple[str, str], int] = {} 88 88 89 89 JOURNAL_ALTERNATIVE = ( 90 - "use `journal` with one of {identity, health, talent}, or use `sol`/`sol call`" 90 + "use `journal` with one of {" 91 + + cogitate_journal_command_list() 92 + + "}, or use `sol`/`sol call`" 91 93 ) 92 94 READ_ALTERNATIVE = ( 93 95 "use a bounded read tool: read_file, list_directory, glob, or grep_search" ··· 246 248 if ( 247 249 tokens[0] == "journal" 248 250 and len(tokens) >= 2 249 - and tokens[1] not in ALLOWED_JOURNAL_COMMANDS 251 + and tokens[1] not in COGITATE_JOURNAL_COMMANDS 250 252 ): 251 253 findings.append( 252 254 (
+4 -4
solstone/talent/partner.md
··· 20 20 21 21 ## Step 1: Read current state 22 22 23 - Read the current profile with `journal identity partner` — the settled 24 - `sol`-surface read form for `identity/partner.md`. 23 + Read the current profile with `journal identity partner` through the provided 24 + `sol` tool — it is the approved direct host read command for `identity/partner.md`. 25 25 26 26 Note which sections have real observations vs `[observing]` placeholders. 27 27 ··· 40 40 41 41 For each of the five profile sections, analyze the gathered data and write 42 42 observations if you have sufficient evidence. Use `journal identity partner --update-section` 43 - for each section you update — it is the owned write command for `partner.md` 44 - (there is no `sol call` verb for it yet). 43 + through the provided `sol` tool for each section you update — it is the approved 44 + direct host write command for `partner.md`. 45 45 46 46 ### Section guidance 47 47
+18 -6
solstone/think/cogitate_contract.py
··· 15 15 from dataclasses import dataclass 16 16 from typing import Any 17 17 18 - COGITATE_RUNTIME_PREAMBLE = """\ 18 + COGITATE_JOURNAL_COMMANDS = ("identity", "health", "talent") 19 + 20 + 21 + def cogitate_journal_command_list() -> str: 22 + """Return the approved direct journal command families for prompt copy.""" 23 + return ", ".join(COGITATE_JOURNAL_COMMANDS) 24 + 25 + 26 + COGITATE_RUNTIME_PREAMBLE = f"""\ 19 27 You are a solstone cogitate talent running inside the live system. This runtime contract is authoritative; do not assume capabilities beyond it. 20 28 21 29 - Reach the journal through the `sol` command line: emit `sol` / `sol call ...` command lines, e.g. sol(command="sol call activities list"). The runtime runs each call as a single parsed command-line invocation, not an arbitrary shell. The `sol` CLI is the one authoritative path between you and the journal; never assume direct database, socket, or HTTP access. 22 - - Write journal state only through `sol` domain commands (the `sol call ...` verbs for the data you own). There is no general-purpose write tool; persistence that does not go through a `sol` domain command will not happen. 30 + - The approved host command families are {cogitate_journal_command_list()}; run them directly as `journal <family> ...` through the same tool, never prefixed with `sol` or `sol call`. 31 + - Write journal state only through approved journal commands (`sol call ...` verbs for the data you own, plus approved direct host commands when a prompt names one). There is no general-purpose write tool; persistence that does not go through an approved journal command will not happen. 23 32 - Raw evidence reads use the provided read tools (`read_file`, `list_directory`, `glob`, `grep_search`), bounded to the journal root: a denylist (`.git`, caches, credentials, virtualenvs, `node_modules`) and per-call / per-run caps apply. Prefer `sol call` reads; use raw reads only for evidence that has no `sol` command. 24 33 - Finalize as your run is configured: call `emit_final` when an `emit_final` tool is present; otherwise finish through the built-in finish tool; a side-effect-only talent that has already persisted its work finishes quietly with no output. 25 - - Do not assume tools or context you were not given: no bare `journal ...` commands, no raw `cat` / `ls` / shell file reads, no shell composition (pipes, redirects, chaining, or command substitution; one command per call), no auto-loaded skills or AGENTS.md / CLAUDE.md, no browser or web access, no MCP tools, and no delegating to sub-agents. Any guidance file is a normal journal file with no special status; this contract is your source of truth. 34 + - Do not assume tools or context you were not given: no other bare `journal ...` family, no raw `cat` / `ls` / shell file reads, no shell composition (pipes, redirects, chaining, or command substitution; one command per call), no auto-loaded skills or AGENTS.md / CLAUDE.md, no browser or web access, no MCP tools, and no delegating to sub-agents. Any guidance file is a normal journal file with no special status; this contract is your source of truth. 26 35 """ 27 36 28 37 COGITATE_DIAGNOSTIC_PREAMBLE = """\ ··· 71 80 "normal": AccessCapabilities(sol=True, reads=True, submit=False), 72 81 "system-read": AccessCapabilities(sol=True, reads=True, submit=False), 73 82 "outbound": AccessCapabilities(sol=True, reads=False, submit=True), 74 - # Pure sol-surface synthesis: the journal is reached only through `sol` 75 - # domain commands, with no raw-filesystem read tier and no outbound submit. 83 + # Pure command-surface synthesis: the journal is reached only through 84 + # approved journal commands, with no raw-filesystem read tier and no 85 + # outbound submit. 76 86 # For synthesis talents (weekly_reflection, partner) whose source of record 77 - # is `sol call journal` / `sol call activities`, not the raw journal tree. 87 + # is a documented command form, not the raw journal tree. 78 88 "synthesis": AccessCapabilities(sol=True, reads=False, submit=False), 79 89 "diagnostic": AccessCapabilities(sol=False, reads=False, submit=False), 80 90 } ··· 109 119 __all__ = [ 110 120 "AccessCapabilities", 111 121 "COGITATE_DIAGNOSTIC_PREAMBLE", 122 + "COGITATE_JOURNAL_COMMANDS", 112 123 "COGITATE_RUNTIME_PREAMBLE", 113 124 "COGITATE_ACCESS_TIERS", 114 125 "COGITATE_READ_TOOL_NAMES", ··· 116 127 "TALENT_ACCESS_TIERS", 117 128 "TALENT_FINALIZATION_MODES", 118 129 "capabilities_for_access_tier", 130 + "cogitate_journal_command_list", 119 131 "expects_emit_final", 120 132 ]
+41 -2
solstone/think/cogitate_policy.py
··· 12 12 13 13 from solstone.think.cogitate_contract import ( 14 14 COGITATE_ACCESS_TIERS, 15 + COGITATE_JOURNAL_COMMANDS, 15 16 COGITATE_READ_TOOL_NAMES, 16 17 capabilities_for_access_tier, 17 18 ) ··· 77 78 return cap is not None and count >= cap 78 79 79 80 80 - _JOURNAL_COMMANDS = {"identity", "health", "talent"} 81 81 _SHELL_OPERATOR_CHARS = frozenset("();<>|&") 82 82 _WRITE_TOOLS = {"write_file", "replace"} 83 83 _READ_TOOLS = frozenset(COGITATE_READ_TOOL_NAMES) ··· 95 95 RESTRICTED_COMMAND_DENY = ( 96 96 "policy_deny: run_shell_command restricted to sol or approved journal invocations" 97 97 ) 98 + HYBRID_JOURNAL_COMMAND_DENY = ( 99 + "policy_deny: `sol call journal {family}` is not a `sol call` verb; " 100 + "`{family}` is an approved host command family — run it directly as `{repair}`" 101 + ) 102 + BARE_JOURNAL_REPAIR_DENY = ( 103 + "policy_deny: `journal {family}` is not a host command; run `{repair}` instead" 104 + ) 105 + _BARE_JOURNAL_REPAIR_PREFIXES = { 106 + "search": ("sol", "call", "journal", "search"), 107 + "facet": ("sol", "call", "journal", "facet"), 108 + } 98 109 99 110 100 111 @dataclass(frozen=True) ··· 136 147 return quote is not None 137 148 138 149 150 + def _hybrid_journal_deny(argv: list[str]) -> str: 151 + family = argv[3] 152 + repair = shlex.join(["journal", *argv[3:]]) 153 + return HYBRID_JOURNAL_COMMAND_DENY.format(family=family, repair=repair) 154 + 155 + 156 + def _bare_journal_repair_deny(argv: list[str]) -> str: 157 + family = argv[1] 158 + repair = shlex.join([*_BARE_JOURNAL_REPAIR_PREFIXES[family], *argv[2:]]) 159 + return BARE_JOURNAL_REPAIR_DENY.format(family=family, repair=repair) 160 + 161 + 139 162 class MaxTurnsExhausted(RuntimeError): 140 163 """Raised when the SDK tool loop exceeds its turn ceiling.""" 141 164 ··· 182 205 if not argv: 183 206 return CommandDecision(False, EMPTY_COMMAND_DENY, None) 184 207 208 + if ( 209 + argv[0:3] == ["sol", "call", "journal"] 210 + and len(argv) >= 4 211 + and argv[3] in COGITATE_JOURNAL_COMMANDS 212 + ): 213 + return CommandDecision(False, _hybrid_journal_deny(argv), None) 214 + 215 + if ( 216 + argv[0] == "journal" 217 + and len(argv) >= 2 218 + and argv[1] in _BARE_JOURNAL_REPAIR_PREFIXES 219 + ): 220 + return CommandDecision(False, _bare_journal_repair_deny(argv), None) 221 + 185 222 if not ( 186 223 argv[0] == "sol" 187 224 or ( 188 - argv[0] == "journal" and len(argv) >= 2 and argv[1] in _JOURNAL_COMMANDS 225 + argv[0] == "journal" 226 + and len(argv) >= 2 227 + and argv[1] in COGITATE_JOURNAL_COMMANDS 189 228 ) 190 229 ): 191 230 return CommandDecision(False, RESTRICTED_COMMAND_DENY, None)
+11 -4
solstone/think/providers/cli.py
··· 24 24 COGITATE_DIAGNOSTIC_PREAMBLE, 25 25 COGITATE_READ_TOOL_NAMES, 26 26 COGITATE_RUNTIME_PREAMBLE, 27 + cogitate_journal_command_list, 27 28 ) 28 29 from solstone.think.providers.shared import JSONEventCallback, safe_raw 29 30 from solstone.think.utils import get_project_root, now_ms ··· 88 89 def cogitate_sol_tool_hint(tool_name: str) -> str: 89 90 """Return the shell-tool hint for non-write cogitate runs.""" 90 91 return ( 91 - "When the instructions tell you to run `sol ...` commands, invoke them " 92 - f"through the `{tool_name}` tool. Example: " 93 - f'`{tool_name}(command="sol call activities list")`. ' 94 - "Do not invent or call a tool literally named `sol`." 92 + "When the instructions tell you to run `sol ...` or approved " 93 + f"`journal ...` commands, invoke them through the `{tool_name}` tool. " 94 + "Normal journal access uses `sol` / `sol call ...`; the approved " 95 + "direct `journal` families are " 96 + + cogitate_journal_command_list() 97 + + " and must be run unprefixed as `journal <family> ...`. Examples: " 98 + f'`{tool_name}(command="sol call activities list")`, ' 99 + f'`{tool_name}(command="journal identity partner")`. ' 100 + "Do not invent or call a tool literally named `sol`, and do not " 101 + "rewrite approved `journal` commands as `sol call journal ...`." 95 102 ) 96 103 97 104
+13 -4
solstone/think/providers/openhands.py
··· 32 32 from solstone.log_policy import apply_http_logging_policy, snapshot_root_logging 33 33 from solstone.think.cogitate_contract import ( 34 34 capabilities_for_access_tier, 35 + cogitate_journal_command_list, 35 36 expects_emit_final, 36 37 ) 37 38 from solstone.think.cogitate_policy import ( ··· 779 780 class SolAction(Action): 780 781 command: str = Field( 781 782 description=( 782 - "Single `sol` or approved `journal` command-line invocation to " 783 - "run directly, without a shell." 783 + "Single command-line invocation to run directly, without a shell: " 784 + "use `sol`/`sol call ...` for normal journal access; run approved " 785 + "`journal` families (" 786 + + cogitate_journal_command_list() 787 + + ") directly as `journal <family> ...`, never prefixed with " 788 + "`sol` or `sol call`." 784 789 ) 785 790 ) 786 791 ··· 937 942 ) 938 943 tool = sol_tool_cls( 939 944 description=( 940 - "Run one policy-approved `sol` or `journal` command-line invocation " 941 - "directly, without a shell." 945 + "Run one policy-approved command directly, without a shell: use " 946 + "`sol`/`sol call ...` for normal journal access; run approved " 947 + "`journal` families (" 948 + + cogitate_journal_command_list() 949 + + ") directly as `journal <family> ...`, never prefixed with " 950 + "`sol` or `sol call`." 942 951 ), 943 952 action_type=sol_action, 944 953 observation_type=sol_observation,
+3 -1
solstone/think/talent_cli.py
··· 40 40 41 41 from solstone.think.cogitate_contract import ( 42 42 COGITATE_ACCESS_TIERS, 43 + COGITATE_JOURNAL_COMMANDS, 43 44 COGITATE_READ_TOOL_NAMES, 44 45 capabilities_for_access_tier, 45 46 expects_emit_final, ··· 471 472 472 473 def _scan_command_examples(body: str, *, cap: int = 6) -> list[str]: 473 474 """Scan prompt body text for command examples.""" 475 + journal_alternation = "|".join(COGITATE_JOURNAL_COMMANDS) 474 476 pattern = re.compile( 475 477 r"`(?P<cmd>(?:sol\s+call\s+[^\n`]+|journal\s+" 476 - r"(?:identity|health|talent)\b[^\n`]*))`" 478 + rf"(?:{journal_alternation})\b[^\n`]*))`" 477 479 ) 478 480 seen: set[str] = set() 479 481 result: list[str] = []
+1 -2
tests/test_check_cogitate_prompts.py
··· 79 79 ( 80 80 2, 81 81 "bare-journal", 82 - "forbidden `journal supervisor`; use `journal` with one of " 83 - "{identity, health, talent}, or use `sol`/`sol call`", 82 + f"forbidden `journal supervisor`; {ccp.JOURNAL_ALTERNATIVE}", 84 83 ) 85 84 ] 86 85
+3 -1
tests/test_cli_provider.py
··· 75 75 for tool_name in ("Bash", "run_shell_command", "bash"): 76 76 hint = cogitate_sol_tool_hint(tool_name) 77 77 assert tool_name in hint 78 - assert "Do not invent or call a tool literally named `sol`." in hint 78 + assert "Do not invent or call a tool literally named `sol`" in hint 79 79 assert 'command="sol call activities list"' in hint 80 + assert 'command="journal identity partner"' in hint 81 + assert "must be run unprefixed as `journal <family> ...`" in hint 80 82 81 83 def test_assemble_prompt_appends_sol_tool_hint_when_provided(self): 82 84 body, system = assemble_prompt(
+56 -2
tests/test_cogitate_contract.py
··· 1 1 # SPDX-License-Identifier: AGPL-3.0-only 2 2 # Copyright (c) 2026 sol pbc 3 3 4 + import ast 4 5 from pathlib import Path 5 6 6 7 import pytest ··· 9 10 from solstone.think.cogitate_contract import ( 10 11 COGITATE_ACCESS_TIERS, 11 12 COGITATE_DIAGNOSTIC_PREAMBLE, 13 + COGITATE_JOURNAL_COMMANDS, 12 14 COGITATE_READ_TOOL_NAMES, 13 15 COGITATE_RUNTIME_PREAMBLE, 14 16 FUTURE_ACCESS_TIERS, ··· 81 83 "glob", 82 84 "grep_search", 83 85 ) 86 + assert COGITATE_JOURNAL_COMMANDS == ("identity", "health", "talent") 87 + assert cogitate_contract.cogitate_journal_command_list() == ( 88 + "identity, health, talent" 89 + ) 84 90 assert FUTURE_ACCESS_TIERS == ("code-agent",) 85 91 assert TALENT_ACCESS_TIERS == ( 86 92 "normal", ··· 146 152 def test_cogitate_runtime_preamble_content_guard(): 147 153 assert "sol call ..." in COGITATE_RUNTIME_PREAMBLE 148 154 assert "single parsed command-line invocation" in COGITATE_RUNTIME_PREAMBLE 155 + assert "approved host command families" in COGITATE_RUNTIME_PREAMBLE 156 + assert "journal <family> ..." in COGITATE_RUNTIME_PREAMBLE 157 + assert "never prefixed with `sol` or `sol call`" in COGITATE_RUNTIME_PREAMBLE 158 + assert "no bare `journal ...` commands" not in COGITATE_RUNTIME_PREAMBLE 159 + assert "no other bare `journal ...` family" in COGITATE_RUNTIME_PREAMBLE 149 160 assert "journal root" in COGITATE_RUNTIME_PREAMBLE 150 161 assert "node_modules" in COGITATE_RUNTIME_PREAMBLE 151 162 assert "emit_final" in COGITATE_RUNTIME_PREAMBLE ··· 167 178 assert "grep_search" not in system 168 179 assert "through the `sol` tool" not in system 169 180 assert "Limit filesystem reads" not in system 170 - assert "through a `sol` domain command" in COGITATE_RUNTIME_PREAMBLE 181 + assert "through an approved journal command" in COGITATE_RUNTIME_PREAMBLE 171 182 assert "no MCP tools" in COGITATE_RUNTIME_PREAMBLE 172 - assert "no bare `journal ...` commands" in COGITATE_RUNTIME_PREAMBLE 183 + assert "approved host command families" in COGITATE_RUNTIME_PREAMBLE 184 + assert "no bare `journal ...` commands" not in COGITATE_RUNTIME_PREAMBLE 185 + assert "no other bare `journal ...` family" in COGITATE_RUNTIME_PREAMBLE 173 186 assert "no shell composition" in COGITATE_RUNTIME_PREAMBLE 174 187 assert "read_file" in COGITATE_RUNTIME_PREAMBLE 175 188 assert "list_directory" in COGITATE_RUNTIME_PREAMBLE ··· 192 205 assert closing_fence 193 206 194 207 assert block == COGITATE_RUNTIME_PREAMBLE.rstrip("\n") 208 + 209 + 210 + def test_cogitate_journal_command_vocabulary_not_retyped(): 211 + repo_root = Path(__file__).resolve().parents[1] 212 + allowed = { 213 + Path("solstone/think/cogitate_contract.py"), 214 + Path("tests/test_cogitate_contract.py"), 215 + } 216 + roots = [repo_root / "solstone", repo_root / "scripts", repo_root / "tests"] 217 + findings: list[str] = [] 218 + family_set = set(COGITATE_JOURNAL_COMMANDS) 219 + list_markers = ( 220 + "identity, health, talent", 221 + "{identity, health, talent}", 222 + "identity|health|talent", 223 + ) 224 + 225 + for root in roots: 226 + for path in sorted(root.rglob("*.py")): 227 + rel = path.relative_to(repo_root) 228 + if rel in allowed: 229 + continue 230 + text = path.read_text(encoding="utf-8") 231 + tree = ast.parse(text, filename=str(rel)) 232 + for node in ast.walk(tree): 233 + if isinstance(node, ast.Constant) and isinstance(node.value, str): 234 + if any(marker in node.value for marker in list_markers): 235 + findings.append(f"{rel}:{node.lineno}") 236 + elif isinstance(node, (ast.Tuple, ast.List, ast.Set)): 237 + values = { 238 + elt.value 239 + for elt in node.elts 240 + if isinstance(elt, ast.Constant) and isinstance(elt.value, str) 241 + } 242 + if family_set <= values: 243 + findings.append(f"{rel}:{node.lineno}") 244 + 245 + assert findings == [], ( 246 + "render from `COGITATE_JOURNAL_COMMANDS` in " 247 + "solstone/think/cogitate_contract.py: " + ", ".join(findings) 248 + )
+79
tests/test_cogitate_policy.py
··· 73 73 @pytest.mark.parametrize( 74 74 "command", 75 75 [ 76 + "journal identity partner", 76 77 "journal health logs --since 1h", 77 78 "journal talent logs --daily -c 10", 78 79 ], ··· 81 82 policy = _policy(tmp_path) 82 83 83 84 allowed, reason = policy.check("run_shell_command", {"command": command}) 85 + 86 + assert allowed is True 87 + assert reason == "ok" 88 + 89 + 90 + @pytest.mark.parametrize( 91 + ("command", "reason"), 92 + [ 93 + ( 94 + "sol call journal identity partner", 95 + "policy_deny: `sol call journal identity` is not a `sol call` verb; " 96 + "`identity` is an approved host command family — run it directly as " 97 + "`journal identity partner`", 98 + ), 99 + ( 100 + "sol call journal identity partner --update-section 'work patterns' --value x", 101 + "policy_deny: `sol call journal identity` is not a `sol call` verb; " 102 + "`identity` is an approved host command family — run it directly as " 103 + "`journal identity partner --update-section 'work patterns' --value x`", 104 + ), 105 + ( 106 + "sol call journal health", 107 + "policy_deny: `sol call journal health` is not a `sol call` verb; " 108 + "`health` is an approved host command family — run it directly as " 109 + "`journal health`", 110 + ), 111 + ( 112 + "sol call journal talent logs", 113 + "policy_deny: `sol call journal talent` is not a `sol call` verb; " 114 + "`talent` is an approved host command family — run it directly as " 115 + "`journal talent logs`", 116 + ), 117 + ], 118 + ) 119 + def test_policy_denies_hybrid_journal_invocations_with_repair( 120 + tmp_path, command, reason 121 + ): 122 + policy = _policy(tmp_path) 123 + 124 + allowed, actual_reason = policy.check("run_shell_command", {"command": command}) 125 + 126 + assert allowed is False 127 + assert actual_reason == reason 128 + 129 + 130 + @pytest.mark.parametrize( 131 + ("command", "reason"), 132 + [ 133 + ( 134 + 'journal search "partner"', 135 + "policy_deny: `journal search` is not a host command; run " 136 + "`sol call journal search partner` instead", 137 + ), 138 + ( 139 + "journal facet show work", 140 + "policy_deny: `journal facet` is not a host command; run " 141 + "`sol call journal facet show work` instead", 142 + ), 143 + ], 144 + ) 145 + def test_policy_denies_bare_journal_search_and_facet_with_repair( 146 + tmp_path, command, reason 147 + ): 148 + policy = _policy(tmp_path) 149 + 150 + allowed, actual_reason = policy.check("run_shell_command", {"command": command}) 151 + 152 + assert allowed is False 153 + assert actual_reason == reason 154 + 155 + 156 + def test_policy_allows_sol_call_journal_search_with_hybrid_text(tmp_path): 157 + policy = _policy(tmp_path) 158 + 159 + allowed, reason = policy.check( 160 + "run_shell_command", 161 + {"command": 'sol call journal search "sol call journal identity"'}, 162 + ) 84 163 85 164 assert allowed is True 86 165 assert reason == "ok"
+70
tests/test_openhands_sol_tool.py
··· 7 7 8 8 import pytest 9 9 10 + from solstone.think.cogitate_contract import cogitate_journal_command_list 10 11 from solstone.think.cogitate_policy import CogitatePolicy 11 12 from solstone.think.providers import local_admission, openhands 12 13 from solstone.think.providers.local_admission import LocalSlotLease ··· 57 58 ) 58 59 59 60 61 + def test_sol_tool_descriptions_name_direct_journal_rule( 62 + fake_openhands, 63 + tmp_path, 64 + ): 65 + tool, _executor = _sol_tool_and_executor(tmp_path=tmp_path, events=[]) 66 + family_list = cogitate_journal_command_list() 67 + 68 + assert tool.description == ( 69 + "Run one policy-approved command directly, without a shell: use " 70 + "`sol`/`sol call ...` for normal journal access; run approved " 71 + f"`journal` families ({family_list}) directly as " 72 + "`journal <family> ...`, never prefixed with `sol` or `sol call`." 73 + ) 74 + assert openhands.SolAction.command.description == ( 75 + "Single command-line invocation to run directly, without a shell: use " 76 + "`sol`/`sol call ...` for normal journal access; run approved " 77 + f"`journal` families ({family_list}) directly as " 78 + "`journal <family> ...`, never prefixed with `sol` or `sol call`." 79 + ) 80 + 81 + 60 82 def test_read_only_allowed_sol_call_returns_non_error_observation( 61 83 fake_openhands, 62 84 fixed_time, ··· 113 135 114 136 assert observation.is_error is True 115 137 assert observation.text.startswith("policy_deny:") 138 + assert executor.read_call_count == 0 139 + assert events == [] 140 + 141 + 142 + @pytest.mark.parametrize( 143 + ("command", "reason"), 144 + [ 145 + ( 146 + "sol call journal identity partner", 147 + "policy_deny: `sol call journal identity` is not a `sol call` verb; " 148 + "`identity` is an approved host command family — run it directly as " 149 + "`journal identity partner`", 150 + ), 151 + ( 152 + 'journal search "partner"', 153 + "policy_deny: `journal search` is not a host command; run " 154 + "`sol call journal search partner` instead", 155 + ), 156 + ( 157 + "journal facet show work", 158 + "policy_deny: `journal facet` is not a host command; run " 159 + "`sol call journal facet show work` instead", 160 + ), 161 + ], 162 + ) 163 + def test_policy_repair_denies_are_recoverable_without_running_command( 164 + fake_openhands, 165 + fixed_time, 166 + tmp_path, 167 + monkeypatch, 168 + command, 169 + reason, 170 + ): 171 + events: list[dict] = [] 172 + tool, executor = _sol_tool_and_executor( 173 + tmp_path=tmp_path, 174 + events=events, 175 + ) 176 + monkeypatch.setattr( 177 + openhands, 178 + "_run_command", 179 + lambda _argv: pytest.fail("denied commands must not run"), 180 + ) 181 + 182 + observation = tool(tool.action_from_arguments({"command": command})) 183 + 184 + assert observation.is_error is True 185 + assert observation.text == reason 116 186 assert executor.read_call_count == 0 117 187 assert events == [] 118 188
+17
tests/test_talent_cli.py
··· 448 448 ] 449 449 450 450 451 + def test_real_partner_prompt_uses_direct_journal_identity_without_contradiction(): 452 + config = get_talent("partner") 453 + 454 + prompt_body, system_instruction = assemble_prompt(config, sol_tool_name="sol") 455 + 456 + assert system_instruction is not None 457 + assert "approved host command families" in system_instruction 458 + assert "journal <family> ..." in system_instruction 459 + assert "never prefixed with `sol` or `sol call`" in system_instruction 460 + assert "no bare `journal ...` commands" not in system_instruction 461 + assert "no other bare `journal ...` family" in system_instruction 462 + assert "`journal identity partner` through the provided" in prompt_body 463 + assert "`journal identity partner --update-section`" in prompt_body 464 + assert "`sol`-surface read form" not in prompt_body 465 + assert "there is no `sol call` verb" not in prompt_body 466 + 467 + 451 468 def test_inventory_degrades_bad_access_tier_in_rows_table_and_json( 452 469 tmp_path, 453 470 monkeypatch,