Skip to content

Commit b4fc734

Browse files
committed
Another deep audit and production-hardening pass is complete. Here is what landed:
Write-path enforcement (fail-closed) _pre_tool_call in roadmap_hooks.py — blocks write_file / patch targeting ROADMAP.md outside the resolved project workspace before the tool runs (kernel/joyzoning pattern). Wired into hooks.py pre-tool chain: kernel_pre → jz_pre → roadmap_pre. Config: block_writes_outside_workspace: true (default). Bootstrap completeness gate New bootstrap_complete gate in gate.py — detects unfilled template guidance via workspace state. Optional kanban block via block_kanban_on_bootstrap_incomplete: false (default; opt-in). Operator hints recommend fill_bootstrap when placeholders remain. Agent ergonomics Session hooks use build_steering_context() — emit roadmap_path, workspace_source, handle unresolved workspace gracefully. JoyZoning next_actions now surface ROADMAP path and bootstrap-fill hints. Session brief includes bootstrap_complete / bootstrap_placeholder_count. Evidence-driven bootstrap (fewer generic phrases) bootstrap_skeleton_from_evidence() derives users, workflows, architecture from README/git evidence instead of static template lines. Skeleton no longer emits phrases like “Derived from README and config evidence during bootstrap.” Audit + tests Audit covers pre_tool_call block, joyzoning merge hints, and production-language scan exceptions for bootstrap gate labels. New tests: RoadmapHooksTests (pre_tool_call block + optional bootstrap gate). Verification make verify → smoke OK, audit OK, 58 tests OK Opt-in knobs in ~/.hermes/config.yaml: dietcode: roadmap: block_writes_outside_workspace: true # blocks bad ROADMAP writes at pre_tool_call block_kanban_on_bootstrap_incomplete: false # set true to block kanban until bootstrap filled Run make deploy-fast to sync to your active Hermes plugin if you want these changes live in the IDE.
1 parent e6ff3a5 commit b4fc734

20 files changed

Lines changed: 941 additions & 91 deletions

‎.dietcode/roadmap-state.json‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"last_validated_at": "2026-06-10T22:14:53Z",
3+
"schema_valid": false,
4+
"health_status": null,
5+
"recent_checkpoint_date": null,
6+
"phase": "validate_pending",
7+
"validation_issue_count": 1,
8+
"validation_pending": false,
9+
"updated_at": "2026-06-10T22:14:53Z"
10+
}

‎lib/agent/roadmap/cockpit.py‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,19 @@
1212
from plugins.dietcode.lib.agent.roadmap.progress import read_current, read_last_error
1313
from plugins.dietcode.lib.agent.roadmap.snapshot import get_workspace_snapshot
1414
from plugins.dietcode.lib.agent.roadmap.skill_install import _SKILL_REL
15+
from plugins.dietcode.lib.agent.roadmap.steering_context import build_steering_context
1516

1617

1718
def build_cockpit_payload(*, workspace: Optional[str] = None) -> dict[str, Any]:
1819
cfg = get_roadmap_config()
19-
root = resolve_workspace_root(workspace)
20+
if workspace and str(workspace).strip():
21+
root = resolve_workspace_root(workspace)
22+
workspace_source = "explicit"
23+
else:
24+
from plugins.dietcode.lib.agent.roadmap.config import resolve_workspace
25+
26+
root, workspace_source = resolve_workspace()
27+
steering = build_steering_context(workspace=root)
2028
snap = get_workspace_snapshot(root, tier="full")
2129
evidence = snap.evidence
2230
roadmap = evidence.get("roadmap") or {}
@@ -57,6 +65,10 @@ def build_cockpit_payload(*, workspace: Optional[str] = None) -> dict[str, Any]:
5765
"ok": True,
5866
"generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
5967
"workspace": root,
68+
"workspace_source": workspace_source,
69+
"workspace_safe": steering.get("workspace_safe", True),
70+
"bootstrap_complete": ws_state.get("bootstrap_complete"),
71+
"bootstrap_placeholder_count": ws_state.get("bootstrap_placeholder_count"),
6072
"enabled": cfg.enabled,
6173
"skill_path": _SKILL_REL,
6274
"roadmap_exists": bool(roadmap.get("exists")),
@@ -96,10 +108,15 @@ def format_cockpit_report(*, workspace: Optional[str] = None) -> str:
96108
lines = [
97109
"🗺️ Roadmap cockpit",
98110
f"Workspace: {data.get('workspace')}",
111+
f"ROADMAP.md: {data.get('roadmap_path')}",
112+
]
113+
if data.get("workspace_source"):
114+
lines.append(f"Workspace source: {data['workspace_source']}")
115+
lines.extend([
99116
f"Phase: {data.get('phase')}",
100117
f"Enabled: {data.get('enabled')}",
101118
"",
102-
]
119+
])
103120

104121
if data.get("roadmap_exists"):
105122
lines.append(f"ROADMAP.md: present | health={data.get('health_status') or 'unparsed'}")
@@ -125,6 +142,10 @@ def format_cockpit_report(*, workspace: Optional[str] = None) -> str:
125142
for sig in signals[:3]:
126143
lines.append(f" • {sig.get('code')}: {sig.get('detail')}")
127144

145+
if data.get("bootstrap_complete") is False and data.get("bootstrap_placeholder_count"):
146+
lines.append(
147+
f"⚠️ Bootstrap placeholders: {data['bootstrap_placeholder_count']} — replace template text before closing pass"
148+
)
128149
lines.append("")
129150
lines.append(data.get("operator_summary") or "")
130151
if data.get("progress_phase"):

‎lib/agent/roadmap/config.py‎

Lines changed: 121 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
import time
66
from dataclasses import dataclass
7+
from pathlib import Path
78
from typing import Optional
89

910
_config_cache: Optional["RoadmapConfig"] = None
@@ -67,24 +68,133 @@ def get_roadmap_config() -> RoadmapConfig:
6768
return _config_cache
6869

6970

70-
def resolve_workspace_root(explicit: Optional[str] = None) -> str:
71-
"""Resolve the project workspace for ROADMAP.md."""
71+
class RoadmapWorkspaceError(ValueError):
72+
"""Project workspace for ROADMAP.md could not be resolved safely."""
73+
74+
75+
def _reject_quarantined(root: str) -> None:
76+
try:
77+
from plugins.dietcode.lib.kernel_workspace import is_quarantined_root
78+
except ImportError:
79+
return
80+
if is_quarantined_root(root):
81+
raise RoadmapWorkspaceError(
82+
f"ROADMAP.md belongs in your Hermes project workspace, not the DietCode plugin tree: {root}. "
83+
"Set kanban.workspace or HERMES_KANBAN_WORKSPACE to your project root."
84+
)
85+
86+
87+
def _candidate_from_env() -> tuple[Optional[str], str]:
88+
for key in (
89+
"HERMES_KANBAN_WORKSPACE",
90+
"JOYZONING_WORKSPACE_ROOT",
91+
"DIETCODE_WORKSPACE_ROOT",
92+
):
93+
val = os.environ.get(key, "").strip()
94+
if not val:
95+
continue
96+
root = str(Path(val).expanduser().resolve())
97+
try:
98+
from plugins.dietcode.lib.kernel_workspace import is_quarantined_root
99+
100+
if is_quarantined_root(root):
101+
continue
102+
except ImportError:
103+
pass
104+
return root, key
105+
return None, "env:unset"
106+
107+
108+
def _candidate_from_kanban_config() -> tuple[Optional[str], str]:
109+
try:
110+
from hermes_cli.config import load_config
111+
112+
raw = load_config()
113+
if not isinstance(raw, dict):
114+
return None, "kanban:unset"
115+
kanban = raw.get("kanban", {})
116+
if not isinstance(kanban, dict):
117+
return None, "kanban:unset"
118+
ws = str(kanban.get("workspace") or kanban.get("workspace_root") or "").strip()
119+
if not ws:
120+
return None, "kanban:unset"
121+
root = str(Path(ws).expanduser().resolve())
122+
try:
123+
from plugins.dietcode.lib.kernel_workspace import is_quarantined_root
124+
125+
if is_quarantined_root(root):
126+
return None, "kanban:quarantined"
127+
except ImportError:
128+
pass
129+
return root, "kanban.workspace"
130+
except Exception:
131+
return None, "kanban:unset"
132+
133+
134+
def resolve_workspace(*, explicit: Optional[str] = None) -> tuple[str, str]:
135+
"""Resolve the user project workspace for ROADMAP.md (never plugin/kernel trees).
136+
137+
Returns ``(absolute_path, resolution_source)``.
138+
"""
72139
if explicit and str(explicit).strip():
73-
from pathlib import Path
140+
root = str(Path(explicit).expanduser().resolve())
141+
_reject_quarantined(root)
142+
return root, "explicit"
74143

75-
return str(Path(explicit).expanduser().resolve())
144+
try:
145+
from plugins.dietcode.lib.kernel_workspace import (
146+
is_quarantined_root,
147+
resolve_workspace_root as resolve_kernel_workspace,
148+
)
149+
150+
report = resolve_kernel_workspace()
151+
candidate = report.resolved_workspace_root
152+
if candidate and not is_quarantined_root(candidate):
153+
return candidate, report.resolution_detail
154+
except Exception:
155+
pass
76156

77157
try:
78158
from plugins.dietcode.lib.agent.joyzoning.jsdp_harness_client import (
79159
resolve_workspace_root as _jz_resolve,
80160
)
81161

82-
return _jz_resolve(explicit=None)
162+
jz_root = _jz_resolve(explicit=None)
163+
if jz_root:
164+
try:
165+
from plugins.dietcode.lib.kernel_workspace import is_quarantined_root
166+
167+
if not is_quarantined_root(jz_root):
168+
return jz_root, "joyzoning.jsdp"
169+
except ImportError:
170+
return jz_root, "joyzoning.jsdp"
83171
except Exception:
84-
from pathlib import Path
172+
pass
173+
174+
env_root, env_source = _candidate_from_env()
175+
if env_root:
176+
return env_root, env_source
85177

86-
for key in ("HERMES_KANBAN_WORKSPACE", "JOYZONING_WORKSPACE_ROOT"):
87-
val = os.environ.get(key, "").strip()
88-
if val:
89-
return str(Path(val).expanduser().resolve())
90-
return str(Path.cwd().resolve())
178+
kanban_root, kanban_source = _candidate_from_kanban_config()
179+
if kanban_root:
180+
return kanban_root, kanban_source
181+
182+
cwd = str(Path.cwd().resolve())
183+
try:
184+
from plugins.dietcode.lib.kernel_workspace import is_quarantined_root
185+
186+
if not is_quarantined_root(cwd):
187+
return cwd, "cwd"
188+
except ImportError:
189+
return cwd, "cwd"
190+
191+
raise RoadmapWorkspaceError(
192+
"Could not resolve a project workspace for ROADMAP.md. "
193+
"Set kanban.workspace in ~/.hermes/config.yaml or export HERMES_KANBAN_WORKSPACE "
194+
"to your project root (not ~/.hermes/plugins/dietcode)."
195+
)
196+
197+
198+
def resolve_workspace_root(explicit: Optional[str] = None) -> str:
199+
"""Resolve the project workspace for ROADMAP.md."""
200+
return resolve_workspace(explicit=explicit)[0]

‎lib/agent/roadmap/doctor.py‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,51 @@
1414

1515
def run_checks(*, workspace: Optional[str] = None) -> dict[str, Any]:
1616
cfg = get_roadmap_config()
17-
root = resolve_workspace_root(workspace)
17+
from plugins.dietcode.lib.agent.roadmap.config import RoadmapWorkspaceError, resolve_workspace
18+
19+
workspace_source = "explicit"
20+
try:
21+
if workspace and str(workspace).strip():
22+
root = resolve_workspace_root(workspace)
23+
else:
24+
root, workspace_source = resolve_workspace()
25+
except RoadmapWorkspaceError as exc:
26+
return {
27+
"success": False,
28+
"ok": False,
29+
"workspace": None,
30+
"workspace_source": "unresolved",
31+
"enabled": cfg.enabled,
32+
"checks": [{"name": "workspace_resolved", "ok": False, "detail": str(exc)}],
33+
"recommendations": [
34+
"Set kanban.workspace in ~/.hermes/config.yaml",
35+
"export HERMES_KANBAN_WORKSPACE=/path/to/your/project",
36+
],
37+
"recommended_next_action": {
38+
"action": "configure_workspace",
39+
"command": "export HERMES_KANBAN_WORKSPACE=/path/to/project",
40+
"detail": str(exc),
41+
},
42+
}
43+
1844
checks: list[dict[str, Any]] = []
1945
recommendations: list[str] = []
2046

2147
def _check(name: str, ok: bool, detail: str = "") -> None:
2248
checks.append({"name": name, "ok": ok, "detail": detail})
2349

2450
_check("roadmap.enabled", cfg.enabled, "enabled" if cfg.enabled else "disabled in config")
51+
try:
52+
from plugins.dietcode.lib.kernel_workspace import is_quarantined_root
53+
54+
_check(
55+
"workspace_not_plugin_tree",
56+
not is_quarantined_root(root),
57+
f"{root} ({workspace_source})",
58+
)
59+
except ImportError:
60+
pass
61+
2562
_check(
2663
"auto_install_skills",
2764
True,
@@ -129,6 +166,7 @@ def _check(name: str, ok: bool, detail: str = "") -> None:
129166
"success": ok,
130167
"ok": ok,
131168
"workspace": root,
169+
"workspace_source": workspace_source,
132170
"enabled": cfg.enabled,
133171
"checks": checks,
134172
"validation": validation.to_dict() if validation else None,

‎lib/agent/roadmap/errors.py‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ def error_envelope(
4141
def _recovery_for_code(code: str, action: str) -> str:
4242
mapping = {
4343
"roadmap_disabled": "Set dietcode.roadmap.enabled: true in Hermes config",
44-
"workspace_unresolved": "Set HERMES_KANBAN_WORKSPACE or run from project root",
44+
"workspace_unresolved": "Set kanban.workspace in ~/.hermes/config.yaml or export HERMES_KANBAN_WORKSPACE",
45+
"workspace_quarantined": "Point workspace at your project — never ~/.hermes/plugins/dietcode",
4546
"roadmap_missing": "roadmap(action='checkpoint') to bootstrap ROADMAP.md",
4647
"schema_invalid": "roadmap(action='validate') then repair reported issues",
4748
"checkpoint_stale": "roadmap(action='checkpoint', context='stale refresh')",
@@ -82,10 +83,13 @@ def as_tool_error(payload: dict[str, Any]) -> str:
8283

8384

8485
def from_exception(exc: Exception, *, action: str = "") -> dict[str, Any]:
86+
from plugins.dietcode.lib.agent.roadmap.config import RoadmapWorkspaceError
87+
88+
code = "workspace_unresolved" if isinstance(exc, RoadmapWorkspaceError) else "roadmap_failed"
8589
return error_envelope(
86-
code="roadmap_failed",
90+
code=code,
8791
message=str(exc),
8892
action=action,
8993
detail=type(exc).__name__,
90-
safe_to_retry=True,
94+
safe_to_retry=not isinstance(exc, RoadmapWorkspaceError),
9195
)

‎lib/agent/roadmap/gate.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ def _check_checkpoint_fresh(_: dict[str, Any], inputs: dict[str, Any]) -> bool:
9696
return not bool((inputs.get("freshness") or {}).get("stale"))
9797

9898

99+
def _check_workspace_safe(_: dict[str, Any], inputs: dict[str, Any]) -> bool:
100+
try:
101+
from plugins.dietcode.lib.kernel_workspace import is_quarantined_root
102+
except ImportError:
103+
return True
104+
ws = str(inputs.get("workspace") or "")
105+
return bool(ws) and not is_quarantined_root(ws)
106+
107+
99108
def _check_skill_installed(_: dict[str, Any], inputs: dict[str, Any]) -> bool:
100109
root = inputs.get("workspace") or resolve_workspace_root()
101110
skill = (
@@ -125,6 +134,15 @@ def _check_validation_current(_: dict[str, Any], inputs: dict[str, Any]) -> bool
125134
"safe": True,
126135
"blocks_kanban_complete": False,
127136
},
137+
{
138+
"id": "workspace_safe",
139+
"label": "Project workspace (not plugin install tree)",
140+
"is_open": _check_workspace_safe,
141+
"why_closed": "ROADMAP.md must live in the Hermes project workspace, not the DietCode plugin directory",
142+
"fix": "Set kanban.workspace or HERMES_KANBAN_WORKSPACE to your project root",
143+
"safe": True,
144+
"blocks_kanban_complete": True,
145+
},
128146
{
129147
"id": "roadmap_present",
130148
"label": "ROADMAP.md exists",

0 commit comments

Comments
 (0)