easy-coding-harness 0.3.0 → 0.3.2

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 CHANGED
@@ -6,6 +6,17 @@
6
6
  - `y`:常规功能升级
7
7
  - `z`:日常 bug 修复
8
8
 
9
+ ## 0.3.2
10
+
11
+ - npm 包元数据切换到 GitHub:`repository`、`homepage` 和 `bugs.url` 均指向 `github.com/ysxiiun/easy-coding-harness`。
12
+
13
+ ## 0.3.1
14
+
15
+ - 修复确认执行后首行状态栏仍显示上一阶段的问题:`UserPromptSubmit` hook 在明确确认输入下先执行合法状态迁移,再重新读取最新状态渲染状态栏。
16
+ - `WAITING_CONFIRM -> IMPLEMENT` 和 `VERIFICATION -> MEMORY_SHORT` 采用状态前置策略,避免真实动作已经开始但 `task.json.status` 仍停留在旧阶段。
17
+ - ec-workflow 强化统一规则:所有阶段推进必须先通过 state API 持久化下一阶段,再执行该阶段真实动作;hook 已完成前置迁移时不得重复写入。
18
+ - README 中 CHANGELOG 超链接改为 GitHub `master` 文件地址,便于 npm 页面和外部用户访问。
19
+
9
20
  ## 0.3.0
10
21
 
11
22
  - 补充 `repository` / `homepage` / `bugs` 包元数据,并把 README 中的 CHANGELOG 链接改为 GitLab 绝对地址,修复 npm 页面相对链接跳转到 404 的问题。
package/README.md CHANGED
@@ -135,7 +135,7 @@ easy-coding upgrade
135
135
  - `y`:常规功能升级
136
136
  - `z`:日常 bug 修复
137
137
 
138
- 完整更新日志见 [CHANGELOG.md](https://gitlab.alibaba-inc.com/shixin.ysx/easy-coding-harness/-/blob/master/CHANGELOG.md)。
138
+ 完整更新日志见 [CHANGELOG.md](https://github.com/ysxiiun/easy-coding-harness/blob/master/CHANGELOG.md)。
139
139
 
140
140
  ## 开发者命令
141
141
 
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "easy-coding-harness",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "CLI scaffold for installing Easy Coding harness files into agent-native directories.",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "git+https://gitlab.alibaba-inc.com/shixin.ysx/easy-coding-harness.git"
7
+ "url": "git+https://github.com/ysxiiun/easy-coding-harness.git"
8
8
  },
9
- "homepage": "https://gitlab.alibaba-inc.com/shixin.ysx/easy-coding-harness#readme",
9
+ "homepage": "https://github.com/ysxiiun/easy-coding-harness#readme",
10
10
  "bugs": {
11
- "url": "https://gitlab.alibaba-inc.com/shixin.ysx/easy-coding-harness/-/issues"
11
+ "url": "https://github.com/ysxiiun/easy-coding-harness/issues"
12
12
  },
13
13
  "type": "module",
14
14
  "bin": {
@@ -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,9 @@ 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.
148
163
  When transitioning to MEMORY_LONG, pass the state API snapshot to ec-memory and treat its
149
164
  `memory_long` object as authoritative: `action == "no-op"` advances to COMPLETE without
150
165
  reading or writing long memory; `action == "distill"` runs distillation for `trim_count`
@@ -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