easy-coding-harness 0.2.1 → 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/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "easy-coding-harness",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "CLI scaffold for installing Easy Coding harness files into agent-native directories.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://gitlab.alibaba-inc.com/shixin.ysx/easy-coding-harness.git"
8
+ },
9
+ "homepage": "https://gitlab.alibaba-inc.com/shixin.ysx/easy-coding-harness#readme",
10
+ "bugs": {
11
+ "url": "https://gitlab.alibaba-inc.com/shixin.ysx/easy-coding-harness/-/issues"
12
+ },
5
13
  "type": "module",
6
14
  "bin": {
7
15
  "easy-coding": "./dist/cli.js"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ec-implementer
3
- description: Easy Coding implementation sub-agent. Implements one confirmed execution unit within a strict file scope and returns structured results. Dispatched by ec-implementing during parallel IMPLEMENT.
3
+ description: Easy Coding implementation sub-agent. Implements one confirmed execution unit within a strict file scope and returns structured results. Dispatched by ec-implementing for every unit regardless of strategy.
4
4
  ---
5
5
 
6
6
  You are an Easy Coding implementation sub-agent. You receive a task card with one unit and
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ec-reviewer
3
- description: Easy Coding review sub-agent. Reviews changed files along one assigned dimension (correctness or compliance) and returns evidence-backed findings. Dispatched by ec-reviewing when the change set is large.
3
+ description: Easy Coding review sub-agent. Reviews changed files along one assigned dimension (correctness or compliance) and returns evidence-backed findings. Dispatched by ec-reviewing for every review regardless of change-set size.
4
4
  ---
5
5
 
6
6
  You are an Easy Coding review sub-agent. You review the changed files along the single
@@ -155,10 +155,13 @@ Decompose the work into units, then append ONE `plan` record to
155
155
  {"type":"plan","strategy":"parallel","units":[{"id":"U1","title":"...","type":"backend","files":["..."],"depends_on":[],"rules_sections":["naming","error-handling"],"abstract_modules":["user-service"]}],"parallel_groups":[{"level":0,"units":["U1","U2"]},{"level":1,"units":["U3"]}]}
156
156
  ```
157
157
 
158
- Strategy selection (drives whether ec-implementing spawns sub-agents):
159
- - `single` — one unit. Main agent implements directly.
160
- - `sequential` — multiple units with a hard dependency chain.
161
- - `parallel` — two or more independent units. ec-implementing MUST use sub-agents.
158
+ Strategy selection (drives ec-implementing's sub-agent orchestration shape — every strategy
159
+ dispatches sub-agents; none implements inline):
160
+ - `single` — one unit. ec-implementing dispatches one sub-agent.
161
+ - `sequential` — multiple units with a hard dependency chain. ec-implementing dispatches
162
+ sub-agents one at a time in dependency order.
163
+ - `parallel` — two or more independent units. ec-implementing dispatches sub-agents per level
164
+ concurrently.
162
165
 
163
166
  Each unit carries `rules_sections` and `abstract_modules` so ec-implementing can build a
164
167
  precise task card without the sub-agent re-reading the whole repo. `depends_on` sets the
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ec-implementing
3
- description: IMPLEMENT-stage skill. Use when ec-workflow enters IMPLEMENT with a confirmed plan. Executes execution.jsonl units under strict file-scope control, enforces RULES compliance and encoding preservation, writes tests per strategy, and dispatches sub-agents for parallel work.
3
+ description: IMPLEMENT-stage skill. Use when ec-workflow enters IMPLEMENT with a confirmed plan. Executes execution.jsonl units under strict file-scope control, enforces RULES compliance and encoding preservation, writes tests per strategy, and dispatches sub-agents for every unit regardless of strategy.
4
4
  ---
5
5
 
6
6
  # ec-implementing — execute the confirmed plan
@@ -33,38 +33,45 @@ Communicate with the user in the user's language.
33
33
 
34
34
  ## Sub-agent dispatch
35
35
 
36
- Read the `plan` record's `strategy` field. Then:
36
+ Read the `plan` record's `strategy` field. EVERY strategy dispatches sub-agents — the field
37
+ only decides the orchestration shape, never whether the main agent writes code itself:
37
38
 
38
- - `single` → main agent implements directly, no sub-agents.
39
- - `sequential` → main agent implements units one by one in dependency order.
40
- - `parallel` sub-agents are MANDATORY (see gate below).
39
+ - `single` → dispatch ONE sub-agent for the single unit.
40
+ - `sequential` → dispatch sub-agents one at a time in dependency order (await each `result`
41
+ before dispatching the next).
42
+ - `parallel` → dispatch sub-agents per level concurrently (see gate below).
41
43
 
42
44
  <HARD-GATE>
43
- PARALLEL STRATEGY = MANDATORY SUB-AGENT DISPATCH. NO EXCEPTIONS.
45
+ EVERY STRATEGY = MANDATORY SUB-AGENT DISPATCH. NO EXCEPTIONS.
44
46
 
45
- If the plan record says `"strategy":"parallel"`, you MUST dispatch sub-agents using
46
- {{sub_agent_dispatch}}. You are FORBIDDEN from implementing parallel units yourself.
47
- Doing the work inline instead of dispatching is a protocol violation equivalent to
48
- skipping WAITING_CONFIRM.
47
+ You MUST dispatch sub-agents using {{sub_agent_dispatch}} for every unit, whatever the
48
+ strategy. You are FORBIDDEN from implementing any unit yourself in the main agent. Doing the
49
+ work inline instead of dispatching is a protocol violation equivalent to skipping
50
+ WAITING_CONFIRM.
49
51
 
50
52
  Self-check before writing ANY implementation code:
51
- - Is strategy "parallel"? Did I dispatch via {{sub_agent_dispatch}}? If no → STOP.
52
- - Am I working on a unit in a parallel level without dispatching? → STOP.
53
+ - Am I about to write implementation code in the main agent? → STOP. Dispatch a sub-agent.
54
+ - Did every unit get a dispatch (single=1, sequential=N serial, parallel=N concurrent)? If no → STOP.
53
55
 
54
- The ONLY case where you implement code directly is strategy "single" or "sequential".
56
+ There is NO case where the main agent writes implementation code itself. Even a single unit
57
+ goes through a sub-agent — this isolates implementation context from the main agent's window.
55
58
  </HARD-GATE>
56
59
 
57
- Parallel dispatch loop:
58
- 1. Sort `parallel_groups` by level.
59
- 2. Read RULES.md and ABSTRACT.md once.
60
- 3. For each unit in the current level, build a **task card** (next section) and dispatch.
61
- 4. Dispatch all units in the level concurrently via {{sub_agent_dispatch}}.
60
+ ## Dispatch loop (all strategies)
61
+
62
+ 1. Read RULES.md and ABSTRACT.md once — the main agent pre-digests context; sub-agents never
63
+ read them.
64
+ 2. For each unit, build a **task card** (next section). Append a `dispatch` record before
65
+ dispatching and a `result` record per returned unit, to execution.jsonl.
66
+ 3. Dispatch according to strategy, via {{sub_agent_dispatch}}:
67
+ - `single` → dispatch the one unit, await its `result`.
68
+ - `sequential` → sort units by `depends_on`; dispatch one, await its `result`, then the next.
69
+ - `parallel` → sort `parallel_groups` by level; dispatch all units in a level concurrently,
70
+ await the level, then advance.
62
71
  Platform spawn rule: {{platform_spawn_instruction}}
63
- 5. Append a `dispatch` record per unit, then a `result` record per returned unit, to
64
- execution.jsonl.
65
- 6. After the level returns: check for file conflicts (two units touched the same file),
72
+ 4. After each unit/level returns: check for file conflicts (two units touched the same file),
66
73
  collect `issues` and `needs_attention`. Resolve conflicts before advancing.
67
- 7. Advance to the next level. After all levels, summarize.
74
+ 5. After all units/levels, summarize.
68
75
 
69
76
  ## Task card — the sub-agent contract
70
77
 
@@ -105,8 +112,8 @@ hit something that invalidates the plan, return to ANALYSIS instead of improvisi
105
112
 
106
113
  ## Self-check gates (before handing back)
107
114
 
108
- - [ ] Strategy was "parallel" ALL units dispatched via sub-agents? (VIOLATION if no)
109
- - [ ] Strategy was "sequential" → units implemented in dependency order?
115
+ - [ ] EVERY unit was dispatched to a sub-agent no inline implementation by the main agent? (VIOLATION if no)
116
+ - [ ] Sequential units dispatched in dependency order; parallel units dispatched per level?
110
117
  - [ ] Each dispatched unit has a `dispatch` record in execution.jsonl?
111
118
  - [ ] Each returned unit has a `result` record?
112
119
  - [ ] No files modified outside the change-scope table?
@@ -57,21 +57,25 @@ responsibility and only runs when the threshold is exceeded.
57
57
  ## MEMORY_LONG — distill durable knowledge (CONDITIONAL)
58
58
 
59
59
  <HARD-GATE>
60
- MEMORY_LONG IS A NO-OP WHEN SHORT MEMORY COUNT <= threshold.
60
+ MEMORY_LONG IS CONTROLLED BY THE STATE API `memory_long` INSTRUCTION.
61
61
 
62
62
  Before performing ANY distillation work:
63
- 1. Count `.md` files in `.easy-coding/memory/short/` (only files with schema-v2 frontmatter).
64
- 2. Read `memory.short_term_max` from `.easy-coding/config.yaml` (default: 10).
65
- 3. If count <= short_term_max: output "MEMORY_LONG: no-op (short memory count = {N},
63
+ 1. Read the `memory_long` object returned by the state API transition to MEMORY_LONG.
64
+ If it is not visible in context, re-run the same transition command for MEMORY_LONG to
65
+ re-emit the snapshot, then use that `memory_long` object.
66
+ 2. Treat `memory_long.action` as authoritative. Do NOT recount short memories yourself and
67
+ do NOT override the state API instruction with prompt reasoning.
68
+ 3. If `action == "no-op"`: output "MEMORY_LONG: no-op (short memory count = {short_count},
66
69
  threshold = {short_term_max})" and immediately hand back to ec-workflow to advance to
67
70
  COMPLETE. Do NOT read long memory files, do NOT attempt distillation, do NOT modify any file.
68
- 4. If count > short_term_max: proceed with distillation below.
71
+ 4. If `action == "distill"`: distill exactly the older `trim_count` short-memory entries
72
+ and keep the latest `short_term_keep` entries.
69
73
 
70
74
  This gate is absolute. Even a single short memory entry below threshold does NOT trigger
71
75
  long-term compression regardless of any other signal.
72
76
  </HARD-GATE>
73
77
 
74
- ### When count > threshold: distillation flow
78
+ ### When action == "distill": distillation flow
75
79
 
76
80
  Three-file long memory:
77
81
  - `MEMORY.md` — index of all entries with status (active | deprecated | superseded | deleted).
@@ -79,8 +83,8 @@ Three-file long memory:
79
83
  - `TECHNICAL.md` — architecture decisions, implementation patterns, gotchas.
80
84
 
81
85
  Distillation steps:
82
- 1. Read `memory.short_term_keep` from config (default: 5). Keep the latest N entries;
83
- the older entries become distillation candidates.
86
+ 1. Use `memory_long.short_term_keep` and `memory_long.trim_count`. Keep the latest
87
+ `short_term_keep` entries; the older `trim_count` entries become distillation candidates.
84
88
  2. Read the `target_long` of candidate short memories; route to business/technical.
85
89
  3. **Progressive loading** — read only the existing long entries matching this round's
86
90
  domain/tags/related_files. No unbounded whole-repo memory scan.
@@ -90,6 +94,10 @@ Distillation steps:
90
94
  5. **Retirement check**: for older entries decide delete (no value) / merge (semantic
91
95
  duplicate) / deprecate (was valid, now superseded).
92
96
  6. Update the `MEMORY.md` index to reflect every change.
97
+ 7. After the long-memory files and index are successfully written, delete the consumed
98
+ short-memory candidate files. Leave only the latest `short_term_keep` short memories.
99
+ This is sliding-window consumption after successful distillation, not destructive
100
+ deletion of durable long-term knowledge.
93
101
 
94
102
  ## ABSTRACT backfill / update
95
103
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ec-reviewing
3
- description: REVIEW-stage skill. Use when ec-workflow enters REVIEW. Reviews changed files across correctness, RULES compliance, completeness, tests, and security; returns a graded verdict (accept/fix/replan/blocked) with file-and-line evidence; dispatches multi-dimension sub-agents when the change set is large.
3
+ description: REVIEW-stage skill. Use when ec-workflow enters REVIEW. Reviews changed files across correctness, RULES compliance, completeness, tests, and security; returns a graded verdict (accept/fix/replan/blocked) with file-and-line evidence; dispatches multi-dimension sub-agents for every review regardless of change-set size.
4
4
  ---
5
5
 
6
6
  # ec-reviewing — graded, evidence-backed review
@@ -85,7 +85,7 @@ any stage --[user abort via ec-task-close]--> CLOSED
85
85
  | REVIEW | ec-reviewing | multi-dimension code review | verdict = accept |
86
86
  | VERIFICATION | ec-verification | hard gate: lint/typecheck/test + coverage | all pass AND user accepts |
87
87
  | MEMORY_SHORT | ec-memory | archive: short memory entry | written |
88
- | MEMORY_LONG | ec-memory | archive: long memory distillation | written |
88
+ | MEMORY_LONG | ec-memory | archive: long memory distillation (conditional — no-op when short memory is under threshold) | state API `memory_long` instruction handled |
89
89
  | COMPLETE | ec-workflow | clear current_task, set task status, summary | terminal |
90
90
  | CLOSED | ec-task-close | user abort; no memory flow | terminal |
91
91
 
@@ -145,6 +145,10 @@ routing matches, and switching happens again.
145
145
  - **Archive only after user acceptance.** VERIFICATION passing does not complete the task.
146
146
  After the user accepts, call state API transitions in order:
147
147
  MEMORY_SHORT → MEMORY_LONG → COMPLETE. Do not jump directly from VERIFICATION to COMPLETE.
148
+ When transitioning to MEMORY_LONG, pass the state API snapshot to ec-memory and treat its
149
+ `memory_long` object as authoritative: `action == "no-op"` advances to COMPLETE without
150
+ reading or writing long memory; `action == "distill"` runs distillation for `trim_count`
151
+ older short-memory entries.
148
152
  - **COMPLETE closeout:** call the state API with `--stage COMPLETE`. The state API clears
149
153
  session `current_task` for terminal tasks, so the next hook injection returns to Ready.
150
154
  Then output a summary (what was done, files changed, key decisions).
@@ -27,6 +27,9 @@ VALID_TRANSITIONS: dict[str, set[str]] = {
27
27
  "CLOSED": set(),
28
28
  }
29
29
 
30
+ DEFAULT_SHORT_TERM_MAX = 10
31
+ DEFAULT_SHORT_TERM_KEEP = 5
32
+
30
33
 
31
34
  class StateError(Exception):
32
35
  pass
@@ -61,6 +64,94 @@ def load_json(path: Path) -> dict | None:
61
64
  return None
62
65
 
63
66
 
67
+ def parse_positive_int(value: str) -> int | None:
68
+ normalized = value.split("#", 1)[0].strip().strip("'\"")
69
+ try:
70
+ parsed = int(normalized)
71
+ except ValueError:
72
+ return None
73
+ return parsed if parsed >= 0 else None
74
+
75
+
76
+ def read_memory_config(root: Path) -> dict[str, int]:
77
+ config = {
78
+ "short_term_max": DEFAULT_SHORT_TERM_MAX,
79
+ "short_term_keep": DEFAULT_SHORT_TERM_KEEP,
80
+ }
81
+ path = root / ".easy-coding" / "config.yaml"
82
+ try:
83
+ lines = path.read_text(encoding="utf-8").splitlines()
84
+ except OSError:
85
+ return config
86
+
87
+ in_memory = False
88
+ memory_indent = 0
89
+ for raw_line in lines:
90
+ without_comment = raw_line.split("#", 1)[0].rstrip()
91
+ stripped = without_comment.strip()
92
+ if not stripped:
93
+ continue
94
+ indent = len(without_comment) - len(without_comment.lstrip(" "))
95
+ if stripped == "memory:":
96
+ in_memory = True
97
+ memory_indent = indent
98
+ continue
99
+ if in_memory and indent <= memory_indent:
100
+ in_memory = False
101
+ if not in_memory or ":" not in stripped:
102
+ continue
103
+ key, value = stripped.split(":", 1)
104
+ if key not in config:
105
+ continue
106
+ parsed = parse_positive_int(value)
107
+ if parsed is not None:
108
+ config[key] = parsed
109
+ return config
110
+
111
+
112
+ def count_short_memories(root: Path) -> int:
113
+ short_dir = root / ".easy-coding" / "memory" / "short"
114
+ if not short_dir.is_dir():
115
+ return 0
116
+ count = 0
117
+ for entry in short_dir.glob("*.md"):
118
+ try:
119
+ if is_schema_v2_short_memory(entry.read_text(encoding="utf-8")):
120
+ count += 1
121
+ except OSError:
122
+ continue
123
+ return count
124
+
125
+
126
+ def is_schema_v2_short_memory(content: str) -> bool:
127
+ lines = content.splitlines()
128
+ if not lines or lines[0].strip() != "---":
129
+ return False
130
+ for line in lines[1:]:
131
+ stripped = line.strip()
132
+ if stripped == "---":
133
+ return False
134
+ if not stripped.startswith("memory_schema:"):
135
+ continue
136
+ _, value = stripped.split(":", 1)
137
+ return parse_positive_int(value) == 2
138
+ return False
139
+
140
+
141
+ def build_memory_long_instruction(root: Path) -> dict:
142
+ config = read_memory_config(root)
143
+ short_count = count_short_memories(root)
144
+ action = "distill" if short_count > config["short_term_max"] else "no-op"
145
+ trim_count = max(0, short_count - config["short_term_keep"]) if action == "distill" else 0
146
+ return {
147
+ "short_count": short_count,
148
+ "short_term_max": config["short_term_max"],
149
+ "short_term_keep": config["short_term_keep"],
150
+ "action": action,
151
+ "trim_count": trim_count,
152
+ }
153
+
154
+
64
155
  def write_json(path: Path, data: dict) -> None:
65
156
  path.parent.mkdir(parents=True, exist_ok=True)
66
157
  path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
@@ -331,7 +422,10 @@ def transition_task(
331
422
  session["last_seen_stage"] = stage
332
423
  session["last_agent"] = agent
333
424
  write_session(root, session, session_file)
334
- return snapshot_state(root, session_file, session)
425
+ snapshot = snapshot_state(root, session_file, session)
426
+ if stage == "MEMORY_LONG":
427
+ snapshot["memory_long"] = build_memory_long_instruction(root)
428
+ return snapshot
335
429
 
336
430
 
337
431
  def close_current_task(