easy-coding-harness 0.3.0 → 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
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
- `y`:常规功能升级
|
|
7
7
|
- `z`:日常 bug 修复
|
|
8
8
|
|
|
9
|
+
## 0.3.1
|
|
10
|
+
|
|
11
|
+
- 修复确认执行后首行状态栏仍显示上一阶段的问题:`UserPromptSubmit` hook 在明确确认输入下先执行合法状态迁移,再重新读取最新状态渲染状态栏。
|
|
12
|
+
- `WAITING_CONFIRM -> IMPLEMENT` 和 `VERIFICATION -> MEMORY_SHORT` 采用状态前置策略,避免真实动作已经开始但 `task.json.status` 仍停留在旧阶段。
|
|
13
|
+
- ec-workflow 强化统一规则:所有阶段推进必须先通过 state API 持久化下一阶段,再执行该阶段真实动作;hook 已完成前置迁移时不得重复写入。
|
|
14
|
+
- README 中 CHANGELOG 超链接改为 GitHub `master` 文件地址,便于 npm 页面和外部用户访问。
|
|
15
|
+
|
|
9
16
|
## 0.3.0
|
|
10
17
|
|
|
11
18
|
- 补充 `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://
|
|
138
|
+
完整更新日志见 [CHANGELOG.md](https://github.com/ysxiiun/easy-coding-harness/blob/master/CHANGELOG.md)。
|
|
139
139
|
|
|
140
140
|
## 开发者命令
|
|
141
141
|
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|