@dotdotgod/hermes 0.0.0-stage → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +94 -0
- package/README.md +126 -2
- package/__init__.py +48 -0
- package/mcp/bridge-entry.mjs +41 -0
- package/mcp/bridge.mjs +16217 -0
- package/mcp/cli.mjs +5842 -0
- package/mcp/server.mjs +23573 -0
- package/mcp/tools.json +749 -0
- package/package.json +37 -3
- package/plugin.yaml +18 -0
- package/policy.py +78 -0
- package/proxy.py +95 -0
- package/runtime.py +239 -0
- package/skills/document-clarify/SKILL.md +41 -0
- package/skills/impact-review/SKILL.md +31 -0
- package/skills/project-initializer/SKILL.md +37 -0
- package/skills/project-initializer/references/agent-docs.md +41 -0
- package/skills/project-initializer/scripts/init_project.sh +525 -0
- package/skills/project-initializer/templates/case-and-evidence.json +224 -0
- package/skills/project-initializer/templates/dotdotgod.config.json +239 -0
- package/skills/project-initializer/templates/policy.json +246 -0
- package/skills/project-initializer/templates/portfolio.json +260 -0
- package/skills/project-initializer/templates/publication.json +270 -0
- package/skills/project-initializer/templates/research.json +292 -0
- package/skills/project-initializer/templates/software.json +239 -0
- package/skills/project-load/SKILL.md +31 -0
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdotgod/hermes",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Hermes CLI and messaging-gateway adapter without Plan Mode.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"license": "Elastic-2.0",
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=22.19.0"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
15
|
+
"esbuild": "0.27.3",
|
|
16
|
+
"@dotdotgod/context": "0.6.0"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"plugin.yaml",
|
|
20
|
+
"__init__.py",
|
|
21
|
+
"runtime.py",
|
|
22
|
+
"proxy.py",
|
|
23
|
+
"policy.py",
|
|
24
|
+
"mcp",
|
|
25
|
+
"skills",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/dotdotgod/dotdotgod-kit.git",
|
|
32
|
+
"directory": "packages/hermes"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build:runtime": "node ../../scripts/build-adapter-runtime.mjs hermes",
|
|
36
|
+
"test": "PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -v -s test -p 'test_*.py' && node --test test/*.test.mjs",
|
|
37
|
+
"verify": "node ../../scripts/build-adapter-runtime.mjs hermes --check && pnpm run test",
|
|
38
|
+
"pack:dry-run": "pnpm pack --dry-run --json"
|
|
39
|
+
}
|
|
6
40
|
}
|
package/plugin.yaml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: dotdotgod
|
|
2
|
+
version: 0.6.0
|
|
3
|
+
description: Project memory, isolated context tools and impact checks for Hermes CLI and messaging gateway; no Plan Mode.
|
|
4
|
+
author: dotdotgod
|
|
5
|
+
license: Elastic-2.0
|
|
6
|
+
config_schema:
|
|
7
|
+
roots:
|
|
8
|
+
type: dict
|
|
9
|
+
default: {}
|
|
10
|
+
description: Repository labels mapped to absolute local paths, configured by the operator.
|
|
11
|
+
gateway_access:
|
|
12
|
+
type: dict
|
|
13
|
+
default: {}
|
|
14
|
+
description: 'Allowed repository labels per platform:sender_id principal; no wildcard grants.'
|
|
15
|
+
cli_root:
|
|
16
|
+
type: str
|
|
17
|
+
default: ''
|
|
18
|
+
description: CLI repository; empty uses the process launch directory.
|
package/policy.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Root-contained changed-file fingerprints and deliberately limited command gates."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
# ponytail: recognize common release/test invocations, not arbitrary shell/program semantics.
|
|
8
|
+
# Keep host approval/sandbox policy; add a host execution-policy integration if stronger isolation is required.
|
|
9
|
+
GATE = re.compile(r"\bgit\b[^\n;|]{0,200}\b(?:commit|push|tag)\b|\b(?:npm|pnpm|yarn)\b[^\n;|]{0,100}\b(?:publish|deploy|verify|test|build|lint|check)\b|\bpytest\b|\bnode\s+--test\b|\bpython\S*\s+-m\s+unittest\b")
|
|
10
|
+
EXCLUDED = (".dotdotgod/", "node_modules/", "dist/", "build/", "coverage/", "docs/plan/", "docs/archive/")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def contained(root, value):
|
|
14
|
+
path = (root / value).resolve()
|
|
15
|
+
if not path.is_relative_to(root):
|
|
16
|
+
raise ValueError("Path escapes selected repository")
|
|
17
|
+
return path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def tracked(path):
|
|
21
|
+
return not path.startswith(EXCLUDED)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def fingerprint(path):
|
|
25
|
+
try:
|
|
26
|
+
with path.open("rb") as stream:
|
|
27
|
+
return hashlib.file_digest(stream, "sha256").hexdigest()
|
|
28
|
+
except FileNotFoundError:
|
|
29
|
+
return "missing"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def snapshot(root):
|
|
33
|
+
# ponytail: hash all dirty bytes per gate; cache Git/file signatures only if profiling warrants it.
|
|
34
|
+
# Dirty and untracked paths only; ignored/local memory is not indexed for impact.
|
|
35
|
+
names = set()
|
|
36
|
+
for args in (["diff", "--name-only", "-z", "HEAD"], ["ls-files", "--others", "--exclude-standard", "-z"]):
|
|
37
|
+
run = subprocess.run(["git", "-C", str(root), *args], capture_output=True, timeout=10)
|
|
38
|
+
if run.returncode:
|
|
39
|
+
# A new repository can have no HEAD; fall back to tracked worktree paths.
|
|
40
|
+
if args[0] == "diff":
|
|
41
|
+
run = subprocess.run(["git", "-C", str(root), "ls-files", "-z"], capture_output=True, timeout=10)
|
|
42
|
+
if run.returncode:
|
|
43
|
+
raise RuntimeError("Impact requires a readable Git worktree; no release commands allowed")
|
|
44
|
+
names.update(run.stdout.decode("utf-8", errors="strict").split("\0"))
|
|
45
|
+
return {name: fingerprint(contained(root, name)) for name in sorted(names) if name and tracked(name)}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def commands(name, args):
|
|
49
|
+
if name.endswith("execute"):
|
|
50
|
+
return [" ".join(str(v) for v in [item.get("command", ""), item.get("executable", ""), *item.get("args", [])])
|
|
51
|
+
for item in args.get("commands", [])]
|
|
52
|
+
return [str(args.get(key, "")) for key in ("command", "code", "goal", "tasks", "data")]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def gate(name, args, pending):
|
|
56
|
+
if not pending:
|
|
57
|
+
return None
|
|
58
|
+
opaque = name.endswith("execute_file") or (name == "process" and args.get("action") in {"write", "submit"})
|
|
59
|
+
if opaque or any(GATE.search(text) for text in commands(name, args)):
|
|
60
|
+
return "Run dotdotgod_project_impact for pending paths before verification/commit/push/publish: " + ", ".join(sorted(pending)[:8])
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def file_arguments(root, name, args):
|
|
65
|
+
# Hermes file tools use path(s); paths in file patches are still subject to host policy.
|
|
66
|
+
if name in {"read_file", "write_file", "file_edit", "file_write", "file_read"}:
|
|
67
|
+
for key in ("path", "file_path"):
|
|
68
|
+
if isinstance(args.get(key), str):
|
|
69
|
+
try:
|
|
70
|
+
target = contained(root, args[key])
|
|
71
|
+
except ValueError:
|
|
72
|
+
target = Path(args[key]).resolve()
|
|
73
|
+
if name not in {"read_file", "file_read"} or not target.is_relative_to(Path(__file__).parent.resolve() / "skills"):
|
|
74
|
+
raise
|
|
75
|
+
args[key] = str(target)
|
|
76
|
+
if name == "terminal":
|
|
77
|
+
args["workdir"] = str(contained(root, args.get("workdir") or "."))
|
|
78
|
+
return args
|
package/proxy.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""A serialized stdio bridge per host session. All context processing stays in Node."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import queue
|
|
5
|
+
import signal
|
|
6
|
+
import subprocess
|
|
7
|
+
import threading
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Proxy:
|
|
12
|
+
def __init__(self, root, session_id):
|
|
13
|
+
self.root = str(Path(root).resolve(strict=True))
|
|
14
|
+
self.session_id = session_id
|
|
15
|
+
self.lock = threading.RLock()
|
|
16
|
+
self.process = None
|
|
17
|
+
self.responses = None
|
|
18
|
+
|
|
19
|
+
def _start(self):
|
|
20
|
+
bridge = Path(__file__).parent / "mcp" / "bridge.mjs"
|
|
21
|
+
self.process = subprocess.Popen(
|
|
22
|
+
["node", str(bridge), self.root, self.session_id], cwd=self.root,
|
|
23
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
24
|
+
text=True, encoding="utf-8", errors="replace", start_new_session=(os.name != "nt"),
|
|
25
|
+
)
|
|
26
|
+
responses = self.responses = queue.Queue()
|
|
27
|
+
process = self.process
|
|
28
|
+
diagnostic = {"stderr": ""}
|
|
29
|
+
|
|
30
|
+
def read_errors():
|
|
31
|
+
for chunk in iter(lambda: process.stderr.read(1024), ""):
|
|
32
|
+
diagnostic["stderr"] = (diagnostic["stderr"] + chunk)[-8000:]
|
|
33
|
+
|
|
34
|
+
errors = threading.Thread(target=read_errors, daemon=True)
|
|
35
|
+
errors.start()
|
|
36
|
+
|
|
37
|
+
def read():
|
|
38
|
+
try:
|
|
39
|
+
for line in process.stdout:
|
|
40
|
+
responses.put(json.loads(line))
|
|
41
|
+
except Exception as error:
|
|
42
|
+
responses.put({"ok": False, "error": str(error)})
|
|
43
|
+
finally:
|
|
44
|
+
errors.join(timeout=0.1)
|
|
45
|
+
responses.put({"ok": False, "error": "MCP bridge disconnected; call not replayed. " + diagnostic["stderr"]})
|
|
46
|
+
|
|
47
|
+
threading.Thread(target=read, daemon=True).start()
|
|
48
|
+
|
|
49
|
+
def call(self, name, arguments=None):
|
|
50
|
+
with self.lock:
|
|
51
|
+
if self.process is None or self.process.poll() is not None:
|
|
52
|
+
self.close()
|
|
53
|
+
self._start()
|
|
54
|
+
try:
|
|
55
|
+
request = {"name": name, "arguments": arguments or {}}
|
|
56
|
+
process, responses = self.process, self.responses
|
|
57
|
+
process.stdin.write(json.dumps(request) + "\n")
|
|
58
|
+
process.stdin.flush()
|
|
59
|
+
response = responses.get(timeout=690)
|
|
60
|
+
if not response.get("ok"):
|
|
61
|
+
raise RuntimeError(response.get("error", "MCP bridge failed"))
|
|
62
|
+
value = response["value"]
|
|
63
|
+
if name == "session_resume" and not value.get("isError"):
|
|
64
|
+
self.session_id = value["structuredContent"]["sessionId"]
|
|
65
|
+
return value
|
|
66
|
+
except Exception:
|
|
67
|
+
self.close()
|
|
68
|
+
raise # Never replay a possibly dispatched mutation.
|
|
69
|
+
|
|
70
|
+
def close(self):
|
|
71
|
+
# Closing is also the explicit cancellation path, so it must not wait for call's lock.
|
|
72
|
+
process, self.process = self.process, None
|
|
73
|
+
if process is None:
|
|
74
|
+
return
|
|
75
|
+
if process.poll() is None:
|
|
76
|
+
# Signal the bridge only: it sends MCP cancellation before closing the server.
|
|
77
|
+
process.terminate()
|
|
78
|
+
try:
|
|
79
|
+
process.wait(timeout=3)
|
|
80
|
+
except subprocess.TimeoutExpired:
|
|
81
|
+
if os.name != "nt":
|
|
82
|
+
try:
|
|
83
|
+
os.killpg(process.pid, signal.SIGKILL)
|
|
84
|
+
except ProcessLookupError:
|
|
85
|
+
pass
|
|
86
|
+
process.kill()
|
|
87
|
+
process.wait(timeout=2)
|
|
88
|
+
if os.name != "nt":
|
|
89
|
+
try:
|
|
90
|
+
os.killpg(process.pid, signal.SIGKILL)
|
|
91
|
+
except ProcessLookupError:
|
|
92
|
+
pass
|
|
93
|
+
for stream in (process.stdin, process.stdout, process.stderr):
|
|
94
|
+
if stream:
|
|
95
|
+
stream.close()
|
package/runtime.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Hermes lifecycle routing; shared Node tools retain their wire contracts."""
|
|
2
|
+
import atexit
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
import threading
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .policy import contained, file_arguments, gate, snapshot
|
|
12
|
+
from .proxy import Proxy
|
|
13
|
+
|
|
14
|
+
BASE = Path(__file__).parent
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def digest(text):
|
|
18
|
+
return hashlib.sha256(text.encode()).hexdigest()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def envelope(data, text=None):
|
|
22
|
+
return {"content": [{"type": "text", "text": text or json.dumps(data, ensure_ascii=False)}], "structuredContent": data}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Session:
|
|
27
|
+
id: str
|
|
28
|
+
principal: str
|
|
29
|
+
platform: str
|
|
30
|
+
root: Path | None = None
|
|
31
|
+
label: str = ""
|
|
32
|
+
proxy: Proxy | None = None
|
|
33
|
+
loaded: bool = False
|
|
34
|
+
parent_id: str = ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Runtime:
|
|
38
|
+
def __init__(self, ctx):
|
|
39
|
+
self.ctx = ctx
|
|
40
|
+
self.sessions = {}
|
|
41
|
+
self.lock = threading.RLock()
|
|
42
|
+
self.launch_root = Path.cwd().resolve()
|
|
43
|
+
atexit.register(self.close_all)
|
|
44
|
+
|
|
45
|
+
def allowed(self, session):
|
|
46
|
+
roots = self.ctx.get_config("roots", {})
|
|
47
|
+
if not isinstance(roots, dict):
|
|
48
|
+
raise ValueError("roots must be an operator-configured mapping")
|
|
49
|
+
if session.platform in {"", "cli"}:
|
|
50
|
+
return roots
|
|
51
|
+
access = self.ctx.get_config("gateway_access", {})
|
|
52
|
+
labels = access.get(session.principal, []) if isinstance(access, dict) else []
|
|
53
|
+
return {label: roots[label] for label in labels if label in roots} if isinstance(labels, list) else {}
|
|
54
|
+
|
|
55
|
+
def session(self, session_id):
|
|
56
|
+
if not session_id or session_id not in self.sessions:
|
|
57
|
+
raise ValueError("No trusted host session; start a turn before using dotdotgod")
|
|
58
|
+
session = self.sessions[session_id]
|
|
59
|
+
if session.root is None:
|
|
60
|
+
raise ValueError("Select an authorized repository with dotdotgod_select_root first")
|
|
61
|
+
if session.parent_id:
|
|
62
|
+
parent = self.session(session.parent_id)
|
|
63
|
+
if parent.root != session.root or parent.principal != session.principal:
|
|
64
|
+
raise PermissionError("Delegating parent repository or principal changed")
|
|
65
|
+
if session.label:
|
|
66
|
+
value = self.allowed(session).get(session.label)
|
|
67
|
+
if not isinstance(value, str) or Path(value).resolve(strict=True) != session.root:
|
|
68
|
+
raise PermissionError("Repository grant changed; select an authorized root again")
|
|
69
|
+
return session
|
|
70
|
+
|
|
71
|
+
def select(self, session, label):
|
|
72
|
+
if session.parent_id and label != self.session(session.parent_id).label:
|
|
73
|
+
raise PermissionError("Delegated work cannot select a different repository")
|
|
74
|
+
value = self.allowed(session).get(label)
|
|
75
|
+
if not isinstance(value, str) or not Path(value).is_absolute():
|
|
76
|
+
raise PermissionError("Unknown or unauthorized repository label")
|
|
77
|
+
root = Path(value).resolve(strict=True)
|
|
78
|
+
if not root.is_dir():
|
|
79
|
+
raise ValueError("Repository root must be a directory")
|
|
80
|
+
if session.proxy:
|
|
81
|
+
session.proxy.close()
|
|
82
|
+
session.root, session.label, session.proxy, session.loaded = root, label, None, False
|
|
83
|
+
self.ctx.state.set("route:" + digest(session.id), {"principal": session.principal, "label": label})
|
|
84
|
+
return envelope({"ok": True, "root": str(root), "label": label})
|
|
85
|
+
|
|
86
|
+
def client(self, session):
|
|
87
|
+
with self.lock:
|
|
88
|
+
if session.proxy is None:
|
|
89
|
+
# Separate connection, but the existing project DB stays shared for the same root.
|
|
90
|
+
session.proxy = Proxy(session.root, digest(session.principal + "\0" + session.id + "\0" + str(session.root)))
|
|
91
|
+
return session.proxy
|
|
92
|
+
|
|
93
|
+
def pending(self, session):
|
|
94
|
+
current = snapshot(session.root)
|
|
95
|
+
checked = self.ctx.state.get("impact:" + digest(str(session.root)), {})
|
|
96
|
+
return {path: value for path, value in current.items() if checked.get(path) != value}
|
|
97
|
+
|
|
98
|
+
def cli(self, session, args):
|
|
99
|
+
run = subprocess.run(["node", str(BASE / "mcp" / "cli.mjs"), *args], cwd=session.root,
|
|
100
|
+
capture_output=True, text=True, timeout=20)
|
|
101
|
+
if run.returncode:
|
|
102
|
+
raise RuntimeError((run.stderr or run.stdout or "CLI unavailable")[:1000])
|
|
103
|
+
return json.loads(run.stdout)
|
|
104
|
+
|
|
105
|
+
def load(self, session, args):
|
|
106
|
+
focus = str(args.get("focus", "")).strip()
|
|
107
|
+
depth = 3 if focus else 5
|
|
108
|
+
try:
|
|
109
|
+
mapping = self.cli(session, ["map", str(session.root), "--depth", str(depth), "--json"])
|
|
110
|
+
except Exception as error:
|
|
111
|
+
mapping = {"fallback": [p for p in ["AGENTS.md", "README.md", "docs/README.md"] if (session.root / p).is_file()],
|
|
112
|
+
"unavailable": str(error)}
|
|
113
|
+
query = None
|
|
114
|
+
if focus:
|
|
115
|
+
try:
|
|
116
|
+
query = self.cli(session, ["query", str(session.root), focus, "--limit", "30", "--json"])
|
|
117
|
+
except Exception as error:
|
|
118
|
+
query = {"unavailable": str(error), "guidance": "Use map/README routing; do not install embeddings without approval."}
|
|
119
|
+
session.loaded = True
|
|
120
|
+
data = {"ok": True, "root": str(session.root), "focus": focus, "documentationMap": mapping, "query": query}
|
|
121
|
+
return envelope(data, "Project memory (map/query evidence; read bodies selectively):\n" + json.dumps(data, ensure_ascii=False) + "\nHelp: dotdotgod --help")
|
|
122
|
+
|
|
123
|
+
def call(self, name, args, session_id):
|
|
124
|
+
with self.lock:
|
|
125
|
+
if name == "dotdotgod_select_root":
|
|
126
|
+
return self.select(self.sessions[session_id], args.get("label"))
|
|
127
|
+
session = self.session(session_id)
|
|
128
|
+
if name == "dotdotgod_impact_status":
|
|
129
|
+
return envelope({"ok": True, "pending": list(self.pending(session))})
|
|
130
|
+
if name == "dotdotgod_project_load":
|
|
131
|
+
if contained(session.root, args.get("root") or ".") != session.root:
|
|
132
|
+
raise ValueError("Use dotdotgod_select_root to change repositories")
|
|
133
|
+
return self.load(session, args)
|
|
134
|
+
if name in {"execute", "execute_file"}:
|
|
135
|
+
reason = self.check(session, name, args)
|
|
136
|
+
if reason:
|
|
137
|
+
raise PermissionError(reason)
|
|
138
|
+
if name == "dotdotgod_project_impact":
|
|
139
|
+
before = self.pending(session)
|
|
140
|
+
paths = [contained(session.root, path).relative_to(session.root).as_posix() for path in args.get("paths", [])]
|
|
141
|
+
args = {**args, "paths": paths}
|
|
142
|
+
value = self.client(session).call(name, args)
|
|
143
|
+
if name == "dotdotgod_project_impact" and not value.get("isError") and value.get("structuredContent", {}).get("ok") is True:
|
|
144
|
+
with self.lock:
|
|
145
|
+
after = self.pending(session)
|
|
146
|
+
key = "impact:" + digest(str(session.root))
|
|
147
|
+
checked = self.ctx.state.get(key, {})
|
|
148
|
+
for path in paths:
|
|
149
|
+
if path in before and before[path] == after.get(path):
|
|
150
|
+
checked[path] = before[path]
|
|
151
|
+
self.ctx.state.set(key, checked)
|
|
152
|
+
return value
|
|
153
|
+
|
|
154
|
+
def check(self, session, name, args):
|
|
155
|
+
try:
|
|
156
|
+
pending = self.pending(session)
|
|
157
|
+
except Exception:
|
|
158
|
+
pending = {"<impact state unavailable>": "unknown"}
|
|
159
|
+
return gate(name, args, pending)
|
|
160
|
+
|
|
161
|
+
def before_tool(self, tool_name, args, session_id="", **kwargs):
|
|
162
|
+
try:
|
|
163
|
+
if tool_name == "dotdotgod_select_root":
|
|
164
|
+
return None
|
|
165
|
+
session = self.session(session_id)
|
|
166
|
+
reason = self.check(session, tool_name, args)
|
|
167
|
+
if reason:
|
|
168
|
+
return {"action": "block", "message": reason}
|
|
169
|
+
rewritten = file_arguments(session.root, tool_name, dict(args))
|
|
170
|
+
if rewritten != args:
|
|
171
|
+
return {"action": "modify", "args": rewritten}
|
|
172
|
+
except Exception as error:
|
|
173
|
+
return {"action": "block", "message": str(error)}
|
|
174
|
+
|
|
175
|
+
def before_llm(self, session_id, user_message, platform="", sender_id="", parent_session_id="", **kwargs):
|
|
176
|
+
with self.lock:
|
|
177
|
+
parent = self.session(parent_session_id) if platform == "subagent" else None
|
|
178
|
+
principal = parent.principal if parent else platform + ":" + sender_id
|
|
179
|
+
if parent:
|
|
180
|
+
platform = parent.platform
|
|
181
|
+
session = self.sessions.get(session_id)
|
|
182
|
+
if session and session.principal != principal:
|
|
183
|
+
if session.proxy:
|
|
184
|
+
session.proxy.close()
|
|
185
|
+
session = None
|
|
186
|
+
if session is None:
|
|
187
|
+
session = Session(session_id, principal, platform)
|
|
188
|
+
self.sessions[session_id] = session
|
|
189
|
+
saved = self.ctx.state.get("route:" + digest(session_id), {})
|
|
190
|
+
if saved.get("principal") == principal and saved.get("label") in self.allowed(session):
|
|
191
|
+
self.select(session, saved["label"])
|
|
192
|
+
elif parent:
|
|
193
|
+
session.root, session.label, session.parent_id = parent.root, parent.label, parent.id
|
|
194
|
+
elif platform in {"", "cli"}:
|
|
195
|
+
session.root = Path(self.ctx.get_config("cli_root", "") or self.launch_root).resolve(strict=True)
|
|
196
|
+
if session.root is None:
|
|
197
|
+
return {"context": "Select a repository using dotdotgod_select_root. Authorized labels: " + json.dumps(list(self.allowed(session)))}
|
|
198
|
+
try:
|
|
199
|
+
self.session(session_id)
|
|
200
|
+
parts = []
|
|
201
|
+
text = user_message if isinstance(user_message, str) else " ".join(p.get("text", "") for p in user_message if isinstance(p, dict))
|
|
202
|
+
if not session.loaded and not re.search(r"(?:^|\s)/?dd:no-load\b|(?:^|\s)/no-load\b", text):
|
|
203
|
+
parts.append("Call dotdotgod_project_load once with an agent-selected focus, then continue the original request. Read AGENTS.md and maintained README indexes selectively.")
|
|
204
|
+
if text.strip():
|
|
205
|
+
try:
|
|
206
|
+
expanded = self.cli(session, ["expand", str(session.root), text, "--json", "--with-impact", "--fuzzy"])
|
|
207
|
+
refs = expanded.get("refs", [])
|
|
208
|
+
if refs:
|
|
209
|
+
parts.append("Reference evidence (non-authoritative): " + json.dumps(refs, ensure_ascii=False))
|
|
210
|
+
except Exception:
|
|
211
|
+
parts.append("Reference expansion unavailable; use README routing and explicit targeted reads.")
|
|
212
|
+
pending = self.pending(session)
|
|
213
|
+
if pending:
|
|
214
|
+
parts.append("Impact pending: " + ", ".join(list(pending)[:8]) + ". Run dotdotgod_project_impact before broad tests or release commands.")
|
|
215
|
+
return {"context": "\n\n".join(parts)} if parts else None
|
|
216
|
+
except Exception as error:
|
|
217
|
+
return {"context": "dotdotgod context unavailable: " + str(error) + "; do not bypass root/impact checks."}
|
|
218
|
+
|
|
219
|
+
def finalize(self, session_id="", **kwargs):
|
|
220
|
+
for child in list(self.sessions.values()):
|
|
221
|
+
if child.parent_id == session_id:
|
|
222
|
+
self.finalize(child.id)
|
|
223
|
+
session = self.sessions.pop(session_id, None)
|
|
224
|
+
if session and session.proxy:
|
|
225
|
+
session.proxy.close()
|
|
226
|
+
|
|
227
|
+
def end_turn(self, session_id="", interrupted=False, **kwargs):
|
|
228
|
+
if interrupted:
|
|
229
|
+
for child in list(self.sessions.values()):
|
|
230
|
+
if child.parent_id == session_id:
|
|
231
|
+
self.end_turn(child.id, interrupted=True)
|
|
232
|
+
session = self.sessions.get(session_id)
|
|
233
|
+
if session and session.proxy:
|
|
234
|
+
session.proxy.close()
|
|
235
|
+
session.proxy = None
|
|
236
|
+
|
|
237
|
+
def close_all(self):
|
|
238
|
+
for sid in list(self.sessions):
|
|
239
|
+
self.finalize(sid)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: document-clarify
|
|
3
|
+
description: Document Clarify for dotdotgod project memory in Hermes CLI and messaging gateway.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- Generated from packages/shared resources by scripts/generate-adapters.mjs. Do not edit this file directly; edit the shared source and run `pnpm run generate`. -->
|
|
7
|
+
|
|
8
|
+
# Document Clarify
|
|
9
|
+
|
|
10
|
+
Use the operator-authorized selected repository. Prefer the native dotdotgod_project_initialize, dotdotgod_project_load and dotdotgod_project_impact tools for their respective workflows. Context tools use dotdotgod_ names; results contain content and structuredContent, not Pi codemode objects. Do not bypass host permissions or pending impact gates. Hermes native delegate_task handles delegation; this adapter has no Plan Mode.
|
|
11
|
+
|
|
12
|
+
## Memory-Area Context
|
|
13
|
+
|
|
14
|
+
Use `dotdotgod config <root> --json` (or the source-checkout CLI equivalent) to obtain resolved `config.areas`; reuse current resolved settings already in context when the project/config has not changed. Normalize the target to a repository-relative path. Check areas in order, applying each area's `excludePaths` before selecting the first matching `paths` entry. Keep the matched area's relevant guidance in context rather than repeatedly injecting the entire configuration. If no area matches, use the document's purpose and repository conventions without inventing metadata.
|
|
15
|
+
|
|
16
|
+
## Goal
|
|
17
|
+
|
|
18
|
+
Make project documentation easier to understand and act on while preserving established behavior, decisions, task state, historical meaning, and traceability.
|
|
19
|
+
|
|
20
|
+
## Editing Checklist
|
|
21
|
+
|
|
22
|
+
Adapted from [Google Technical Writing One](https://developers.google.com/tech-writing/one); these principles supplement repository policy. Apply them directly without requiring runtime web access.
|
|
23
|
+
|
|
24
|
+
- **Audience:** distinguish what readers already know from what they need to learn or do. Add only evidence-backed context that closes that gap.
|
|
25
|
+
- **Terminology:** use one consistent name per concept; explain unfamiliar terms and abbreviations when readers need them. Preserve exact API names and identifiers.
|
|
26
|
+
- **Sentences:** use specific verbs and make responsibility clear. Prefer active voice when it helps; retain passive voice when appropriate and never invent an unknown actor. Keep one main idea per sentence while preserving conditions, exceptions, and logical relationships.
|
|
27
|
+
- **Paragraphs:** focus each paragraph on one topic and make its main point clear near the beginning. Remove sentences that do not support that topic or relocate them within the approved scope.
|
|
28
|
+
- **Lists and tables:** use numbered lists when order matters, bullets otherwise, and parallel phrasing for comparable items. Use tables when readers need to compare the same attributes across items, not merely to decorate prose.
|
|
29
|
+
- **Language:** apply these principles idiomatically in the document's language. Do not impose English word order, a passive-voice ban, or sentence-length quotas on Korean or other languages.
|
|
30
|
+
|
|
31
|
+
## Workflow
|
|
32
|
+
|
|
33
|
+
1. Confirm the target, audience, requested outcome, and edit scope. Diagnose ambiguity, repetition, or missing information before editing; small changes need no separate diagnostic report. Use matched memory-area metadata when available: `clarify` guidance first, then `description`, `label`, and `role`.
|
|
34
|
+
2. Read only the context needed to preserve meaning: the nearest README, directly linked documents, relevant query results, and targeted history when a past decision matters.
|
|
35
|
+
3. Remove obsolete historical behavior and superseded conditions from current specs when maintained source, tests, or confirmed decisions establish that they no longer apply. Describe current behavior directly rather than narrating how it changed. Do not mistake currently supported compatibility behavior for obsolete history. Preserve current requirements, meaningful limitations, and unresolved uncertainty; when retirement is unverified, ask rather than delete. Preserve historical meaning in archives and reports; do not rewrite past records to look current. Apply criteria suited to the document: README navigation and starting points; spec conditions, behavior, and exceptions; test procedures and pass criteria; architecture boundaries, rationale, and constraints; plan ordering, dependencies, and completion criteria; archive decisions and outcomes in their historical context. Repository-specific guidance takes priority. Clarify terms, ownership, headings, links, and runnable examples. Preserve commands, paths, package/API names, requirement strength, conditions, exceptions, unresolved questions, limitations, and evidence unless verified sources support a change. Do not invent missing facts.
|
|
36
|
+
4. Prefer direct affirmative statements. Remove repeated framing, indirect wording, mixed responsibilities, and irrelevant background. Remove negative or absence statements only when deleting them leaves the reader's understanding, actions, and decisions unchanged. Rewrite redundant contrasts affirmatively: “It is not automatic; the user starts it” becomes “The user starts it.” Preserve meaningful prohibitions, unsupported cases, exceptions, security constraints, and unverified status. Use “X is not Y; it is Z” when the contrast resolves a likely ambiguity. Accuracy takes priority over brevity.
|
|
37
|
+
5. Keep edits within the agreed scope. Confirm scope before splitting or moving files, changing meaning, or editing additional documents outside that scope. Update the nearest README when approved structural changes affect navigation. Follow generated markers and canonical-source instructions. For dotdotgod traceability, edit the fenced `json dotdotgod` block as the canonical mapping. Inspect `dotdotgod traceability links <root> --check --json` before using `--write`; the root-wide write can change unrelated documents, so obtain approval if its affected files exceed the agreed scope. Apply ordinary reference policy: shared documents must not depend on concrete local file paths in inline code or Markdown links; inline-code directory, glob, and placeholder usage examples remain allowed. Preserve traceability's separate stricter local-target prohibition.
|
|
38
|
+
6. Ask for a decision when the requested clarification would change established meaning or when current sources conflict.
|
|
39
|
+
7. Run verification that matches the changed surface. Use documentation and traceability checks for ordinary docs; add generation checks, focused package tests, dry-runs, or workspace verification when shared resources or product behavior are affected. Report briefly: what became clearer; important meaning preserved and unresolved questions; checks run and checks not run. Do not equate shorter text with improved accuracy.
|
|
40
|
+
|
|
41
|
+
When CLI-backed routing is unavailable, continue from the target document, nearest README, direct links, and repository conventions.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: impact-review
|
|
3
|
+
description: Impact Review for dotdotgod project memory in Hermes CLI and messaging gateway.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- Generated from packages/shared resources by scripts/generate-adapters.mjs. Do not edit this file directly; edit the shared source and run `pnpm run generate`. -->
|
|
7
|
+
|
|
8
|
+
# Impact Review
|
|
9
|
+
|
|
10
|
+
Use the operator-authorized selected repository. Prefer the native dotdotgod_project_initialize, dotdotgod_project_load and dotdotgod_project_impact tools for their respective workflows. Context tools use dotdotgod_ names; results contain content and structuredContent, not Pi codemode objects. Do not bypass host permissions or pending impact gates. Hermes native delegate_task handles delegation; this adapter has no Plan Mode.
|
|
11
|
+
|
|
12
|
+
## Goal
|
|
13
|
+
|
|
14
|
+
Review task-relevant source, config, and documentation changes with dotdotgod impact evidence before broad verification, commits, pushes, publishing, or final handoff. Claude Code and Codex use this advisory workflow; trusted runtime or project hooks may enforce a stricter boundary.
|
|
15
|
+
|
|
16
|
+
## Workflow
|
|
17
|
+
|
|
18
|
+
1. Build the changed-file set from Git status. Preserve unrelated user work and exclude dependencies, caches, build output, secrets, and unrelated local memory.
|
|
19
|
+
2. Run one bounded multi-seed impact command for up to 20 unique paths in first-seen order:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
dotdotgod graph impact <root> --changed <path-a> --changed <path-b> --compact
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Use `--yml` when structured detail helps. Split larger sets into ordered batches of 20. In a source checkout, use `node packages/cli/bin/dotdotgod.mjs graph impact`; when the CLI is unavailable, follow README indexes, traceability, package metadata, and focused file search.
|
|
26
|
+
3. Review the combined ranking and each seed's non-seed top results. Prioritize high-signal specs, tests, architecture, and source links, especially `implemented_by`, `verified_by`, and `related_doc` relationships.
|
|
27
|
+
4. Resolve stale or contradictory related docs and tests that belong to the current task.
|
|
28
|
+
5. Select focused tests, documentation validation, generated-resource checks, and package dry-runs from the evidence. For dotdotgod documentation changes, run `dotdotgod validate . --include-local-memory` and add `--check-index` when index freshness matters.
|
|
29
|
+
6. Report the changed files reviewed, the strongest related findings and follow-up actions, and verification completed or skipped.
|
|
30
|
+
|
|
31
|
+
Keep raw impact payloads out of normal summaries. Optional hooks may remind agents about this workflow; default package resources leave full verification, index rebuilds, initialization, archive moves, and write blocking under explicit workflow control.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: project-initializer
|
|
3
|
+
description: Project Initializer for dotdotgod project memory in Hermes CLI and messaging gateway.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- Generated from packages/shared resources by scripts/generate-adapters.mjs. Do not edit this file directly; edit the shared source and run `pnpm run generate`. -->
|
|
7
|
+
|
|
8
|
+
# Project Initializer
|
|
9
|
+
|
|
10
|
+
Use the operator-authorized selected repository. Prefer the native dotdotgod_project_initialize, dotdotgod_project_load and dotdotgod_project_impact tools for their respective workflows. Context tools use dotdotgod_ names; results contain content and structuredContent, not Pi codemode objects. Do not bypass host permissions or pending impact gates. Hermes native delegate_task handles delegation; this adapter has no Plan Mode.
|
|
11
|
+
|
|
12
|
+
## Goal
|
|
13
|
+
|
|
14
|
+
Create a non-destructive dotdotgod baseline with canonical agent instructions, thin agent entrypoints, documentation indexes, a project config selected from an appropriate initialization template, and local-memory ignore rules.
|
|
15
|
+
|
|
16
|
+
## Template Selection
|
|
17
|
+
|
|
18
|
+
1. Inspect existing agent instructions, the root README, docs indexes, major top-level directories, config, and ignore rules.
|
|
19
|
+
2. Discover custom templates as individual JSON files under `~/.dotdotgod/templates/`. Built-in templates are `software`, `research`, `case-and-evidence`, `publication`, `portfolio`, and `policy`.
|
|
20
|
+
3. Choose from project evidence:
|
|
21
|
+
- `software`: application, library, or infrastructure projects organized around specs, architecture, source, and tests.
|
|
22
|
+
- `research`: research diaries, dated records, reports, artifacts, and evaluation evidence.
|
|
23
|
+
- `case-and-evidence`: canonical case records, evidence or legal grounds, and case outputs.
|
|
24
|
+
- `publication`: briefs, outlines, chapters, claims, manuscripts, and research sources.
|
|
25
|
+
- `portfolio`: strategy, positions, ledgers, journals, and market research.
|
|
26
|
+
- `policy`: policy sections, integrated proposals, evidence, and submission outputs.
|
|
27
|
+
4. Consider custom templates by filename and config contents. A same-name custom template replaces the built-in template completely.
|
|
28
|
+
5. Ask the user when multiple choices are plausible and materially change memory areas or traceability. With insufficient evidence, use `~/.dotdotgod/config.json` `defaultTemplate`, then `software`.
|
|
29
|
+
6. Report the selected template and a concise reason.
|
|
30
|
+
|
|
31
|
+
## Workflow
|
|
32
|
+
|
|
33
|
+
1. Preserve existing files and unrelated user work.
|
|
34
|
+
2. Run `dotdotgod init <project-root> [--documentation-root PATH] --template <name>` when available; otherwise run `sh <resolved-skill-directory>/scripts/init_project.sh <project-root> --template <name>`. Existing files must be skipped, not replaced.
|
|
35
|
+
3. The POSIX fallback supports bundled templates only. If a custom template was selected and the CLI is unavailable, stop and explain that custom templates require the CLI; do not silently substitute another template.
|
|
36
|
+
4. Validate the initialized project with `dotdotgod validate <project-root>` when available.
|
|
37
|
+
5. Report created and skipped files, the selected template, validation failures, and unresolved instruction conflicts.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Shared Agent Docs
|
|
2
|
+
|
|
3
|
+
Use `AGENTS.md` as the canonical shared instruction file.
|
|
4
|
+
|
|
5
|
+
## Naming
|
|
6
|
+
|
|
7
|
+
- `AGENTS.md`: preferred shared file name. OpenAI Codex recognizes this convention, and the community `agents.md` convention uses the plural form.
|
|
8
|
+
- `CLAUDE.md`: Claude Code's project memory file. Keep it thin and import `AGENTS.md` with `@AGENTS.md`.
|
|
9
|
+
- `CODEX.md`: project-local Codex pointer. Keep it thin and link to `AGENTS.md`.
|
|
10
|
+
- `.github/copilot-instructions.md`: optional Copilot surface. If present, route to `AGENTS.md`, `docs/README.md`, and project commands instead of copying the full rules body.
|
|
11
|
+
- `.cursor/rules/*.md`: optional Cursor path-scoped rules. If present, keep Cursor-specific scoping there and route durable project rules to shared docs.
|
|
12
|
+
- `llms.txt`: optional documentation discovery surface. If present, point to README indexes and bounded docs entrypoints instead of embedding large docs bodies.
|
|
13
|
+
- `AGENT.md`: avoid for new projects unless an existing tool in the repo requires it.
|
|
14
|
+
|
|
15
|
+
## Content Model
|
|
16
|
+
|
|
17
|
+
Put durable, project-wide instructions in `AGENTS.md`:
|
|
18
|
+
|
|
19
|
+
- project purpose and stack
|
|
20
|
+
- install, test, run, and lint commands
|
|
21
|
+
- architecture and ownership notes
|
|
22
|
+
- documentation map
|
|
23
|
+
- coding and review expectations
|
|
24
|
+
- environment constraints
|
|
25
|
+
|
|
26
|
+
For projects using the dotdotgod CLI, `dotdotgod validate` is the enforcement point for machine-readable docs rules such as fenced `json dotdotgod` traceability blocks in behavior specs. Keep the detailed schema in the CLI and its validation errors.
|
|
27
|
+
|
|
28
|
+
Do not duplicate the same body in `CLAUDE.md`, `CODEX.md`, Copilot instructions, Cursor rules, or llms discovery surfaces; duplication causes drift.
|
|
29
|
+
|
|
30
|
+
## Focused Behavior Contracts
|
|
31
|
+
|
|
32
|
+
Use focused behavior contracts for user-visible rules that need clear implementation and verification links.
|
|
33
|
+
|
|
34
|
+
Good focused contracts:
|
|
35
|
+
|
|
36
|
+
- describe current behavior, not change history
|
|
37
|
+
- stay small enough for agents to load and reason about directly
|
|
38
|
+
- connect to implementation files, tests, related docs, and verification commands with a final fenced `json dotdotgod` traceability block when the project uses dotdotgod validation
|
|
39
|
+
- split large product areas into `docs/spec/<domain>/README.md` plus focused UPPER_SNAKE_CASE spec files
|
|
40
|
+
|
|
41
|
+
Traceability helps agents find related code and checks. It is not a semantic proof that tests cover every edge case.
|