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.
Files changed (36) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/README.md +109 -135
  3. package/dist/cli.js +1218 -282
  4. package/dist/cli.js.map +1 -1
  5. package/package.json +10 -1
  6. package/templates/claude/agents/ec-implementer.md +1 -1
  7. package/templates/claude/agents/ec-reviewer.md +1 -1
  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 +111 -130
  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 +31 -24
  18. package/templates/common/skills/ec-memory/SKILL.md +41 -16
  19. package/templates/common/skills/ec-reviewing/SKILL.md +1 -1
  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 +10 -4
  23. package/templates/common/skills/ec-workflow/SKILL.md +48 -17
  24. package/templates/main-constraint/AGENTS.md.tpl +16 -3
  25. package/templates/main-constraint/CLAUDE.md.tpl +16 -3
  26. package/templates/runtime/memory/SHORT_MEMORY_TEMPLATE.md +75 -0
  27. package/templates/runtime/memory/long/BUSINESS.md +42 -9
  28. package/templates/runtime/memory/long/MEMORY.md +37 -2
  29. package/templates/runtime/memory/long/TECHNICAL.md +45 -1
  30. package/templates/runtime/templates/dev-spec-skeleton.md +80 -0
  31. package/templates/shared-hooks/easy_coding_state.py +610 -0
  32. package/templates/shared-hooks/easy_coding_status.py +72 -81
  33. package/templates/shared-hooks/inject-subagent-context.py +5 -12
  34. package/templates/shared-hooks/inject-workflow-state.py +2 -1
  35. package/templates/shared-hooks/session-start.py +2 -1
  36. 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
- VALID_TRANSITIONS: dict[str, set[str]] = {
13
- "idle": {"INIT"},
14
- "INIT": {"ANALYSIS", "CLOSED"},
15
- "ANALYSIS": {"WAITING_CONFIRM", "CLOSED"},
16
- "WAITING_CONFIRM": {"IMPLEMENT", "ANALYSIS", "CLOSED"},
17
- "IMPLEMENT": {"REVIEW", "ANALYSIS", "CLOSED"},
18
- "REVIEW": {"VERIFICATION", "IMPLEMENT", "ANALYSIS", "CLOSED"},
19
- "VERIFICATION": {"MEMORY_SHORT", "IMPLEMENT", "CLOSED"},
20
- "MEMORY_SHORT": {"MEMORY_LONG", "CLOSED"},
21
- "MEMORY_LONG": {"COMPLETE", "CLOSED"},
22
- "COMPLETE": set(),
23
- "CLOSED": set(),
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
- task_id = session.get("current_task")
35
+ state = snapshot_state(root, session=session)
36
+ task_id = state["current_task"]
70
37
  if task_id:
71
- task = load_task(root, str(task_id))
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
- if task:
75
- last_agent = task.get("last_agent")
76
- if agent and last_agent and last_agent != agent:
77
- line += f" · Handoff -> `{last_agent}`"
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
- task_id = session.get("current_task")
88
- task = load_task(root, str(task_id)) if task_id else None
89
- stage = str(task["status"]) if task and task.get("status") else "idle"
90
- lines = [f"[workflow-state:{stage}]"]
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 task:
95
- last_agent = task.get("last_agent")
96
- if agent and last_agent and last_agent != agent:
97
- lines.append(f"[easy-coding:handoff-from:{last_agent}]")
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
- last_seen = session.get("last_seen_stage")
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
- violation = validate_transition(last_seen, current_stage)
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
- session = load_json(root / ".easy-coding" / "sessions" / f"{os.getppid()}.json") or {}
65
- task_id = session.get("current_task")
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))
@@ -4,7 +4,8 @@ import os
4
4
  from pathlib import Path
5
5
  import sys
6
6
 
7
- from easy_coding_status import build_status_context, load_session
7
+ from easy_coding_state import load_session
8
+ from easy_coding_status import build_status_context
8
9
 
9
10
 
10
11
  def configure_stdio() -> None:
@@ -5,7 +5,8 @@ from datetime import datetime, timezone
5
5
  from pathlib import Path
6
6
  import sys
7
7
 
8
- from easy_coding_status import build_status_context, load_session, write_session
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.