easy-coding-harness 0.1.7 → 0.2.1

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 (37) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +10 -65
  3. package/dist/cli.js +1227 -299
  4. package/dist/cli.js.map +1 -1
  5. package/package.json +2 -1
  6. package/templates/claude/agents/ec-fixer.md +36 -0
  7. package/templates/codex/agents/ec-fixer.toml +25 -0
  8. package/templates/common/bundled-skills/ec-init/SKILL.md +174 -0
  9. package/templates/common/bundled-skills/ec-init/references/memory-migration.md +87 -0
  10. package/templates/common/bundled-skills/ec-meta/SKILL.md +5 -2
  11. package/templates/common/bundled-skills/ec-meta/references/customize-local/README.md +2 -1
  12. package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +14 -11
  13. package/templates/common/bundled-skills/ec-meta/references/platform-files/README.md +8 -4
  14. package/templates/common/skills/ec-analysis/SKILL.md +104 -126
  15. package/templates/common/skills/ec-brainstorming/SKILL.md +1 -1
  16. package/templates/common/skills/ec-git/SKILL.md +10 -10
  17. package/templates/common/skills/ec-implementing/SKILL.md +26 -3
  18. package/templates/common/skills/ec-memory/SKILL.md +56 -19
  19. package/templates/common/skills/ec-reviewing/SKILL.md +39 -10
  20. package/templates/common/skills/ec-task-close/SKILL.md +4 -3
  21. package/templates/common/skills/ec-task-management/SKILL.md +9 -24
  22. package/templates/common/skills/ec-verification/SKILL.md +22 -11
  23. package/templates/common/skills/ec-workflow/SKILL.md +51 -21
  24. package/templates/main-constraint/AGENTS.md.tpl +16 -3
  25. package/templates/main-constraint/CLAUDE.md.tpl +16 -3
  26. package/templates/qoder/agents/ec-fixer.md +36 -0
  27. package/templates/runtime/memory/SHORT_MEMORY_TEMPLATE.md +75 -0
  28. package/templates/runtime/memory/long/BUSINESS.md +42 -9
  29. package/templates/runtime/memory/long/MEMORY.md +37 -2
  30. package/templates/runtime/memory/long/TECHNICAL.md +45 -1
  31. package/templates/runtime/templates/dev-spec-skeleton.md +80 -0
  32. package/templates/shared-hooks/easy_coding_state.py +516 -0
  33. package/templates/shared-hooks/easy_coding_status.py +77 -38
  34. package/templates/shared-hooks/inject-subagent-context.py +4 -11
  35. package/templates/shared-hooks/inject-workflow-state.py +5 -14
  36. package/templates/shared-hooks/session-start.py +90 -16
  37. package/templates/common/skills/ec-init/SKILL.md +0 -96
@@ -4,6 +4,8 @@ import os
4
4
  from pathlib import Path
5
5
  import sys
6
6
 
7
+ from easy_coding_state import snapshot_state
8
+
7
9
 
8
10
  def configure_stdio() -> None:
9
11
  for stream in (sys.stdin, sys.stdout, sys.stderr):
@@ -28,15 +30,6 @@ def find_ec_root(start: Path) -> Path | None:
28
30
  current = current.parent
29
31
 
30
32
 
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
33
  def emit(event_name: str, context: str) -> None:
41
34
  print(
42
35
  json.dumps(
@@ -61,7 +54,7 @@ def main() -> int:
61
54
  if root is None:
62
55
  return 0
63
56
 
64
- state = load_json(root / ".easy-coding" / "state.json") or {}
57
+ state = snapshot_state(root)
65
58
  task_id = state.get("current_task")
66
59
  context = [
67
60
  "[easy-coding:subagent-guard]",
@@ -69,7 +62,7 @@ def main() -> int:
69
62
  "They must not claim completion or verification without fresh evidence.",
70
63
  ]
71
64
  if task_id:
72
- context.append(f"Active Easy Coding task: {task_id}")
65
+ context.append(f"Active Easy Coding task: {task_id} ({state['status']})")
73
66
 
74
67
  event_name = payload.get("hook_event_name") or payload.get("hookEventName") or "PreToolUse"
75
68
  emit(event_name, "\n".join(context))
@@ -4,6 +4,7 @@ import os
4
4
  from pathlib import Path
5
5
  import sys
6
6
 
7
+ from easy_coding_state import load_session
7
8
  from easy_coding_status import build_status_context
8
9
 
9
10
 
@@ -30,16 +31,6 @@ def find_ec_root(start: Path) -> Path | None:
30
31
  current = current.parent
31
32
 
32
33
 
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
34
  def detect_agent() -> str:
44
35
  if os.environ.get("CLAUDE_PROJECT_DIR"):
45
36
  return "claude-code"
@@ -79,12 +70,12 @@ def main() -> int:
79
70
  if root is None:
80
71
  return 0
81
72
 
82
- state = load_state(root)
83
- if state is None:
84
- return 0
73
+ session = load_session(root)
74
+ if session is None:
75
+ session = {"current_task": None, "created_at": ""}
85
76
 
86
77
  event_name = payload.get("hook_event_name") or payload.get("hookEventName") or "UserPromptSubmit"
87
- emit(event_name, build_status_context(root, state, detect_agent()))
78
+ emit(event_name, build_status_context(root, session, detect_agent()))
88
79
  return 0
89
80
 
90
81
 
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env python3
2
2
  import json
3
3
  import os
4
+ from datetime import datetime, timezone
4
5
  from pathlib import Path
5
6
  import sys
6
7
 
8
+ from easy_coding_state import load_session, write_session
7
9
  from easy_coding_status import build_status_context
8
10
 
9
11
 
@@ -59,16 +61,72 @@ def detect_agent() -> str:
59
61
  return "unknown"
60
62
 
61
63
 
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
- }
64
+ def is_process_alive(pid: int) -> bool:
65
+ try:
66
+ os.kill(pid, 0)
67
+ return True
68
+ except (OSError, ProcessLookupError):
69
+ return False
70
+
71
+
72
+ def clean_stale_sessions(root: Path) -> None:
73
+ sessions_dir = root / ".easy-coding" / "sessions"
74
+ if not sessions_dir.is_dir():
75
+ return
76
+
77
+ now = datetime.now(timezone.utc)
78
+ threshold_hours = 24
79
+
80
+ for entry in sessions_dir.iterdir():
81
+ if not entry.suffix == ".json":
82
+ continue
83
+ try:
84
+ data = json.loads(entry.read_text(encoding="utf-8"))
85
+ created = datetime.fromisoformat(data.get("created_at", ""))
86
+ if created.tzinfo is None:
87
+ created = created.replace(tzinfo=timezone.utc)
88
+ age_hours = (now - created).total_seconds() / 3600
89
+ if age_hours <= threshold_hours:
90
+ continue
91
+ pid = int(entry.stem)
92
+ if is_process_alive(pid):
93
+ continue
94
+ entry.unlink()
95
+ except (OSError, json.JSONDecodeError, ValueError, KeyError):
96
+ continue
97
+
98
+
99
+ def migrate_legacy_state(root: Path, agent: str) -> dict | None:
100
+ """Migrate old-format state.json into session file + task.json. Returns session dict."""
101
+ state_path = root / ".easy-coding" / "state.json"
102
+ old_state = load_json(state_path)
103
+ if old_state is None:
104
+ return None
105
+
106
+ task_id = old_state.get("current_task")
107
+ if task_id:
108
+ task_path = root / ".easy-coding" / "tasks" / str(task_id) / "task.json"
109
+ task = load_json(task_path)
110
+ if task:
111
+ if "stage_history" not in task or not task["stage_history"]:
112
+ task["stage_history"] = old_state.get("stage_history", [])
113
+ if "last_agent" not in task or not task["last_agent"]:
114
+ task["last_agent"] = old_state.get("last_agent", agent)
115
+ if old_state.get("confirmed_by_user"):
116
+ task["confirmed_by_user"] = True
117
+ if old_state.get("test_strategy_confirmed"):
118
+ task["test_strategy_confirmed"] = True
119
+ if old_state.get("repo_paths"):
120
+ task["repo_paths"] = old_state["repo_paths"]
121
+ write_json(task_path, task)
122
+
123
+ # Remove legacy state.json
124
+ try:
125
+ state_path.unlink()
126
+ except OSError:
127
+ pass
128
+
129
+ return {"current_task": task_id, "created_at": datetime.now(timezone.utc).isoformat()}
72
130
 
73
131
 
74
132
  def emit(event_name: str, context: str) -> None:
@@ -97,13 +155,29 @@ def main() -> int:
97
155
 
98
156
  agent = detect_agent()
99
157
  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
158
 
106
- emit(event_name, build_status_context(root, state, agent))
159
+ # Migrate legacy state.json if present
160
+ state_path = root / ".easy-coding" / "state.json"
161
+ migrated_session = None
162
+ if state_path.exists():
163
+ migrated_session = migrate_legacy_state(root, agent)
164
+
165
+ # Clean stale session files
166
+ clean_stale_sessions(root)
167
+
168
+ # Create/overwrite session file for this session
169
+ session = load_session(root)
170
+ if session is None:
171
+ if migrated_session:
172
+ session = migrated_session
173
+ else:
174
+ session = {"current_task": None, "created_at": datetime.now(timezone.utc).isoformat()}
175
+ else:
176
+ # Refresh created_at on session start (marks session as active)
177
+ session["created_at"] = datetime.now(timezone.utc).isoformat()
178
+
179
+ write_session(root, session)
180
+ emit(event_name, build_status_context(root, session, agent))
107
181
  return 0
108
182
 
109
183
 
@@ -1,96 +0,0 @@
1
- ---
2
- name: ec-init
3
- description: One-time project knowledge initialization after the easy-coding CLI install. Use when the user runs {{skill_trigger}}ec-init or hook context shows [easy-coding:init-required]. Idempotent. Detects startup vs iterative projects automatically and generates SOUL, RULES, ABSTRACT, and TEST_STRATEGY.
4
- ---
5
-
6
- # ec-init — project knowledge initialization
7
-
8
- The CLI is a dumb file mover; you are the smart half. You analyze the project and produce
9
- the knowledge assets every later stage depends on. This runs once per project; re-runs are
10
- safe.
11
-
12
- Communicate with the user in the user's language.
13
-
14
- ## Entry guard (idempotency — run first)
15
-
16
- 1. Read `.easy-coding/tasks/project-init/task.json`.
17
- - File missing → the CLI never ran. Tell the user to run `easy-coding init` first. Stop.
18
- - `status == "COMPLETE"` → already initialized. Show a short summary (project mode,
19
- generated files, init date from `init_log`) and exit without changing anything.
20
- - `status == "PENDING"` → proceed.
21
- 2. Repeated calls must never duplicate files or corrupt existing ones.
22
-
23
- ## Project mode detection (automatic — never ask the user)
24
-
25
- Scan the project root, excluding `.easy-coding/`, platform dirs (`.claude/`, `.agents/`,
26
- `.codex/`, `.qoder/`), lockfiles, and pure config skeletons (package.json, tsconfig,
27
- linter configs, CI yaml).
28
-
29
- - Substantive source files exist (.ts/.js/.java/.py/.go/.rs/... containing real logic)
30
- → **iterative** (existing project).
31
- - Empty, or config skeleton only → **startup** (new project).
32
-
33
- Record the verdict in `config.yaml` under `project.mode` and in `init_log`.
34
-
35
- ## Iterative project flow
36
-
37
- After each step append `{step, summary, timestamp}` to `init_log` in task.json — a later
38
- agent must be able to see what was generated and on what evidence.
39
-
40
- 1. **SOUL.md — project identity.** Analyze README, package metadata, and code purpose. Write:
41
- - What this project is (2-3 sentences, concrete, no marketing fluff)
42
- - Dialogue standards (language to use with the user, tone, verbosity expectations)
43
- - Hard prohibitions (e.g. never commit secrets, never edit generated directories)
44
- Keep it under ~40 lines; SOUL is loaded on every task.
45
- 2. **RULES.md — coding rules grounded in evidence,** not generic best practice. Detect:
46
- - Languages and versions (from configs and sources)
47
- - Naming conventions actually in use (scan representative files)
48
- - Comment language: if more than 70% of existing comments are Chinese, the rule is
49
- "comments in Chinese"; same logic for English; mixed → follow each file's dominant language
50
- - Error handling style, import ordering, formatter/linter in use (read their configs)
51
- Structure as one section per language plus a General section. Every rule must be
52
- mechanically checkable — "be clean" is not a rule; "exported functions carry explicit
53
- return types" is.
54
- 3. **ABSTRACT.md — architecture cognition.** Scan directory structure and entrypoints. Write:
55
- modules and their responsibilities, core data flow, tech stack with versions, external
56
- dependencies and services, build and run commands. Use a named section per module —
57
- later stages extract sections by name (the `abstract_modules` field in execution plans).
58
- 4. **TEST_STRATEGY.md — project test baseline:** detected framework, test command, where
59
- tests live, naming conventions, coverage expectations, which classes of code this project
60
- tests vs skips. Also fill `config.yaml` `test.framework` and `test.command` with commands
61
- you verified exist (read package.json scripts or equivalent — do not guess).
62
- 5. **Memory init** — ensure `.easy-coding/memory/short/` and `memory/long/` exist with the
63
- three long files (MEMORY.md, BUSINESS.md, TECHNICAL.md). Never write fake entries.
64
- 6. **Mark complete** — task.json `status:"COMPLETE"` plus a final `init_log` entry. Tell the
65
- user initialization is done and daily work goes through `{{skill_trigger}}ec-workflow`.
66
-
67
- ## Startup project flow
68
-
69
- 1. **Lightweight interview** — 3-5 questions, strictly one at a time, multiple-choice
70
- preferred: project name and positioning, primary language and framework, test framework
71
- preference, special coding conventions (optional), comment language (optional).
72
- 2. Generate from the answers:
73
- - SOUL.md (identity from answers, dialogue standards, prohibitions)
74
- - RULES.md (baseline rules for the chosen language; mark sections "refine as code grows")
75
- - TEST_STRATEGY.md (skeleton for the chosen framework)
76
- 3. **Skip ABSTRACT.md** — no architecture exists yet. Note in init_log:
77
- "ABSTRACT pending; ec-memory backfills after the first substantive task." (ec-memory
78
- detects the missing file during MEMORY_LONG and generates it from the then-current code.)
79
- 4. Memory init and completion marking, same as iterative steps 5-6.
80
- 5. Recommend: design first with `{{skill_trigger}}ec-brainstorming`, then build via
81
- `{{skill_trigger}}ec-workflow`.
82
-
83
- ## Quality bar for generated files
84
-
85
- - Every claim grounded in observed evidence — file paths and configs you actually read.
86
- No filler like "follow best practices".
87
- - SOUL stays short. RULES and ABSTRACT run as long as the evidence supports, in named
88
- sections. TEST_STRATEGY must be concrete enough that ec-verification can derive runnable
89
- commands from it.
90
-
91
- ## Boundaries
92
-
93
- - Never modify business source code, dependencies, or git state.
94
- - Never create workflow tasks or touch `current_task` / `current_stage` in state.json.
95
- - Never regenerate knowledge files for a COMPLETE project-init — the user edits them
96
- manually or asks ec-meta for guided customization.