easy-coding-harness 0.2.1 → 0.3.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 +17 -0
- package/README.md +119 -77
- package/dist/cli.js +39 -8
- package/dist/cli.js.map +1 -1
- package/package.json +9 -1
- package/templates/claude/agents/ec-implementer.md +1 -1
- package/templates/claude/agents/ec-reviewer.md +1 -1
- package/templates/common/skills/ec-analysis/SKILL.md +7 -4
- package/templates/common/skills/ec-implementing/SKILL.md +31 -24
- package/templates/common/skills/ec-memory/SKILL.md +16 -8
- package/templates/common/skills/ec-reviewing/SKILL.md +1 -1
- package/templates/common/skills/ec-workflow/SKILL.md +20 -1
- package/templates/shared-hooks/easy_coding_state.py +95 -1
- package/templates/shared-hooks/inject-workflow-state.py +178 -2
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "easy-coding-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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
|
|
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
|
|
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
|
|
159
|
-
-
|
|
160
|
-
- `
|
|
161
|
-
- `
|
|
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
|
|
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.
|
|
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` →
|
|
39
|
-
- `sequential` →
|
|
40
|
-
|
|
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
|
-
|
|
45
|
+
EVERY STRATEGY = MANDATORY SUB-AGENT DISPATCH. NO EXCEPTIONS.
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
-
|
|
52
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- [ ]
|
|
109
|
-
- [ ]
|
|
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
|
|
60
|
+
MEMORY_LONG IS CONTROLLED BY THE STATE API `memory_long` INSTRUCTION.
|
|
61
61
|
|
|
62
62
|
Before performing ANY distillation work:
|
|
63
|
-
1.
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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 |
|
|
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
|
|
|
@@ -112,6 +112,12 @@ routing matches, and switching happens again.
|
|
|
112
112
|
- **Never skip a stage.** ANALYSIS cannot jump to VERIFICATION; IMPLEMENT cannot start before
|
|
113
113
|
WAITING_CONFIRM passes. No exception for "simple" tasks — simple tasks have short analyses,
|
|
114
114
|
not skipped ones.
|
|
115
|
+
- **State before action.** Every stage advance is a two-step protocol: first persist the
|
|
116
|
+
next stage through the state API, then run that stage's real work. Do not start analysis,
|
|
117
|
+
implementation, review, verification, memory writing, closeout, or task switching while
|
|
118
|
+
`task.json.status` still names the previous stage. After every state API call, treat the
|
|
119
|
+
returned snapshot/read-after-write state as authoritative for the next action and for the
|
|
120
|
+
status line.
|
|
115
121
|
- **ANALYSIS entry gate.** When entering ANALYSIS, your FIRST TWO tool calls must be:
|
|
116
122
|
(1) Read `.easy-coding/templates/dev-spec-skeleton.md`, then (2) Write its exact content
|
|
117
123
|
to the task's dev-spec.md. This is a mechanical copy, not a generation task. Do not read
|
|
@@ -127,9 +133,15 @@ routing matches, and switching happens again.
|
|
|
127
133
|
AND the user asked for autonomous execution. `auto_mode` ONLY waives this confirmation step;
|
|
128
134
|
it carries NO scope or delivery-form decision. Never cite `auto_mode` (or "the user already
|
|
129
135
|
decided in INIT") to justify narrowing scope or downgrading a code task to a report.
|
|
136
|
+
On confirmation, the harness hook may have already advanced the task to IMPLEMENT before
|
|
137
|
+
this reply is generated. If the breadcrumb/state is still WAITING_CONFIRM, call the state
|
|
138
|
+
API to transition to IMPLEMENT immediately; only after the read-after-write state says
|
|
139
|
+
IMPLEMENT may you dispatch ec-implementing.
|
|
130
140
|
- **On every transition** call the state API immediately (not at turn end):
|
|
131
141
|
`{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py transition --session-file <P> --stage <STAGE> --agent <agent-id>`.
|
|
132
142
|
Do not hand-edit `status`, `stage_history`, `last_agent`, `current_task`, or session files.
|
|
143
|
+
If the target stage is already present because a hook preflight completed it, do not issue a
|
|
144
|
+
duplicate transition; use the latest snapshot and continue with the target stage's action.
|
|
133
145
|
- **Hook enforcement.** The `inject-workflow-state` hook validates every stage transition
|
|
134
146
|
against the state machine. If you see `[ILLEGAL-TRANSITION:...]` in the injected context,
|
|
135
147
|
you MUST revert the task's status to the previous valid stage and explain why the
|
|
@@ -145,6 +157,13 @@ routing matches, and switching happens again.
|
|
|
145
157
|
- **Archive only after user acceptance.** VERIFICATION passing does not complete the task.
|
|
146
158
|
After the user accepts, call state API transitions in order:
|
|
147
159
|
MEMORY_SHORT → MEMORY_LONG → COMPLETE. Do not jump directly from VERIFICATION to COMPLETE.
|
|
160
|
+
For each archive step, transition first and run the corresponding action second:
|
|
161
|
+
after MEMORY_SHORT is persisted, write the short memory; after MEMORY_LONG is persisted,
|
|
162
|
+
handle the `memory_long` instruction; after COMPLETE is persisted, produce the final summary.
|
|
163
|
+
When transitioning to MEMORY_LONG, pass the state API snapshot to ec-memory and treat its
|
|
164
|
+
`memory_long` object as authoritative: `action == "no-op"` advances to COMPLETE without
|
|
165
|
+
reading or writing long memory; `action == "distill"` runs distillation for `trim_count`
|
|
166
|
+
older short-memory entries.
|
|
148
167
|
- **COMPLETE closeout:** call the state API with `--stage COMPLETE`. The state API clears
|
|
149
168
|
session `current_task` for terminal tasks, so the next hook injection returns to Ready.
|
|
150
169
|
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
|
-
|
|
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(
|
|
@@ -4,9 +4,103 @@ import os
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
import sys
|
|
6
6
|
|
|
7
|
-
from easy_coding_state import load_session
|
|
7
|
+
from easy_coding_state import StateError, load_session, snapshot_state, transition_task
|
|
8
8
|
from easy_coding_status import build_status_context
|
|
9
9
|
|
|
10
|
+
PROMPT_KEYS = ("prompt", "user_prompt", "userPrompt", "message", "text", "input")
|
|
11
|
+
|
|
12
|
+
NEGATIVE_OR_REVISION_PATTERNS = (
|
|
13
|
+
"修改",
|
|
14
|
+
"调整",
|
|
15
|
+
"重写",
|
|
16
|
+
"重新",
|
|
17
|
+
"补充",
|
|
18
|
+
"换个",
|
|
19
|
+
"换一",
|
|
20
|
+
"不要",
|
|
21
|
+
"别",
|
|
22
|
+
"取消",
|
|
23
|
+
"暂停",
|
|
24
|
+
"等一下",
|
|
25
|
+
"先别",
|
|
26
|
+
"有问题",
|
|
27
|
+
"还有问题",
|
|
28
|
+
"不对",
|
|
29
|
+
"不是",
|
|
30
|
+
"不行",
|
|
31
|
+
"继续修复",
|
|
32
|
+
"需要修复",
|
|
33
|
+
"再修复",
|
|
34
|
+
"修一下",
|
|
35
|
+
"修正",
|
|
36
|
+
"?",
|
|
37
|
+
"?",
|
|
38
|
+
"revise",
|
|
39
|
+
"change",
|
|
40
|
+
"cancel",
|
|
41
|
+
"pause",
|
|
42
|
+
"wait",
|
|
43
|
+
"stop",
|
|
44
|
+
"don't",
|
|
45
|
+
"do not",
|
|
46
|
+
"hold",
|
|
47
|
+
"problem",
|
|
48
|
+
"issue",
|
|
49
|
+
"keep fixing",
|
|
50
|
+
"continue fixing",
|
|
51
|
+
"needs fixing",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
WAITING_CONFIRM_PATTERNS = (
|
|
55
|
+
"确认",
|
|
56
|
+
"开始",
|
|
57
|
+
"执行",
|
|
58
|
+
"实施",
|
|
59
|
+
"按方案",
|
|
60
|
+
"按计划",
|
|
61
|
+
"没问题",
|
|
62
|
+
"可以",
|
|
63
|
+
"同意",
|
|
64
|
+
"继续",
|
|
65
|
+
"go ahead",
|
|
66
|
+
"proceed",
|
|
67
|
+
"start",
|
|
68
|
+
"implement",
|
|
69
|
+
"approve",
|
|
70
|
+
"approved",
|
|
71
|
+
"yes",
|
|
72
|
+
"ok",
|
|
73
|
+
"okay",
|
|
74
|
+
"confirm",
|
|
75
|
+
"looks good",
|
|
76
|
+
"ship it",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
VERIFICATION_ACCEPT_PATTERNS = (
|
|
80
|
+
"验收通过",
|
|
81
|
+
"接受",
|
|
82
|
+
"通过",
|
|
83
|
+
"可以结束",
|
|
84
|
+
"归档",
|
|
85
|
+
"完成",
|
|
86
|
+
"确认完成",
|
|
87
|
+
"没问题",
|
|
88
|
+
"可以",
|
|
89
|
+
"同意",
|
|
90
|
+
"accept",
|
|
91
|
+
"accepted",
|
|
92
|
+
"approve",
|
|
93
|
+
"approved",
|
|
94
|
+
"looks good",
|
|
95
|
+
"complete",
|
|
96
|
+
"finish",
|
|
97
|
+
"archive",
|
|
98
|
+
"done",
|
|
99
|
+
"ship it",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
WAITING_CONFIRM_EXACT = {"1", "1.", "开始实施", "确认执行"}
|
|
103
|
+
|
|
10
104
|
|
|
11
105
|
def configure_stdio() -> None:
|
|
12
106
|
for stream in (sys.stdin, sys.stdout, sys.stderr):
|
|
@@ -21,6 +115,56 @@ def read_payload() -> dict:
|
|
|
21
115
|
return {}
|
|
22
116
|
|
|
23
117
|
|
|
118
|
+
def coerce_prompt_text(value: object) -> str:
|
|
119
|
+
if isinstance(value, str):
|
|
120
|
+
return value
|
|
121
|
+
if isinstance(value, list):
|
|
122
|
+
return "\n".join(part for item in value if (part := coerce_prompt_text(item)))
|
|
123
|
+
if isinstance(value, dict):
|
|
124
|
+
parts: list[str] = []
|
|
125
|
+
for key in PROMPT_KEYS:
|
|
126
|
+
if key in value:
|
|
127
|
+
text = coerce_prompt_text(value[key])
|
|
128
|
+
if text:
|
|
129
|
+
parts.append(text)
|
|
130
|
+
if "content" in value:
|
|
131
|
+
text = coerce_prompt_text(value["content"])
|
|
132
|
+
if text:
|
|
133
|
+
parts.append(text)
|
|
134
|
+
return "\n".join(parts)
|
|
135
|
+
return ""
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def extract_user_prompt(payload: dict) -> str:
|
|
139
|
+
for key in PROMPT_KEYS:
|
|
140
|
+
if key not in payload:
|
|
141
|
+
continue
|
|
142
|
+
text = coerce_prompt_text(payload[key])
|
|
143
|
+
if text:
|
|
144
|
+
return text
|
|
145
|
+
return ""
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def normalize_prompt(text: str) -> str:
|
|
149
|
+
return " ".join(text.casefold().strip().split())
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def has_any(text: str, patterns: tuple[str, ...]) -> bool:
|
|
153
|
+
return any(pattern in text for pattern in patterns)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def infer_confirmed_transition(current_stage: str, prompt: str) -> str | None:
|
|
157
|
+
normalized = normalize_prompt(prompt)
|
|
158
|
+
if not normalized or has_any(normalized, NEGATIVE_OR_REVISION_PATTERNS):
|
|
159
|
+
return None
|
|
160
|
+
if current_stage == "WAITING_CONFIRM":
|
|
161
|
+
if normalized in WAITING_CONFIRM_EXACT or has_any(normalized, WAITING_CONFIRM_PATTERNS):
|
|
162
|
+
return "IMPLEMENT"
|
|
163
|
+
if current_stage == "VERIFICATION" and has_any(normalized, VERIFICATION_ACCEPT_PATTERNS):
|
|
164
|
+
return "MEMORY_SHORT"
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
|
|
24
168
|
def find_ec_root(start: Path) -> Path | None:
|
|
25
169
|
current = start.resolve()
|
|
26
170
|
while True:
|
|
@@ -46,6 +190,35 @@ def detect_agent() -> str:
|
|
|
46
190
|
return "unknown"
|
|
47
191
|
|
|
48
192
|
|
|
193
|
+
def preflight_confirmed_transition(root: Path, session: dict, payload: dict, agent: str) -> dict:
|
|
194
|
+
prompt = extract_user_prompt(payload)
|
|
195
|
+
if not prompt:
|
|
196
|
+
return session
|
|
197
|
+
|
|
198
|
+
state = snapshot_state(root, session=session)
|
|
199
|
+
task_id = state.get("current_task")
|
|
200
|
+
if not task_id or state.get("task_missing") or state.get("is_terminal"):
|
|
201
|
+
return session
|
|
202
|
+
|
|
203
|
+
target_stage = infer_confirmed_transition(str(state.get("status") or ""), prompt)
|
|
204
|
+
if not target_stage:
|
|
205
|
+
return session
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
transition_task(
|
|
209
|
+
root,
|
|
210
|
+
target_stage,
|
|
211
|
+
agent,
|
|
212
|
+
task_id=str(task_id),
|
|
213
|
+
session_file=str(state["session_file"]),
|
|
214
|
+
)
|
|
215
|
+
except StateError:
|
|
216
|
+
return load_session(root) or session
|
|
217
|
+
|
|
218
|
+
# Read after write so the status line renders the authoritative latest stage.
|
|
219
|
+
return load_session(root) or session
|
|
220
|
+
|
|
221
|
+
|
|
49
222
|
def emit(event_name: str, context: str) -> None:
|
|
50
223
|
print(
|
|
51
224
|
json.dumps(
|
|
@@ -75,7 +248,10 @@ def main() -> int:
|
|
|
75
248
|
session = {"current_task": None, "created_at": ""}
|
|
76
249
|
|
|
77
250
|
event_name = payload.get("hook_event_name") or payload.get("hookEventName") or "UserPromptSubmit"
|
|
78
|
-
|
|
251
|
+
agent = detect_agent()
|
|
252
|
+
if event_name == "UserPromptSubmit":
|
|
253
|
+
session = preflight_confirmed_transition(root, session, payload, agent)
|
|
254
|
+
emit(event_name, build_status_context(root, session, agent))
|
|
79
255
|
return 0
|
|
80
256
|
|
|
81
257
|
|