feat(agent): add configurable coding_instructions

agent.coding_instructions (a string or list) is appended to the coding brief as
its own stable system block, so users can pin project-wide workflow rules
without editing the shipped brief. Coding-posture only and cache-safe (resolved
once per session; takes effect next session). Empty by default.
This commit is contained in:
Brooklyn Nicholson 2026-06-30 00:59:59 -05:00
parent a10113658b
commit 821d9f709f
4 changed files with 79 additions and 0 deletions

View file

@ -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),
)

View file

@ -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.

View file

@ -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.

View file

@ -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"}})