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.
- package/CHANGELOG.md +110 -0
- package/README.md +10 -65
- package/dist/cli.js +1227 -299
- package/dist/cli.js.map +1 -1
- package/package.json +2 -1
- package/templates/claude/agents/ec-fixer.md +36 -0
- package/templates/codex/agents/ec-fixer.toml +25 -0
- package/templates/common/bundled-skills/ec-init/SKILL.md +174 -0
- package/templates/common/bundled-skills/ec-init/references/memory-migration.md +87 -0
- package/templates/common/bundled-skills/ec-meta/SKILL.md +5 -2
- package/templates/common/bundled-skills/ec-meta/references/customize-local/README.md +2 -1
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +14 -11
- package/templates/common/bundled-skills/ec-meta/references/platform-files/README.md +8 -4
- package/templates/common/skills/ec-analysis/SKILL.md +104 -126
- package/templates/common/skills/ec-brainstorming/SKILL.md +1 -1
- package/templates/common/skills/ec-git/SKILL.md +10 -10
- package/templates/common/skills/ec-implementing/SKILL.md +26 -3
- package/templates/common/skills/ec-memory/SKILL.md +56 -19
- package/templates/common/skills/ec-reviewing/SKILL.md +39 -10
- package/templates/common/skills/ec-task-close/SKILL.md +4 -3
- package/templates/common/skills/ec-task-management/SKILL.md +9 -24
- package/templates/common/skills/ec-verification/SKILL.md +22 -11
- package/templates/common/skills/ec-workflow/SKILL.md +51 -21
- package/templates/main-constraint/AGENTS.md.tpl +16 -3
- package/templates/main-constraint/CLAUDE.md.tpl +16 -3
- package/templates/qoder/agents/ec-fixer.md +36 -0
- package/templates/runtime/memory/SHORT_MEMORY_TEMPLATE.md +75 -0
- package/templates/runtime/memory/long/BUSINESS.md +42 -9
- package/templates/runtime/memory/long/MEMORY.md +37 -2
- package/templates/runtime/memory/long/TECHNICAL.md +45 -1
- package/templates/runtime/templates/dev-spec-skeleton.md +80 -0
- package/templates/shared-hooks/easy_coding_state.py +516 -0
- package/templates/shared-hooks/easy_coding_status.py +77 -38
- package/templates/shared-hooks/inject-subagent-context.py +4 -11
- package/templates/shared-hooks/inject-workflow-state.py +5 -14
- package/templates/shared-hooks/session-start.py +90 -16
- 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 =
|
|
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
|
-
|
|
83
|
-
if
|
|
84
|
-
|
|
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,
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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.
|