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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-coding-harness",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "CLI scaffold for installing Easy Coding harness files into agent-native directories.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
- Tell the user initialization is done and daily work goes through `{{skill_trigger}}ec-workflow`.
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
- then hand control to ec-memory to write the short memory entry.
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
- then hand control to ec-memory to distill long memory or perform its no-op gate.
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. Then output the task summary
87
- (what was done, files changed, key decisions).
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. Read task B's `task.json` to determine its current stage.
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
- create_task(
722
+ attach_status_context(
582
723
  root,
583
- args.task_id,
584
- args.type,
585
- args.title,
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(set_current_task(root, args.task_id, args.agent, session_file))
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(clear_current_task(root, args.agent, session_file))
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(transition_task(root, args.stage, args.agent, args.task_id, session_file))
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(close_current_task(root, args.reason, args.agent, session_file))
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(project_init_complete(root, args.agent))
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(set_repo_path(root, args.repo, args.path, args.task_id, session_file))
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
- HELP_SUFFIX,
5
- get_pending_init_version,
6
- is_project_init_required,
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"]