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.
Files changed (42) hide show
  1. package/README.md +112 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +979 -0
  4. package/dist/cli.js.map +1 -0
  5. package/package.json +53 -0
  6. package/templates/claude/agents/ec-implementer.md +26 -0
  7. package/templates/claude/agents/ec-reviewer.md +30 -0
  8. package/templates/claude/agents/ec-verifier.md +30 -0
  9. package/templates/claude/settings.json +40 -0
  10. package/templates/codex/agents/ec-implementer.toml +25 -0
  11. package/templates/codex/agents/ec-reviewer.toml +28 -0
  12. package/templates/codex/agents/ec-verifier.toml +26 -0
  13. package/templates/codex/config.toml +9 -0
  14. package/templates/codex/hooks.json +24 -0
  15. package/templates/common/bundled-skills/ec-meta/SKILL.md +56 -0
  16. package/templates/common/bundled-skills/ec-meta/references/customize-local/README.md +54 -0
  17. package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +76 -0
  18. package/templates/common/bundled-skills/ec-meta/references/platform-files/README.md +52 -0
  19. package/templates/common/skills/ec-analysis/SKILL.md +113 -0
  20. package/templates/common/skills/ec-brainstorming/SKILL.md +70 -0
  21. package/templates/common/skills/ec-git/SKILL.md +47 -0
  22. package/templates/common/skills/ec-implementing/SKILL.md +89 -0
  23. package/templates/common/skills/ec-init/SKILL.md +96 -0
  24. package/templates/common/skills/ec-memory/SKILL.md +69 -0
  25. package/templates/common/skills/ec-reviewing/SKILL.md +61 -0
  26. package/templates/common/skills/ec-task-close/SKILL.md +35 -0
  27. package/templates/common/skills/ec-task-management/SKILL.md +45 -0
  28. package/templates/common/skills/ec-verification/SKILL.md +78 -0
  29. package/templates/common/skills/ec-workflow/SKILL.md +120 -0
  30. package/templates/main-constraint/AGENTS.md.tpl +58 -0
  31. package/templates/main-constraint/CLAUDE.md.tpl +56 -0
  32. package/templates/qoder/agents/ec-implementer.md +28 -0
  33. package/templates/qoder/agents/ec-reviewer.md +32 -0
  34. package/templates/qoder/agents/ec-verifier.md +32 -0
  35. package/templates/qoder/settings.json +36 -0
  36. package/templates/runtime/memory/long/BUSINESS.md +14 -0
  37. package/templates/runtime/memory/long/MEMORY.md +3 -0
  38. package/templates/runtime/memory/long/TECHNICAL.md +3 -0
  39. package/templates/shared-hooks/easy_coding_status.py +73 -0
  40. package/templates/shared-hooks/inject-subagent-context.py +80 -0
  41. package/templates/shared-hooks/inject-workflow-state.py +92 -0
  42. 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())