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/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "easy-coding-harness",
3
- "version": "0.2.1",
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 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
 
@@ -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
- 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(
@@ -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
- emit(event_name, build_status_context(root, session, detect_agent()))
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