easy-coding-harness 0.1.8 → 0.3.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/CHANGELOG.md +120 -0
- package/README.md +109 -135
- package/dist/cli.js +1218 -282
- package/dist/cli.js.map +1 -1
- package/package.json +10 -1
- package/templates/claude/agents/ec-implementer.md +1 -1
- package/templates/claude/agents/ec-reviewer.md +1 -1
- 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 +111 -130
- 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 +31 -24
- package/templates/common/skills/ec-memory/SKILL.md +41 -16
- package/templates/common/skills/ec-reviewing/SKILL.md +1 -1
- 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 +10 -4
- package/templates/common/skills/ec-workflow/SKILL.md +48 -17
- package/templates/main-constraint/AGENTS.md.tpl +16 -3
- package/templates/main-constraint/CLAUDE.md.tpl +16 -3
- 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 +610 -0
- package/templates/shared-hooks/easy_coding_status.py +72 -81
- package/templates/shared-hooks/inject-subagent-context.py +5 -12
- package/templates/shared-hooks/inject-workflow-state.py +2 -1
- package/templates/shared-hooks/session-start.py +2 -1
- package/templates/common/skills/ec-init/SKILL.md +0 -96
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import os
|
|
3
1
|
from pathlib import Path
|
|
4
2
|
|
|
3
|
+
from easy_coding_state import (
|
|
4
|
+
HELP_SUFFIX,
|
|
5
|
+
get_pending_init_version,
|
|
6
|
+
is_project_init_required,
|
|
7
|
+
record_seen_stage,
|
|
8
|
+
snapshot_state,
|
|
9
|
+
)
|
|
10
|
+
|
|
5
11
|
|
|
6
12
|
READY_LINE = (
|
|
7
13
|
"> **Easy Coding** · Ready · Use `ec-workflow` to start or resume a task, "
|
|
@@ -9,110 +15,95 @@ READY_LINE = (
|
|
|
9
15
|
)
|
|
10
16
|
WAITING_INIT_LINE = "> **Easy Coding** · Waiting init · Use `ec-init` to initialize"
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def load_json(path: Path) -> dict | None:
|
|
28
|
-
if not path.exists():
|
|
29
|
-
return None
|
|
30
|
-
try:
|
|
31
|
-
return json.loads(path.read_text(encoding="utf-8"))
|
|
32
|
-
except (OSError, json.JSONDecodeError):
|
|
33
|
-
return None
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def load_task(root: Path, task_id: str | None) -> dict | None:
|
|
37
|
-
if not task_id:
|
|
38
|
-
return None
|
|
39
|
-
return load_json(root / ".easy-coding" / "tasks" / task_id / "task.json")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def load_session(root: Path) -> dict | None:
|
|
43
|
-
ppid = os.getppid()
|
|
44
|
-
return load_json(root / ".easy-coding" / "sessions" / f"{ppid}.json")
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def write_session(root: Path, session: dict) -> None:
|
|
48
|
-
ppid = os.getppid()
|
|
49
|
-
session_path = root / ".easy-coding" / "sessions" / f"{ppid}.json"
|
|
50
|
-
session_path.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
-
session_path.write_text(json.dumps(session, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def is_project_init_required(root: Path) -> bool:
|
|
55
|
-
project_init = load_json(root / ".easy-coding" / "tasks" / "project-init" / "task.json")
|
|
56
|
-
return bool(project_init and project_init.get("status") != "COMPLETE")
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def validate_transition(previous: str, current: str) -> str | None:
|
|
60
|
-
if previous == current:
|
|
61
|
-
return None
|
|
62
|
-
allowed = VALID_TRANSITIONS.get(previous, set())
|
|
63
|
-
if current in allowed:
|
|
64
|
-
return None
|
|
65
|
-
return f"ILLEGAL TRANSITION: {previous} -> {current}. Allowed from {previous}: {sorted(allowed) or 'NONE (terminal state)'}."
|
|
66
|
-
|
|
18
|
+
MANDATORY_DEV_SPEC_HEADERS: list[str] = [
|
|
19
|
+
"## 技术方案",
|
|
20
|
+
"### 项目模式",
|
|
21
|
+
"### 任务类型",
|
|
22
|
+
"### 需求解析",
|
|
23
|
+
"### 现状",
|
|
24
|
+
"### 冲突摘要",
|
|
25
|
+
"### 待用户决策",
|
|
26
|
+
"### 影响面分析",
|
|
27
|
+
"### 改动范围",
|
|
28
|
+
"### 修改方案",
|
|
29
|
+
"### 实施拆解",
|
|
30
|
+
"### 测试策略",
|
|
31
|
+
"### 风险与注意事项",
|
|
32
|
+
]
|
|
67
33
|
|
|
68
34
|
def build_status_line(root: Path, session: dict, agent: str | None = None) -> str:
|
|
69
|
-
|
|
35
|
+
state = snapshot_state(root, session=session)
|
|
36
|
+
task_id = state["current_task"]
|
|
70
37
|
if task_id:
|
|
71
|
-
|
|
72
|
-
status = str(task["status"]) if task and task.get("status") else "PENDING"
|
|
38
|
+
status = str(state["status"])
|
|
73
39
|
line = f"> **Easy Coding** · `{task_id}` · `{status}`"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
40
|
+
last_agent = state.get("last_agent")
|
|
41
|
+
if agent and last_agent and last_agent != agent:
|
|
42
|
+
line += f" · Handoff -> `{last_agent}`"
|
|
43
|
+
if state["is_terminal"] or state["task_missing"]:
|
|
44
|
+
line += f" · {HELP_SUFFIX}"
|
|
78
45
|
return line
|
|
79
46
|
|
|
80
47
|
if is_project_init_required(root):
|
|
81
48
|
return WAITING_INIT_LINE
|
|
82
49
|
|
|
50
|
+
pending = get_pending_init_version(root)
|
|
51
|
+
if pending:
|
|
52
|
+
return (
|
|
53
|
+
f"> **Easy Coding** · Waiting init · "
|
|
54
|
+
f"Upgrade to v{pending} — run `ec-init` to adapt"
|
|
55
|
+
)
|
|
56
|
+
|
|
83
57
|
return READY_LINE
|
|
84
58
|
|
|
85
59
|
|
|
86
60
|
def build_machine_breadcrumbs(root: Path, session: dict, agent: str | None = None) -> list[str]:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
61
|
+
state = snapshot_state(root, session=session)
|
|
62
|
+
task_id = state["current_task"]
|
|
63
|
+
task = state["task"]
|
|
64
|
+
stage = str(state["status"]) if task else "idle"
|
|
65
|
+
lines = [f"[workflow-state:{stage}]", f"[easy-coding:session-file:{state['session_file']}]"]
|
|
91
66
|
|
|
92
67
|
if task_id:
|
|
93
68
|
lines.append(f"[current-task:{task_id}]")
|
|
94
|
-
if
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
69
|
+
if state["task_missing"]:
|
|
70
|
+
lines.append(f"[easy-coding:current-task-missing:{task_id}]")
|
|
71
|
+
last_agent = state.get("last_agent")
|
|
72
|
+
if agent and last_agent and last_agent != agent:
|
|
73
|
+
lines.append(f"[easy-coding:handoff-from:{last_agent}]")
|
|
98
74
|
|
|
99
75
|
if is_project_init_required(root):
|
|
100
76
|
lines.append("[easy-coding:init-required]")
|
|
77
|
+
else:
|
|
78
|
+
pending = get_pending_init_version(root)
|
|
79
|
+
if pending:
|
|
80
|
+
lines.append(f"[easy-coding:upgrade-init-pending:{pending}]")
|
|
81
|
+
|
|
82
|
+
# Stage-specific reminders
|
|
83
|
+
if stage == "ANALYSIS" and task_id:
|
|
84
|
+
dev_spec = root / ".easy-coding" / "tasks" / str(task_id) / "dev-spec.md"
|
|
85
|
+
if dev_spec.exists():
|
|
86
|
+
try:
|
|
87
|
+
content = dev_spec.read_text(encoding="utf-8")
|
|
88
|
+
missing = [h for h in MANDATORY_DEV_SPEC_HEADERS if h not in content]
|
|
89
|
+
if missing:
|
|
90
|
+
names = ",".join(h.lstrip("#").strip() for h in missing)
|
|
91
|
+
lines.append(f"[easy-coding:analysis-template-drift:missing:{names}]")
|
|
92
|
+
else:
|
|
93
|
+
lines.append("[easy-coding:analysis-template-ok]")
|
|
94
|
+
except OSError:
|
|
95
|
+
lines.append("[easy-coding:analysis-gate:skeleton-first-then-fill]")
|
|
96
|
+
else:
|
|
97
|
+
lines.append("[easy-coding:analysis-gate:skeleton-first-then-fill]")
|
|
101
98
|
|
|
102
99
|
# State machine validation
|
|
103
|
-
|
|
104
|
-
if last_seen and task and task.get("status"):
|
|
100
|
+
if task_id and task and task.get("status"):
|
|
105
101
|
current_stage = str(task["status"])
|
|
106
|
-
|
|
102
|
+
last_seen = session.get("last_seen_stage")
|
|
103
|
+
violation = record_seen_stage(root, str(task_id), current_stage)
|
|
107
104
|
if violation:
|
|
108
105
|
lines.append(f"[ILLEGAL-TRANSITION:{last_seen}->{current_stage}]")
|
|
109
106
|
lines.append(f"[easy-coding:transition-error:{violation}]")
|
|
110
|
-
if last_seen != current_stage:
|
|
111
|
-
session["last_seen_stage"] = current_stage
|
|
112
|
-
write_session(root, session)
|
|
113
|
-
elif task and task.get("status"):
|
|
114
|
-
session["last_seen_stage"] = str(task["status"])
|
|
115
|
-
write_session(root, session)
|
|
116
107
|
|
|
117
108
|
return lines
|
|
118
109
|
|
|
@@ -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,15 +54,15 @@ def main() -> int:
|
|
|
61
54
|
if root is None:
|
|
62
55
|
return 0
|
|
63
56
|
|
|
64
|
-
|
|
65
|
-
task_id =
|
|
57
|
+
state = snapshot_state(root)
|
|
58
|
+
task_id = state.get("current_task")
|
|
66
59
|
context = [
|
|
67
60
|
"[easy-coding:subagent-guard]",
|
|
68
61
|
"Sub-agents must follow the task card, stay within the allowed file scope, and return structured results.",
|
|
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))
|
|
@@ -5,7 +5,8 @@ from datetime import datetime, timezone
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
import sys
|
|
7
7
|
|
|
8
|
-
from
|
|
8
|
+
from easy_coding_state import load_session, write_session
|
|
9
|
+
from easy_coding_status import build_status_context
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def configure_stdio() -> None:
|
|
@@ -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.
|