easy-coding-harness 0.1.4
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/README.md +112 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +979 -0
- package/dist/cli.js.map +1 -0
- package/package.json +53 -0
- package/templates/claude/agents/ec-implementer.md +26 -0
- package/templates/claude/agents/ec-reviewer.md +30 -0
- package/templates/claude/agents/ec-verifier.md +30 -0
- package/templates/claude/settings.json +40 -0
- package/templates/codex/agents/ec-implementer.toml +25 -0
- package/templates/codex/agents/ec-reviewer.toml +28 -0
- package/templates/codex/agents/ec-verifier.toml +26 -0
- package/templates/codex/config.toml +9 -0
- package/templates/codex/hooks.json +24 -0
- package/templates/common/bundled-skills/ec-meta/SKILL.md +56 -0
- package/templates/common/bundled-skills/ec-meta/references/customize-local/README.md +54 -0
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +76 -0
- package/templates/common/bundled-skills/ec-meta/references/platform-files/README.md +52 -0
- package/templates/common/skills/ec-analysis/SKILL.md +113 -0
- package/templates/common/skills/ec-brainstorming/SKILL.md +70 -0
- package/templates/common/skills/ec-git/SKILL.md +47 -0
- package/templates/common/skills/ec-implementing/SKILL.md +89 -0
- package/templates/common/skills/ec-init/SKILL.md +96 -0
- package/templates/common/skills/ec-memory/SKILL.md +69 -0
- package/templates/common/skills/ec-reviewing/SKILL.md +61 -0
- package/templates/common/skills/ec-task-close/SKILL.md +35 -0
- package/templates/common/skills/ec-task-management/SKILL.md +45 -0
- package/templates/common/skills/ec-verification/SKILL.md +78 -0
- package/templates/common/skills/ec-workflow/SKILL.md +120 -0
- package/templates/main-constraint/AGENTS.md.tpl +58 -0
- package/templates/main-constraint/CLAUDE.md.tpl +56 -0
- package/templates/qoder/agents/ec-implementer.md +28 -0
- package/templates/qoder/agents/ec-reviewer.md +32 -0
- package/templates/qoder/agents/ec-verifier.md +32 -0
- package/templates/qoder/settings.json +36 -0
- package/templates/runtime/memory/long/BUSINESS.md +14 -0
- package/templates/runtime/memory/long/MEMORY.md +3 -0
- package/templates/runtime/memory/long/TECHNICAL.md +3 -0
- package/templates/shared-hooks/easy_coding_status.py +73 -0
- package/templates/shared-hooks/inject-subagent-context.py +80 -0
- package/templates/shared-hooks/inject-workflow-state.py +92 -0
- package/templates/shared-hooks/session-start.py +111 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
READY_LINE = (
|
|
6
|
+
"> **Easy Coding** · Ready · Use `ec-analysis` to start requirement analysis, "
|
|
7
|
+
"`ec-brainstorming` to brainstorm, or `ec-task-management` to view tasks"
|
|
8
|
+
)
|
|
9
|
+
WAITING_INIT_LINE = "> **Easy Coding** · Waiting init · Use `ec-init` to initialize"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def load_json(path: Path) -> dict | None:
|
|
13
|
+
if not path.exists():
|
|
14
|
+
return None
|
|
15
|
+
try:
|
|
16
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
17
|
+
except (OSError, json.JSONDecodeError):
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_task(root: Path, task_id: str | None) -> dict | None:
|
|
22
|
+
if not task_id:
|
|
23
|
+
return None
|
|
24
|
+
return load_json(root / ".easy-coding" / "tasks" / task_id / "task.json")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_project_init_required(root: Path) -> bool:
|
|
28
|
+
project_init = load_json(root / ".easy-coding" / "tasks" / "project-init" / "task.json")
|
|
29
|
+
return bool(project_init and project_init.get("status") != "COMPLETE")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def resolve_task_status(root: Path, state: dict, task_id: str | None) -> str:
|
|
33
|
+
task = load_task(root, task_id)
|
|
34
|
+
if task and task.get("status"):
|
|
35
|
+
return str(task["status"])
|
|
36
|
+
return str(state.get("current_stage") or "idle")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_status_line(root: Path, state: dict, agent: str | None = None) -> str:
|
|
40
|
+
task_id = state.get("current_task")
|
|
41
|
+
if task_id:
|
|
42
|
+
status = resolve_task_status(root, state, str(task_id))
|
|
43
|
+
line = f"> **Easy Coding** · `{task_id}` · `{status}`"
|
|
44
|
+
last_agent = state.get("last_agent")
|
|
45
|
+
if agent and last_agent and last_agent != agent:
|
|
46
|
+
line += f" · Handoff -> `{last_agent}`"
|
|
47
|
+
return line
|
|
48
|
+
|
|
49
|
+
if is_project_init_required(root):
|
|
50
|
+
return WAITING_INIT_LINE
|
|
51
|
+
|
|
52
|
+
return READY_LINE
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_machine_breadcrumbs(root: Path, state: dict, agent: str | None = None) -> list[str]:
|
|
56
|
+
stage = state.get("current_stage") or "idle"
|
|
57
|
+
task_id = state.get("current_task")
|
|
58
|
+
lines = [f"[workflow-state:{stage}]"]
|
|
59
|
+
|
|
60
|
+
if task_id:
|
|
61
|
+
lines.append(f"[current-task:{task_id}]")
|
|
62
|
+
last_agent = state.get("last_agent")
|
|
63
|
+
if agent and last_agent and last_agent != agent:
|
|
64
|
+
lines.append(f"[easy-coding:handoff-from:{last_agent}]")
|
|
65
|
+
|
|
66
|
+
if is_project_init_required(root):
|
|
67
|
+
lines.append("[easy-coding:init-required]")
|
|
68
|
+
|
|
69
|
+
return lines
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def build_status_context(root: Path, state: dict, agent: str | None = None) -> str:
|
|
73
|
+
return "\n".join([build_status_line(root, state, agent), *build_machine_breadcrumbs(root, state, agent)])
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def configure_stdio() -> None:
|
|
9
|
+
for stream in (sys.stdin, sys.stdout, sys.stderr):
|
|
10
|
+
if hasattr(stream, "reconfigure"):
|
|
11
|
+
stream.reconfigure(encoding="utf-8")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def read_payload() -> dict:
|
|
15
|
+
try:
|
|
16
|
+
return json.loads(sys.stdin.read() or "{}")
|
|
17
|
+
except (json.JSONDecodeError, ValueError):
|
|
18
|
+
return {}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def find_ec_root(start: Path) -> Path | None:
|
|
22
|
+
current = start.resolve()
|
|
23
|
+
while True:
|
|
24
|
+
if (current / ".easy-coding").is_dir():
|
|
25
|
+
return current
|
|
26
|
+
if current == current.parent:
|
|
27
|
+
return None
|
|
28
|
+
current = current.parent
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_json(path: Path) -> dict | None:
|
|
32
|
+
if not path.exists():
|
|
33
|
+
return None
|
|
34
|
+
try:
|
|
35
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
36
|
+
except (OSError, json.JSONDecodeError):
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def emit(event_name: str, context: str) -> None:
|
|
41
|
+
print(
|
|
42
|
+
json.dumps(
|
|
43
|
+
{
|
|
44
|
+
"hookSpecificOutput": {
|
|
45
|
+
"hookEventName": event_name,
|
|
46
|
+
"additionalContext": context,
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
ensure_ascii=False,
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main() -> int:
|
|
55
|
+
configure_stdio()
|
|
56
|
+
if os.environ.get("EC_HOOKS") == "0":
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
payload = read_payload()
|
|
60
|
+
root = find_ec_root(Path(payload.get("cwd") or os.getcwd()))
|
|
61
|
+
if root is None:
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
state = load_json(root / ".easy-coding" / "state.json") or {}
|
|
65
|
+
task_id = state.get("current_task")
|
|
66
|
+
context = [
|
|
67
|
+
"[easy-coding:subagent-guard]",
|
|
68
|
+
"Sub-agents must follow the task card, stay within the allowed file scope, and return structured results.",
|
|
69
|
+
"They must not claim completion or verification without fresh evidence.",
|
|
70
|
+
]
|
|
71
|
+
if task_id:
|
|
72
|
+
context.append(f"Active Easy Coding task: {task_id}")
|
|
73
|
+
|
|
74
|
+
event_name = payload.get("hook_event_name") or payload.get("hookEventName") or "PreToolUse"
|
|
75
|
+
emit(event_name, "\n".join(context))
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
sys.exit(main())
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from easy_coding_status import build_status_context
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def configure_stdio() -> None:
|
|
11
|
+
for stream in (sys.stdin, sys.stdout, sys.stderr):
|
|
12
|
+
if hasattr(stream, "reconfigure"):
|
|
13
|
+
stream.reconfigure(encoding="utf-8")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_payload() -> dict:
|
|
17
|
+
try:
|
|
18
|
+
return json.loads(sys.stdin.read() or "{}")
|
|
19
|
+
except (json.JSONDecodeError, ValueError):
|
|
20
|
+
return {}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def find_ec_root(start: Path) -> Path | None:
|
|
24
|
+
current = start.resolve()
|
|
25
|
+
while True:
|
|
26
|
+
if (current / ".easy-coding").is_dir():
|
|
27
|
+
return current
|
|
28
|
+
if current == current.parent:
|
|
29
|
+
return None
|
|
30
|
+
current = current.parent
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_state(root: Path) -> dict | None:
|
|
34
|
+
state_path = root / ".easy-coding" / "state.json"
|
|
35
|
+
if not state_path.exists():
|
|
36
|
+
return {"current_stage": "idle", "current_task": None}
|
|
37
|
+
try:
|
|
38
|
+
return json.loads(state_path.read_text(encoding="utf-8"))
|
|
39
|
+
except (OSError, json.JSONDecodeError):
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def detect_agent() -> str:
|
|
44
|
+
if os.environ.get("CLAUDE_PROJECT_DIR"):
|
|
45
|
+
return "claude-code"
|
|
46
|
+
if os.environ.get("QODER_PROJECT_DIR"):
|
|
47
|
+
return "qoder"
|
|
48
|
+
hook_path = Path(sys.argv[0]).as_posix()
|
|
49
|
+
if ".claude/" in hook_path:
|
|
50
|
+
return "claude-code"
|
|
51
|
+
if ".codex/" in hook_path:
|
|
52
|
+
return "codex"
|
|
53
|
+
if ".qoder/" in hook_path or ".qodercn/" in hook_path:
|
|
54
|
+
return "qoder"
|
|
55
|
+
return "unknown"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def emit(event_name: str, context: str) -> None:
|
|
59
|
+
print(
|
|
60
|
+
json.dumps(
|
|
61
|
+
{
|
|
62
|
+
"hookSpecificOutput": {
|
|
63
|
+
"hookEventName": event_name,
|
|
64
|
+
"additionalContext": context,
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
ensure_ascii=False,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main() -> int:
|
|
73
|
+
configure_stdio()
|
|
74
|
+
if os.environ.get("EC_HOOKS") == "0":
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
payload = read_payload()
|
|
78
|
+
root = find_ec_root(Path(payload.get("cwd") or os.getcwd()))
|
|
79
|
+
if root is None:
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
state = load_state(root)
|
|
83
|
+
if state is None:
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
event_name = payload.get("hook_event_name") or payload.get("hookEventName") or "UserPromptSubmit"
|
|
87
|
+
emit(event_name, build_status_context(root, state, detect_agent()))
|
|
88
|
+
return 0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
sys.exit(main())
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from easy_coding_status import build_status_context
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def configure_stdio() -> None:
|
|
11
|
+
for stream in (sys.stdin, sys.stdout, sys.stderr):
|
|
12
|
+
if hasattr(stream, "reconfigure"):
|
|
13
|
+
stream.reconfigure(encoding="utf-8")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_payload() -> dict:
|
|
17
|
+
try:
|
|
18
|
+
return json.loads(sys.stdin.read() or "{}")
|
|
19
|
+
except (json.JSONDecodeError, ValueError):
|
|
20
|
+
return {}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def find_ec_root(start: Path) -> Path | None:
|
|
24
|
+
current = start.resolve()
|
|
25
|
+
while True:
|
|
26
|
+
if (current / ".easy-coding").is_dir():
|
|
27
|
+
return current
|
|
28
|
+
if current == current.parent:
|
|
29
|
+
return None
|
|
30
|
+
current = current.parent
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_json(path: Path) -> dict | None:
|
|
34
|
+
if not path.exists():
|
|
35
|
+
return None
|
|
36
|
+
try:
|
|
37
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
38
|
+
except (OSError, json.JSONDecodeError):
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def write_json(path: Path, data: dict) -> None:
|
|
43
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def detect_agent() -> str:
|
|
48
|
+
if os.environ.get("CLAUDE_PROJECT_DIR"):
|
|
49
|
+
return "claude-code"
|
|
50
|
+
if os.environ.get("QODER_PROJECT_DIR"):
|
|
51
|
+
return "qoder"
|
|
52
|
+
hook_path = Path(sys.argv[0]).as_posix()
|
|
53
|
+
if ".claude/" in hook_path:
|
|
54
|
+
return "claude-code"
|
|
55
|
+
if ".codex/" in hook_path:
|
|
56
|
+
return "codex"
|
|
57
|
+
if ".qoder/" in hook_path or ".qodercn/" in hook_path:
|
|
58
|
+
return "qoder"
|
|
59
|
+
return "unknown"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def default_state(agent: str) -> dict:
|
|
63
|
+
return {
|
|
64
|
+
"current_stage": "idle",
|
|
65
|
+
"current_task": None,
|
|
66
|
+
"last_agent": agent,
|
|
67
|
+
"stage_history": [],
|
|
68
|
+
"confirmed_by_user": False,
|
|
69
|
+
"test_strategy_confirmed": False,
|
|
70
|
+
"repo_paths": {},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def emit(event_name: str, context: str) -> None:
|
|
75
|
+
print(
|
|
76
|
+
json.dumps(
|
|
77
|
+
{
|
|
78
|
+
"hookSpecificOutput": {
|
|
79
|
+
"hookEventName": event_name,
|
|
80
|
+
"additionalContext": context,
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
ensure_ascii=False,
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main() -> int:
|
|
89
|
+
configure_stdio()
|
|
90
|
+
if os.environ.get("EC_HOOKS") == "0":
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
payload = read_payload()
|
|
94
|
+
root = find_ec_root(Path(payload.get("cwd") or os.getcwd()))
|
|
95
|
+
if root is None:
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
agent = detect_agent()
|
|
99
|
+
event_name = payload.get("hook_event_name") or payload.get("hookEventName") or "SessionStart"
|
|
100
|
+
state_path = root / ".easy-coding" / "state.json"
|
|
101
|
+
state = load_json(state_path)
|
|
102
|
+
if state is None:
|
|
103
|
+
state = default_state(agent)
|
|
104
|
+
write_json(state_path, state)
|
|
105
|
+
|
|
106
|
+
emit(event_name, build_status_context(root, state, agent))
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
sys.exit(main())
|