diff --git a/agent/coding_context.py b/agent/coding_context.py index 8fb51a0b04d..00f6d996d47 100644 --- a/agent/coding_context.py +++ b/agent/coding_context.py @@ -353,6 +353,29 @@ def _coding_mode(config: Optional[dict[str, Any]]) -> str: return "auto" +def _coding_instructions(config: Optional[dict[str, Any]]) -> str: + """Standing operator instructions for the coding posture (config). + + ``agent.coding_instructions`` — a string or list of strings appended to the + coding brief as an extra stable system block, so a user can pin project-wide + coding-workflow rules (e.g. "for UI work don't run tsc/lint until I approve; + clean the diff before committing") without editing the shipped brief. + Cache-safe: resolved once per session into the stable system-prompt tier, + like the rest of the posture. + """ + if config is None: + try: + from hermes_cli.config import load_config + + config = load_config() + except Exception: + config = {} + raw = ((config or {}).get("agent", {}) or {}).get("coding_instructions", "") + if isinstance(raw, (list, tuple)): + return "\n".join(str(item).strip() for item in raw if str(item).strip()) + return str(raw or "").strip() + + def _resolve_cwd(cwd: Optional[str | Path]) -> Path: if cwd: return Path(cwd).expanduser() @@ -459,6 +482,9 @@ class RuntimeMode: # only to steer edit-format guidance toward the model's family — see # ``_edit_format_line``. Fixed for the session, so cache-safe. model: Optional[str] = None + # Standing operator instructions (``agent.coding_instructions``), appended + # as an extra stable system block. Empty unless the user configures it. + instructions: str = "" @property def kind(self) -> str: @@ -505,6 +531,10 @@ class RuntimeMode: workspace = build_coding_workspace_block(self.cwd) if workspace: blocks.append(workspace) + # Operator instructions ride their own block so the brief (block 0) stays + # byte-stable and cache-keyed independently of user config. + if self.instructions: + blocks.append(f"Operator instructions (from config):\n{self.instructions}") return blocks def compact_skill_categories(self) -> frozenset[str]: @@ -557,6 +587,7 @@ def resolve_runtime_mode( cwd=resolved_cwd, config_mode=mode, model=model, + instructions=_coding_instructions(config), ) diff --git a/cli-config.yaml.example b/cli-config.yaml.example index d8f8e6d48a2..504d1a08fe0 100644 --- a/cli-config.yaml.example +++ b/cli-config.yaml.example @@ -646,6 +646,14 @@ agent: # force it on or off; the HERMES_VERIFY_ON_STOP env var (1/0) takes precedence. # verify_on_stop: auto + # Standing operator instructions for the coding posture (when Hermes is in a + # code workspace). Appended to the coding brief as an extra system block, so + # you can pin project-wide workflow rules without editing the shipped brief. + # Accepts a string or a list of strings. Takes effect next session. + # coding_instructions: + # - "For UI work, don't run tsc/lint until I approve the look." + # - "Clean the diff before you commit and push." + # When verify-on-stop finds edited code without fresh verification evidence, # append guidance for creative UI work (avoid broad tsc/lint/test before visual # approval) and clean-diff expectations. Set false to keep that nudge terse. diff --git a/hermes_cli/config.py b/hermes_cli/config.py index f422af82529..98f6b2f818d 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -999,6 +999,13 @@ DEFAULT_CONFIG = { # "on" — force the prompt posture everywhere. # "off" — disable entirely. "coding_context": "auto", + # Standing operator instructions for the coding posture. A string (or + # list of strings) appended to the coding brief as an extra stable + # system block — pin project-wide workflow rules here instead of editing + # the shipped brief, e.g. "For UI work, don't run tsc/lint until I + # approve. Clean the diff before you commit and push." Cache-safe: + # takes effect next session. Empty by default. + "coding_instructions": "", # When verify-on-stop finds edited code without fresh verification # evidence, append guidance for creative UI work (avoid broad # tsc/lint/test before visual approval) and clean-diff expectations. diff --git a/tests/agent/test_coding_context.py b/tests/agent/test_coding_context.py index b6606dfb147..64fdc9c9940 100644 --- a/tests/agent/test_coding_context.py +++ b/tests/agent/test_coding_context.py @@ -353,6 +353,39 @@ class TestRuntimeMode: assert any("coding agent" in b for b in blocks) assert any("Workspace" in b for b in blocks) + def test_coding_instructions_append_their_own_block(self, tmp_path): + _git_init(tmp_path) + cfg = { + "agent": { + "coding_context": "on", + "coding_instructions": "Clean the diff before commit.", + } + } + mode = cc.resolve_runtime_mode(platform="cli", cwd=tmp_path, config=cfg) + blocks = mode.system_blocks() + # The brief stays block 0 (byte-stable, cache-keyed independently); the + # operator instructions ride a separate trailing block. + assert blocks[0] == cc.CODING_AGENT_GUIDANCE + assert any("Clean the diff before commit." in b for b in blocks[1:]) + + def test_coding_instructions_accept_a_list(self, tmp_path): + _git_init(tmp_path) + cfg = { + "agent": { + "coding_context": "on", + "coding_instructions": ["No tsc/lint on UI.", "Clean the diff."], + } + } + mode = cc.resolve_runtime_mode(platform="cli", cwd=tmp_path, config=cfg) + instr_block = mode.system_blocks()[-1] + assert "No tsc/lint on UI." in instr_block + assert "Clean the diff." in instr_block + + def test_no_instructions_block_when_unset(self, tmp_path): + _git_init(tmp_path) + mode = cc.resolve_runtime_mode(platform="cli", cwd=tmp_path, config={"agent": {"coding_context": "on"}}) + assert not any("Operator instructions" in b for b in mode.system_blocks()) + def test_toolset_selection_gated_on_focus(self, tmp_path): _git_init(tmp_path) focus = cc.resolve_runtime_mode(platform="cli", cwd=tmp_path, config={"agent": {"coding_context": "focus"}})