easy-coding-harness 0.3.2 → 0.3.3
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 +7 -0
- package/package.json +1 -1
- package/templates/common/bundled-skills/ec-init/SKILL.md +4 -2
- package/templates/common/skills/ec-analysis/SKILL.md +1 -0
- package/templates/common/skills/ec-task-close/SKILL.md +2 -0
- package/templates/common/skills/ec-task-management/SKILL.md +4 -0
- package/templates/common/skills/ec-verification/SKILL.md +7 -4
- package/templates/common/skills/ec-workflow/SKILL.md +8 -2
- package/templates/shared-hooks/easy_coding_state.py +199 -11
- package/templates/shared-hooks/easy_coding_status.py +4 -109
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
- `y`:常规功能升级
|
|
7
7
|
- `z`:日常 bug 修复
|
|
8
8
|
|
|
9
|
+
## 0.3.3
|
|
10
|
+
|
|
11
|
+
- 修复同一轮会话内状态栏仍显示旧状态的问题:`easy_coding_state.py` 的所有写状态命令现在都会在写入后立即回读 session/task,并返回最新 `status_line` 和 `status_context`。
|
|
12
|
+
- `create-task`、`set-current`、`clear-current`、`transition`、`close-current`、`project-init-complete`、`set-repo-path` 输出统一携带最新状态上下文,覆盖新建任务、切换任务、阶段迁移、关闭任务和初始化完成等所有状态写路径。
|
|
13
|
+
- 状态渲染逻辑收口到 state API,`easy_coding_status.py` 仅保留兼容导出,避免 hook 渲染和 state API 回读逻辑再次漂移。
|
|
14
|
+
- skill 模板强化规则:任何状态写入命令返回后,必须使用返回的 `status_context` 作为当前轮的权威状态来源,丢弃旧的 hook 注入状态。
|
|
15
|
+
|
|
9
16
|
## 0.3.2
|
|
10
17
|
|
|
11
18
|
- npm 包元数据切换到 GitHub:`repository`、`homepage` 和 `bugs.url` 均指向 `github.com/ysxiiun/easy-coding-harness`。
|
package/package.json
CHANGED
|
@@ -82,7 +82,8 @@ On decline: exit with the gap list as a reference for the user to address manual
|
|
|
82
82
|
gaps were fixed), run
|
|
83
83
|
`{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py project-init-complete --agent <agent-id>`.
|
|
84
84
|
This removes `pending_init_since` and clears the "Waiting init · Upgrade" status line so the
|
|
85
|
-
user is no longer prompted on every session.
|
|
85
|
+
user is no longer prompted on every session. Use the returned `status_context` as the current
|
|
86
|
+
status source.
|
|
86
87
|
|
|
87
88
|
## Project mode detection (automatic — never ask the user)
|
|
88
89
|
|
|
@@ -138,7 +139,8 @@ agent must be able to see what was generated and on what evidence.
|
|
|
138
139
|
fake entries.
|
|
139
140
|
7. **Mark complete** — write the final `init_log` entry, then run
|
|
140
141
|
`{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py project-init-complete --agent <agent-id>`.
|
|
141
|
-
|
|
142
|
+
Use the returned `status_context`, then tell the user initialization is done and daily work
|
|
143
|
+
goes through `{{skill_trigger}}ec-workflow`.
|
|
142
144
|
|
|
143
145
|
## Startup project flow
|
|
144
146
|
|
|
@@ -72,6 +72,7 @@ If the task spans repositories: declare them in the dev-spec (trigger repo + inv
|
|
|
72
72
|
by **name**, never local paths). For each involved repo, read its ABSTRACT to understand the
|
|
73
73
|
interface. Cache any local path the user provides through the state API only:
|
|
74
74
|
`{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py set-repo-path --session-file <P> --repo <repo-name> --path <local-path>`.
|
|
75
|
+
Use the returned `status_context` as the latest state source after this write.
|
|
75
76
|
If a repo name cannot be located locally, ask the user for the path before proceeding.
|
|
76
77
|
|
|
77
78
|
## Analysis procedure (mandatory sequence)
|
|
@@ -22,6 +22,8 @@ when you recognize abandonment intent in the user's message.
|
|
|
22
22
|
`{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py close-current --session-file <P> --reason "<reason>" --agent <agent-id>`.
|
|
23
23
|
This sets `task.json.status` to `CLOSED`, records `closed_reason`, updates history, and
|
|
24
24
|
clears session `current_task` so the next hook injection returns to Ready.
|
|
25
|
+
Use the returned `status_context` as the authoritative status source for the rest of the
|
|
26
|
+
current turn.
|
|
25
27
|
4. **No memory flow.** Do not run MEMORY_SHORT/LONG. An incomplete task's memory is dirty data.
|
|
26
28
|
5. **Linked tasks.** If the task has `spawned_from` or `spawned_tasks`, note the closure fact
|
|
27
29
|
on the relation so a future agent understands the chain.
|
|
@@ -25,6 +25,10 @@ If the user explicitly wants to start the task now, omit `--no-set-current`; oth
|
|
|
25
25
|
the current workflow pointer untouched and tell them to run `{{skill_trigger}}ec-workflow`
|
|
26
26
|
when ready.
|
|
27
27
|
|
|
28
|
+
The command returns `status_line` and `status_context`. If the command sets or changes
|
|
29
|
+
`current_task`, use the returned context as the authoritative status source for the rest of
|
|
30
|
+
the current turn instead of older hook-injected status text.
|
|
31
|
+
|
|
28
32
|
## Boundaries
|
|
29
33
|
|
|
30
34
|
- No workflow recovery — point the user to ec-workflow to resume or progress a task.
|
|
@@ -77,13 +77,16 @@ change formally returns to IMPLEMENT and re-walks REVIEW → VERIFICATION.
|
|
|
77
77
|
Runs automatically once the user accepts:
|
|
78
78
|
1. MEMORY_SHORT — call
|
|
79
79
|
`{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py transition --session-file <P> --stage MEMORY_SHORT --agent <agent-id>`,
|
|
80
|
-
|
|
80
|
+
use the returned `status_context` as the latest status source, then hand control to
|
|
81
|
+
ec-memory to write the short memory entry.
|
|
81
82
|
2. MEMORY_LONG — after MEMORY_SHORT returns, call
|
|
82
83
|
`{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py transition --session-file <P> --stage MEMORY_LONG --agent <agent-id>`,
|
|
83
|
-
|
|
84
|
+
use the returned `status_context` and `memory_long` object as authoritative, then hand
|
|
85
|
+
control to ec-memory to distill long memory or perform its no-op gate.
|
|
84
86
|
3. COMPLETE — after MEMORY_LONG returns, call
|
|
85
87
|
`{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py transition --session-file <P> --stage COMPLETE --agent <agent-id>`,
|
|
86
|
-
which clears session `current_task` for the completed task.
|
|
87
|
-
(what was done, files changed, key
|
|
88
|
+
which clears session `current_task` for the completed task. Use the returned
|
|
89
|
+
`status_context`, then output the task summary (what was done, files changed, key
|
|
90
|
+
decisions).
|
|
88
91
|
|
|
89
92
|
Hand control back to ec-workflow at each transition; ec-workflow owns the stage writes.
|
|
@@ -61,7 +61,7 @@ replies are English.
|
|
|
61
61
|
the task through the state API, which creates `task.json`, sets `status:"INIT"`, writes
|
|
62
62
|
`stage_history`, and sets the session `current_task`:
|
|
63
63
|
`{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py create-task --session-file <P> --task-id <MM-DD-task-slug> --type <type> --title "<one-line summary>" --agent <agent-id>`.
|
|
64
|
-
Then enter INIT.
|
|
64
|
+
Use the returned `status_context` as the current status source. Then enter INIT.
|
|
65
65
|
|
|
66
66
|
## State machine
|
|
67
67
|
|
|
@@ -100,7 +100,8 @@ When the user confirms switching from task A to task B:
|
|
|
100
100
|
1. Task A's status is already persisted in its `task.json` — nothing extra to save.
|
|
101
101
|
2. Set the current task through the state API:
|
|
102
102
|
`{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py set-current --session-file <P> --task-id <task-b-id> --agent <agent-id>`.
|
|
103
|
-
3.
|
|
103
|
+
3. Use the returned `status_context` as the current status source, then read task B's
|
|
104
|
+
`task.json` to determine its current stage.
|
|
104
105
|
4. Resume task B's stage via the appropriate stage skill.
|
|
105
106
|
|
|
106
107
|
No data is lost — task A's dev-spec, execution.jsonl, and test-strategy.md stay intact on
|
|
@@ -140,6 +141,9 @@ routing matches, and switching happens again.
|
|
|
140
141
|
- **On every transition** call the state API immediately (not at turn end):
|
|
141
142
|
`{{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py transition --session-file <P> --stage <STAGE> --agent <agent-id>`.
|
|
142
143
|
Do not hand-edit `status`, `stage_history`, `last_agent`, `current_task`, or session files.
|
|
144
|
+
The command returns `status_line` and `status_context`; after any state-changing command,
|
|
145
|
+
discard older hook-injected status text and use this returned context as the authoritative
|
|
146
|
+
status source for the rest of the current turn.
|
|
143
147
|
If the target stage is already present because a hook preflight completed it, do not issue a
|
|
144
148
|
duplicate transition; use the latest snapshot and continue with the target stage's action.
|
|
145
149
|
- **Hook enforcement.** The `inject-workflow-state` hook validates every stage transition
|
|
@@ -160,6 +164,8 @@ routing matches, and switching happens again.
|
|
|
160
164
|
For each archive step, transition first and run the corresponding action second:
|
|
161
165
|
after MEMORY_SHORT is persisted, write the short memory; after MEMORY_LONG is persisted,
|
|
162
166
|
handle the `memory_long` instruction; after COMPLETE is persisted, produce the final summary.
|
|
167
|
+
At each transition, use the state API's returned `status_context` as the latest status
|
|
168
|
+
source before running the next action.
|
|
163
169
|
When transitioning to MEMORY_LONG, pass the state API snapshot to ec-memory and treat its
|
|
164
170
|
`memory_long` object as authoritative: `action == "no-op"` advances to COMPLETE without
|
|
165
171
|
reading or writing long memory; `action == "distill"` runs distillation for `trim_count`
|
|
@@ -12,6 +12,27 @@ HELP_SUFFIX = (
|
|
|
12
12
|
"Use `ec-workflow` to start or resume a task, "
|
|
13
13
|
"`ec-brainstorming` to brainstorm, or `ec-task-management` to view tasks"
|
|
14
14
|
)
|
|
15
|
+
READY_LINE = (
|
|
16
|
+
"> **Easy Coding** · Ready · Use `ec-workflow` to start or resume a task, "
|
|
17
|
+
"`ec-brainstorming` to brainstorm, or `ec-task-management` to view tasks"
|
|
18
|
+
)
|
|
19
|
+
WAITING_INIT_LINE = "> **Easy Coding** · Waiting init · Use `ec-init` to initialize"
|
|
20
|
+
|
|
21
|
+
MANDATORY_DEV_SPEC_HEADERS: list[str] = [
|
|
22
|
+
"## 技术方案",
|
|
23
|
+
"### 项目模式",
|
|
24
|
+
"### 任务类型",
|
|
25
|
+
"### 需求解析",
|
|
26
|
+
"### 现状",
|
|
27
|
+
"### 冲突摘要",
|
|
28
|
+
"### 待用户决策",
|
|
29
|
+
"### 影响面分析",
|
|
30
|
+
"### 改动范围",
|
|
31
|
+
"### 修改方案",
|
|
32
|
+
"### 实施拆解",
|
|
33
|
+
"### 测试策略",
|
|
34
|
+
"### 风险与注意事项",
|
|
35
|
+
]
|
|
15
36
|
|
|
16
37
|
VALID_TRANSITIONS: dict[str, set[str]] = {
|
|
17
38
|
"idle": {"INIT"},
|
|
@@ -291,6 +312,126 @@ def snapshot_state(
|
|
|
291
312
|
}
|
|
292
313
|
|
|
293
314
|
|
|
315
|
+
def build_status_line(
|
|
316
|
+
root: Path,
|
|
317
|
+
session: dict,
|
|
318
|
+
agent: str | None = None,
|
|
319
|
+
session_file: str | Path | None = None,
|
|
320
|
+
) -> str:
|
|
321
|
+
state = snapshot_state(root, session_file, session)
|
|
322
|
+
task_id = state["current_task"]
|
|
323
|
+
if task_id:
|
|
324
|
+
status = str(state["status"])
|
|
325
|
+
line = f"> **Easy Coding** · `{task_id}` · `{status}`"
|
|
326
|
+
last_agent = state.get("last_agent")
|
|
327
|
+
if agent and last_agent and last_agent != agent:
|
|
328
|
+
line += f" · Handoff -> `{last_agent}`"
|
|
329
|
+
if state["is_terminal"] or state["task_missing"]:
|
|
330
|
+
line += f" · {HELP_SUFFIX}"
|
|
331
|
+
return line
|
|
332
|
+
|
|
333
|
+
if is_project_init_required(root):
|
|
334
|
+
return WAITING_INIT_LINE
|
|
335
|
+
|
|
336
|
+
pending = get_pending_init_version(root)
|
|
337
|
+
if pending:
|
|
338
|
+
return (
|
|
339
|
+
f"> **Easy Coding** · Waiting init · "
|
|
340
|
+
f"Upgrade to v{pending} — run `ec-init` to adapt"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
return READY_LINE
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def build_machine_breadcrumbs(
|
|
347
|
+
root: Path,
|
|
348
|
+
session: dict,
|
|
349
|
+
agent: str | None = None,
|
|
350
|
+
session_file: str | Path | None = None,
|
|
351
|
+
) -> list[str]:
|
|
352
|
+
state = snapshot_state(root, session_file, session)
|
|
353
|
+
task_id = state["current_task"]
|
|
354
|
+
task = state["task"]
|
|
355
|
+
stage = str(state["status"]) if task else "idle"
|
|
356
|
+
resolved_session_file = str(state["session_file"])
|
|
357
|
+
lines = [f"[workflow-state:{stage}]", f"[easy-coding:session-file:{resolved_session_file}]"]
|
|
358
|
+
|
|
359
|
+
if task_id:
|
|
360
|
+
lines.append(f"[current-task:{task_id}]")
|
|
361
|
+
if state["task_missing"]:
|
|
362
|
+
lines.append(f"[easy-coding:current-task-missing:{task_id}]")
|
|
363
|
+
last_agent = state.get("last_agent")
|
|
364
|
+
if agent and last_agent and last_agent != agent:
|
|
365
|
+
lines.append(f"[easy-coding:handoff-from:{last_agent}]")
|
|
366
|
+
|
|
367
|
+
if is_project_init_required(root):
|
|
368
|
+
lines.append("[easy-coding:init-required]")
|
|
369
|
+
else:
|
|
370
|
+
pending = get_pending_init_version(root)
|
|
371
|
+
if pending:
|
|
372
|
+
lines.append(f"[easy-coding:upgrade-init-pending:{pending}]")
|
|
373
|
+
|
|
374
|
+
# Stage-specific reminders
|
|
375
|
+
if stage == "ANALYSIS" and task_id:
|
|
376
|
+
dev_spec = root / ".easy-coding" / "tasks" / str(task_id) / "dev-spec.md"
|
|
377
|
+
if dev_spec.exists():
|
|
378
|
+
try:
|
|
379
|
+
content = dev_spec.read_text(encoding="utf-8")
|
|
380
|
+
missing = [h for h in MANDATORY_DEV_SPEC_HEADERS if h not in content]
|
|
381
|
+
if missing:
|
|
382
|
+
names = ",".join(h.lstrip("#").strip() for h in missing)
|
|
383
|
+
lines.append(f"[easy-coding:analysis-template-drift:missing:{names}]")
|
|
384
|
+
else:
|
|
385
|
+
lines.append("[easy-coding:analysis-template-ok]")
|
|
386
|
+
except OSError:
|
|
387
|
+
lines.append("[easy-coding:analysis-gate:skeleton-first-then-fill]")
|
|
388
|
+
else:
|
|
389
|
+
lines.append("[easy-coding:analysis-gate:skeleton-first-then-fill]")
|
|
390
|
+
|
|
391
|
+
# State machine validation
|
|
392
|
+
if task_id and task and task.get("status"):
|
|
393
|
+
current_stage = str(task["status"])
|
|
394
|
+
last_seen = session.get("last_seen_stage")
|
|
395
|
+
violation = record_seen_stage(root, str(task_id), current_stage, resolved_session_file)
|
|
396
|
+
if violation:
|
|
397
|
+
lines.append(f"[ILLEGAL-TRANSITION:{last_seen}->{current_stage}]")
|
|
398
|
+
lines.append(f"[easy-coding:transition-error:{violation}]")
|
|
399
|
+
|
|
400
|
+
return lines
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def build_status_context(
|
|
404
|
+
root: Path,
|
|
405
|
+
session: dict,
|
|
406
|
+
agent: str | None = None,
|
|
407
|
+
session_file: str | Path | None = None,
|
|
408
|
+
) -> str:
|
|
409
|
+
return "\n".join(
|
|
410
|
+
[
|
|
411
|
+
build_status_line(root, session, agent, session_file),
|
|
412
|
+
*build_machine_breadcrumbs(root, session, agent, session_file),
|
|
413
|
+
]
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def attach_status_context(
|
|
418
|
+
root: Path,
|
|
419
|
+
data: dict,
|
|
420
|
+
agent: str | None = None,
|
|
421
|
+
session_file: str | Path | None = None,
|
|
422
|
+
) -> dict:
|
|
423
|
+
resolved_session_file = session_file or data.get("session_file")
|
|
424
|
+
session = load_session(root, resolved_session_file)
|
|
425
|
+
if session is None:
|
|
426
|
+
session = default_session()
|
|
427
|
+
context = build_status_context(root, session, agent, resolved_session_file)
|
|
428
|
+
first_line = context.splitlines()[0] if context else ""
|
|
429
|
+
enriched = dict(data)
|
|
430
|
+
enriched["status_line"] = first_line
|
|
431
|
+
enriched["status_context"] = context
|
|
432
|
+
return enriched
|
|
433
|
+
|
|
434
|
+
|
|
294
435
|
def list_tasks(root: Path) -> list[dict]:
|
|
295
436
|
tasks_dir = root / ".easy-coding" / "tasks"
|
|
296
437
|
if not tasks_dir.is_dir():
|
|
@@ -578,28 +719,75 @@ def main() -> int:
|
|
|
578
719
|
emit({"tasks": list_tasks(root)})
|
|
579
720
|
elif command == "create-task":
|
|
580
721
|
emit(
|
|
581
|
-
|
|
722
|
+
attach_status_context(
|
|
582
723
|
root,
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
724
|
+
create_task(
|
|
725
|
+
root,
|
|
726
|
+
args.task_id,
|
|
727
|
+
args.type,
|
|
728
|
+
args.title,
|
|
729
|
+
args.agent,
|
|
730
|
+
not args.no_set_current,
|
|
731
|
+
session_file,
|
|
732
|
+
),
|
|
586
733
|
args.agent,
|
|
587
|
-
not args.no_set_current,
|
|
588
734
|
session_file,
|
|
589
735
|
)
|
|
590
736
|
)
|
|
591
737
|
elif command == "set-current":
|
|
592
|
-
emit(
|
|
738
|
+
emit(
|
|
739
|
+
attach_status_context(
|
|
740
|
+
root,
|
|
741
|
+
set_current_task(root, args.task_id, args.agent, session_file),
|
|
742
|
+
args.agent,
|
|
743
|
+
session_file,
|
|
744
|
+
)
|
|
745
|
+
)
|
|
593
746
|
elif command == "clear-current":
|
|
594
|
-
emit(
|
|
747
|
+
emit(
|
|
748
|
+
attach_status_context(
|
|
749
|
+
root,
|
|
750
|
+
clear_current_task(root, args.agent, session_file),
|
|
751
|
+
args.agent,
|
|
752
|
+
session_file,
|
|
753
|
+
)
|
|
754
|
+
)
|
|
595
755
|
elif command == "transition":
|
|
596
|
-
emit(
|
|
756
|
+
emit(
|
|
757
|
+
attach_status_context(
|
|
758
|
+
root,
|
|
759
|
+
transition_task(root, args.stage, args.agent, args.task_id, session_file),
|
|
760
|
+
args.agent,
|
|
761
|
+
session_file,
|
|
762
|
+
)
|
|
763
|
+
)
|
|
597
764
|
elif command == "close-current":
|
|
598
|
-
emit(
|
|
765
|
+
emit(
|
|
766
|
+
attach_status_context(
|
|
767
|
+
root,
|
|
768
|
+
close_current_task(root, args.reason, args.agent, session_file),
|
|
769
|
+
args.agent,
|
|
770
|
+
session_file,
|
|
771
|
+
)
|
|
772
|
+
)
|
|
599
773
|
elif command == "project-init-complete":
|
|
600
|
-
emit(
|
|
774
|
+
emit(
|
|
775
|
+
attach_status_context(
|
|
776
|
+
root,
|
|
777
|
+
project_init_complete(root, args.agent),
|
|
778
|
+
args.agent,
|
|
779
|
+
session_file,
|
|
780
|
+
)
|
|
781
|
+
)
|
|
601
782
|
elif command == "set-repo-path":
|
|
602
|
-
emit(
|
|
783
|
+
emit(
|
|
784
|
+
attach_status_context(
|
|
785
|
+
root,
|
|
786
|
+
set_repo_path(root, args.repo, args.path, args.task_id, session_file),
|
|
787
|
+
None,
|
|
788
|
+
session_file,
|
|
789
|
+
)
|
|
790
|
+
)
|
|
603
791
|
return 0
|
|
604
792
|
except StateError as error:
|
|
605
793
|
print(json.dumps({"error": str(error)}, ensure_ascii=False), file=sys.stderr)
|
|
@@ -1,112 +1,7 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
1
|
from easy_coding_state import (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
record_seen_stage,
|
|
8
|
-
snapshot_state,
|
|
2
|
+
build_machine_breadcrumbs,
|
|
3
|
+
build_status_context,
|
|
4
|
+
build_status_line,
|
|
9
5
|
)
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
READY_LINE = (
|
|
13
|
-
"> **Easy Coding** · Ready · Use `ec-workflow` to start or resume a task, "
|
|
14
|
-
"`ec-brainstorming` to brainstorm, or `ec-task-management` to view tasks"
|
|
15
|
-
)
|
|
16
|
-
WAITING_INIT_LINE = "> **Easy Coding** · Waiting init · Use `ec-init` to initialize"
|
|
17
|
-
|
|
18
|
-
MANDATORY_DEV_SPEC_HEADERS: list[str] = [
|
|
19
|
-
"## 技术方案",
|
|
20
|
-
"### 项目模式",
|
|
21
|
-
"### 任务类型",
|
|
22
|
-
"### 需求解析",
|
|
23
|
-
"### 现状",
|
|
24
|
-
"### 冲突摘要",
|
|
25
|
-
"### 待用户决策",
|
|
26
|
-
"### 影响面分析",
|
|
27
|
-
"### 改动范围",
|
|
28
|
-
"### 修改方案",
|
|
29
|
-
"### 实施拆解",
|
|
30
|
-
"### 测试策略",
|
|
31
|
-
"### 风险与注意事项",
|
|
32
|
-
]
|
|
33
|
-
|
|
34
|
-
def build_status_line(root: Path, session: dict, agent: str | None = None) -> str:
|
|
35
|
-
state = snapshot_state(root, session=session)
|
|
36
|
-
task_id = state["current_task"]
|
|
37
|
-
if task_id:
|
|
38
|
-
status = str(state["status"])
|
|
39
|
-
line = f"> **Easy Coding** · `{task_id}` · `{status}`"
|
|
40
|
-
last_agent = state.get("last_agent")
|
|
41
|
-
if agent and last_agent and last_agent != agent:
|
|
42
|
-
line += f" · Handoff -> `{last_agent}`"
|
|
43
|
-
if state["is_terminal"] or state["task_missing"]:
|
|
44
|
-
line += f" · {HELP_SUFFIX}"
|
|
45
|
-
return line
|
|
46
|
-
|
|
47
|
-
if is_project_init_required(root):
|
|
48
|
-
return WAITING_INIT_LINE
|
|
49
|
-
|
|
50
|
-
pending = get_pending_init_version(root)
|
|
51
|
-
if pending:
|
|
52
|
-
return (
|
|
53
|
-
f"> **Easy Coding** · Waiting init · "
|
|
54
|
-
f"Upgrade to v{pending} — run `ec-init` to adapt"
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
return READY_LINE
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def build_machine_breadcrumbs(root: Path, session: dict, agent: str | None = None) -> list[str]:
|
|
61
|
-
state = snapshot_state(root, session=session)
|
|
62
|
-
task_id = state["current_task"]
|
|
63
|
-
task = state["task"]
|
|
64
|
-
stage = str(state["status"]) if task else "idle"
|
|
65
|
-
lines = [f"[workflow-state:{stage}]", f"[easy-coding:session-file:{state['session_file']}]"]
|
|
66
|
-
|
|
67
|
-
if task_id:
|
|
68
|
-
lines.append(f"[current-task:{task_id}]")
|
|
69
|
-
if state["task_missing"]:
|
|
70
|
-
lines.append(f"[easy-coding:current-task-missing:{task_id}]")
|
|
71
|
-
last_agent = state.get("last_agent")
|
|
72
|
-
if agent and last_agent and last_agent != agent:
|
|
73
|
-
lines.append(f"[easy-coding:handoff-from:{last_agent}]")
|
|
74
|
-
|
|
75
|
-
if is_project_init_required(root):
|
|
76
|
-
lines.append("[easy-coding:init-required]")
|
|
77
|
-
else:
|
|
78
|
-
pending = get_pending_init_version(root)
|
|
79
|
-
if pending:
|
|
80
|
-
lines.append(f"[easy-coding:upgrade-init-pending:{pending}]")
|
|
81
|
-
|
|
82
|
-
# Stage-specific reminders
|
|
83
|
-
if stage == "ANALYSIS" and task_id:
|
|
84
|
-
dev_spec = root / ".easy-coding" / "tasks" / str(task_id) / "dev-spec.md"
|
|
85
|
-
if dev_spec.exists():
|
|
86
|
-
try:
|
|
87
|
-
content = dev_spec.read_text(encoding="utf-8")
|
|
88
|
-
missing = [h for h in MANDATORY_DEV_SPEC_HEADERS if h not in content]
|
|
89
|
-
if missing:
|
|
90
|
-
names = ",".join(h.lstrip("#").strip() for h in missing)
|
|
91
|
-
lines.append(f"[easy-coding:analysis-template-drift:missing:{names}]")
|
|
92
|
-
else:
|
|
93
|
-
lines.append("[easy-coding:analysis-template-ok]")
|
|
94
|
-
except OSError:
|
|
95
|
-
lines.append("[easy-coding:analysis-gate:skeleton-first-then-fill]")
|
|
96
|
-
else:
|
|
97
|
-
lines.append("[easy-coding:analysis-gate:skeleton-first-then-fill]")
|
|
98
|
-
|
|
99
|
-
# State machine validation
|
|
100
|
-
if task_id and task and task.get("status"):
|
|
101
|
-
current_stage = str(task["status"])
|
|
102
|
-
last_seen = session.get("last_seen_stage")
|
|
103
|
-
violation = record_seen_stage(root, str(task_id), current_stage)
|
|
104
|
-
if violation:
|
|
105
|
-
lines.append(f"[ILLEGAL-TRANSITION:{last_seen}->{current_stage}]")
|
|
106
|
-
lines.append(f"[easy-coding:transition-error:{violation}]")
|
|
107
|
-
|
|
108
|
-
return lines
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def build_status_context(root: Path, session: dict, agent: str | None = None) -> str:
|
|
112
|
-
return "\n".join([build_status_line(root, session, agent), *build_machine_breadcrumbs(root, session, agent)])
|
|
7
|
+
__all__ = ["build_machine_breadcrumbs", "build_status_context", "build_status_line"]
|