easy-coding-harness 0.9.0-beta.0 → 0.9.1-beta.0

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,28 @@
6
6
  - `y`:常规功能升级
7
7
  - `z`:日常 bug 修复
8
8
 
9
+ ## 0.9.1-beta.0
10
+
11
+ - 修复 Codex 根代理把协作路径 `/root` 写入 `task.json.last_agent` 后,hook 运行时身份
12
+ `codex` 因字符串不一致而错误展示 `Handoff -> /root` 的问题。
13
+ - 状态 API 统一规范化 Agent 身份,并在状态行、机器 breadcrumb、任务列表与 claim
14
+ 判定中兼容存量 `/root`、`/root/...` 所有者;真实 Claude Code、Codex、Qoder 跨平台
15
+ 交接语义保持不变,存量任务无需迁移或手工改写。
16
+ - 新增 Codex 根代理写入归一化、存量状态展示和真实跨平台 takeover 回归测试,同步架构
17
+ 说明与介绍页版本。
18
+
19
+ ## 0.9.0
20
+
21
+ - 正式发布相互独立的 `approval_mode` 与 `workflow_mode`:审批等待不再与执行深度
22
+ 耦合,新增仅在 ANALYSIS → IMPLEMENT 确认一次的 `confirm` 审批模式,并默认使用
23
+ `adaptive` 在分析结束时自动选择、展示和冻结工作流模式。
24
+ - 保留完整代码任务状态链,所有新代码任务均进入 REVIEW;通过 `fast`、`standard`、
25
+ `strict` 调整各状态内部的上下文加载、审查独立性、验证范围与记忆深度,在减少
26
+ Token、时间和重复返工的同时维持机械质量下限。
27
+ - 正式启用实现与配置指纹绑定的 REVIEW/VERIFICATION 证据、语义单元返工与连续失败
28
+ 重分析机制;配置 Schema 升至 3,并完整兼容旧 `lite`、approve、guard、auto 及
29
+ 0.9 之前在途任务的迁移语义。
30
+
9
31
  ## 0.9.0-beta.0
10
32
 
11
33
  - 将单一 `behavior.confirm_mode` 拆分为 `behavior.approval_mode` 与
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-coding-harness",
3
- "version": "0.9.0-beta.0",
3
+ "version": "0.9.1-beta.0",
4
4
  "description": "CLI scaffold for installing Easy Coding harness files into agent-native directories.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -93,6 +93,9 @@ demand.
93
93
  continues. All platform-agnostic artifacts (dev-spec, execution.jsonl, task.json, memory)
94
94
  make cross-agent handoff lossless. `task.json.last_agent` records the last owner so a new
95
95
  agent knows a task was handed off rather than self-interrupted.
96
+ Owner identities use platform namespaces. Codex collaboration paths such as `/root` and
97
+ `/root/...` normalize to `codex`, so internal Codex delegation is not mistaken for a cross-agent
98
+ handoff; existing task files with those paths remain compatible without migration.
96
99
 
97
100
  Handoff is target-less. The leaving agent writes a `handoff` record with `from`, `stage`,
98
101
  `summary`, and `timestamp`, then releases its session pointer. It does not know or record the
@@ -87,6 +87,7 @@ DEFAULT_SHORT_TERM_KEEP = 5
87
87
  SESSION_STALE_THRESHOLD_HOURS = 30 * 24
88
88
  SESSION_COMPONENT_PATTERN = re.compile(r"^[A-Za-z0-9._-]+$")
89
89
  SESSION_AGENT_NAMESPACES = {"claude-code", "codex", "qoder", "unknown"}
90
+ CODEX_AGENT_PATH_PATTERN = re.compile(r"^/root(?:/[a-z0-9._-]+)*$")
90
91
  LEGACY_STATE_LOCK_TIMEOUT_SECONDS = 5.0
91
92
  LEGACY_STATE_LOCK_STALE_SECONDS = 60.0
92
93
  LEGACY_STATE_LOCK_POLL_SECONDS = 0.02
@@ -153,11 +154,26 @@ def short_memory_id_sort_key(memory_id: str) -> tuple[int, str]:
153
154
  return (2, memory_id)
154
155
 
155
156
 
157
+ def normalize_agent_identity(agent: str | None) -> str:
158
+ raw_agent = str(agent or "unknown").strip()
159
+ normalized = raw_agent.lower()
160
+ # Codex 协作树中的 /root 路径表示 Codex 内部执行者,不是一个新的跨平台 Agent。
161
+ if CODEX_AGENT_PATH_PATTERN.fullmatch(normalized):
162
+ return "codex"
163
+ if normalized in SESSION_AGENT_NAMESPACES:
164
+ return normalized
165
+ return raw_agent
166
+
167
+
156
168
  def normalize_session_agent(agent: str | None) -> str:
157
- normalized = str(agent or "unknown").strip().lower()
169
+ normalized = normalize_agent_identity(agent)
158
170
  return normalized if normalized in SESSION_AGENT_NAMESPACES else "unknown"
159
171
 
160
172
 
173
+ def agents_equivalent(first: str | None, second: str | None) -> bool:
174
+ return normalize_agent_identity(first) == normalize_agent_identity(second)
175
+
176
+
161
177
  def detect_runtime_agent() -> str:
162
178
  script_path = Path(sys.argv[0]).as_posix()
163
179
  if ".qoder/" in script_path or ".qodercn/" in script_path:
@@ -2046,7 +2062,7 @@ def build_status_line(
2046
2062
  status = str(state["status"])
2047
2063
  line = f"{status_brand} · `{task_id}` · `{status}`"
2048
2064
  last_agent = state.get("last_agent")
2049
- if agent and last_agent and last_agent != agent:
2065
+ if agent and last_agent and not agents_equivalent(last_agent, agent):
2050
2066
  line += f" · Handoff -> `{last_agent}`"
2051
2067
  if state["is_terminal"] or state["task_missing"]:
2052
2068
  line += f" · {HELP_SUFFIX}"
@@ -2090,7 +2106,7 @@ def build_machine_breadcrumbs(
2090
2106
  if state["task_missing"]:
2091
2107
  lines.append(f"[easy-coding:current-task-missing:{task_id}]")
2092
2108
  last_agent = state.get("last_agent")
2093
- if agent and last_agent and last_agent != agent:
2109
+ if agent and last_agent and not agents_equivalent(last_agent, agent):
2094
2110
  lines.append(f"[easy-coding:handoff-from:{last_agent}]")
2095
2111
  pending = state.get("pending_transition")
2096
2112
  if isinstance(pending, dict):
@@ -2200,7 +2216,7 @@ def task_claim_action(task: dict, agent: str | None) -> str | None:
2200
2216
  if status in TERMINAL_STATUSES or not agent:
2201
2217
  return None
2202
2218
  last_agent = task.get("last_agent")
2203
- return "continue" if not last_agent or last_agent == agent else "takeover"
2219
+ return "continue" if not last_agent or agents_equivalent(last_agent, agent) else "takeover"
2204
2220
 
2205
2221
 
2206
2222
  def list_tasks(root: Path, agent: str | None = None) -> list[dict]:
@@ -2459,7 +2475,11 @@ def claim_task(root: Path, task_id: str, agent: str, session_file: str | Path |
2459
2475
  raise StateError(f"Cannot claim terminal task: {task_id}")
2460
2476
 
2461
2477
  previous_agent = task.get("last_agent")
2462
- action = "continue" if not previous_agent or previous_agent == agent else "takeover"
2478
+ action = (
2479
+ "continue"
2480
+ if not previous_agent or agents_equivalent(previous_agent, agent)
2481
+ else "takeover"
2482
+ )
2463
2483
  latest_handoff = latest_handoff_record(root, task_id)
2464
2484
  task["last_agent"] = agent
2465
2485
  write_task(root, task_id, task)
@@ -3324,21 +3344,25 @@ def main() -> int:
3324
3344
  root = resolve_root(getattr(args, "cwd", None))
3325
3345
  session_file = getattr(args, "session_file", None)
3326
3346
  command = args.command or "snapshot"
3327
- agent = normalize_session_agent(getattr(args, "agent", None) or detect_runtime_agent())
3347
+ agent = normalize_agent_identity(
3348
+ getattr(args, "agent", None) or detect_runtime_agent()
3349
+ )
3350
+ session_agent = normalize_session_agent(agent)
3351
+ visible_agent = None if agent == "unknown" else agent
3328
3352
  if session_file is None and command == "project-init-complete":
3329
3353
  raise StateError(
3330
3354
  "project-init-complete requires --session-file from the current hook context."
3331
3355
  )
3332
3356
  if session_file is None and command not in {"list-tasks", "memory-new-id"}:
3333
- if agent == "unknown":
3357
+ if session_agent == "unknown":
3334
3358
  raise StateError(
3335
3359
  "Cannot resolve the logical session. Pass --session-file or --agent."
3336
3360
  )
3337
- _, session_file = ensure_hook_session(root, {}, agent)
3361
+ _, session_file = ensure_hook_session(root, {}, session_agent)
3338
3362
  if command == "snapshot":
3339
3363
  emit(snapshot_state(root, session_file))
3340
3364
  elif command == "list-tasks":
3341
- emit({"tasks": list_tasks(root, getattr(args, "agent", None))})
3365
+ emit({"tasks": list_tasks(root, visible_agent)})
3342
3366
  elif command == "create-task":
3343
3367
  emit(
3344
3368
  attach_status_context(
@@ -3348,11 +3372,11 @@ def main() -> int:
3348
3372
  args.task_id,
3349
3373
  args.type,
3350
3374
  args.title,
3351
- args.agent,
3375
+ agent,
3352
3376
  not args.no_set_current,
3353
3377
  session_file,
3354
3378
  ),
3355
- args.agent,
3379
+ agent,
3356
3380
  session_file,
3357
3381
  )
3358
3382
  )
@@ -3360,8 +3384,8 @@ def main() -> int:
3360
3384
  emit(
3361
3385
  attach_status_context(
3362
3386
  root,
3363
- set_current_task(root, args.task_id, args.agent, session_file),
3364
- args.agent,
3387
+ set_current_task(root, args.task_id, agent, session_file),
3388
+ agent,
3365
3389
  session_file,
3366
3390
  )
3367
3391
  )
@@ -3369,8 +3393,8 @@ def main() -> int:
3369
3393
  emit(
3370
3394
  attach_status_context(
3371
3395
  root,
3372
- clear_current_task(root, args.agent, session_file),
3373
- args.agent,
3396
+ clear_current_task(root, agent, session_file),
3397
+ agent,
3374
3398
  session_file,
3375
3399
  )
3376
3400
  )
@@ -3378,8 +3402,8 @@ def main() -> int:
3378
3402
  emit(
3379
3403
  attach_status_context(
3380
3404
  root,
3381
- set_session_approval_mode(root, args.mode, args.agent, session_file),
3382
- args.agent,
3405
+ set_session_approval_mode(root, args.mode, agent, session_file),
3406
+ agent,
3383
3407
  session_file,
3384
3408
  )
3385
3409
  )
@@ -3387,8 +3411,8 @@ def main() -> int:
3387
3411
  emit(
3388
3412
  attach_status_context(
3389
3413
  root,
3390
- set_session_legacy_confirm_mode(root, args.mode, args.agent, session_file),
3391
- args.agent,
3414
+ set_session_legacy_confirm_mode(root, args.mode, agent, session_file),
3415
+ agent,
3392
3416
  session_file,
3393
3417
  )
3394
3418
  )
@@ -3396,8 +3420,8 @@ def main() -> int:
3396
3420
  emit(
3397
3421
  attach_status_context(
3398
3422
  root,
3399
- clear_session_approval_mode(root, args.agent, session_file),
3400
- args.agent,
3423
+ clear_session_approval_mode(root, agent, session_file),
3424
+ agent,
3401
3425
  session_file,
3402
3426
  )
3403
3427
  )
@@ -3405,8 +3429,8 @@ def main() -> int:
3405
3429
  emit(
3406
3430
  attach_status_context(
3407
3431
  root,
3408
- clear_session_legacy_confirm_mode(root, args.agent, session_file),
3409
- args.agent,
3432
+ clear_session_legacy_confirm_mode(root, agent, session_file),
3433
+ agent,
3410
3434
  session_file,
3411
3435
  )
3412
3436
  )
@@ -3414,8 +3438,8 @@ def main() -> int:
3414
3438
  emit(
3415
3439
  attach_status_context(
3416
3440
  root,
3417
- set_session_workflow_mode(root, args.mode, args.agent, session_file),
3418
- args.agent,
3441
+ set_session_workflow_mode(root, args.mode, agent, session_file),
3442
+ agent,
3419
3443
  session_file,
3420
3444
  )
3421
3445
  )
@@ -3423,8 +3447,8 @@ def main() -> int:
3423
3447
  emit(
3424
3448
  attach_status_context(
3425
3449
  root,
3426
- clear_session_workflow_mode(root, args.agent, session_file),
3427
- args.agent,
3450
+ clear_session_workflow_mode(root, agent, session_file),
3451
+ agent,
3428
3452
  session_file,
3429
3453
  )
3430
3454
  )
@@ -3439,11 +3463,11 @@ def main() -> int:
3439
3463
  args.minimum,
3440
3464
  args.source,
3441
3465
  args.reason,
3442
- args.agent,
3466
+ agent,
3443
3467
  args.task_id,
3444
3468
  session_file,
3445
3469
  ),
3446
- args.agent,
3470
+ agent,
3447
3471
  session_file,
3448
3472
  )
3449
3473
  )
@@ -3458,7 +3482,7 @@ def main() -> int:
3458
3482
  "minimum_mode": minimum_mode,
3459
3483
  "reasons": reasons,
3460
3484
  },
3461
- args.agent,
3485
+ visible_agent,
3462
3486
  session_file,
3463
3487
  )
3464
3488
  )
@@ -3470,11 +3494,11 @@ def main() -> int:
3470
3494
  root,
3471
3495
  args.mode,
3472
3496
  args.reason,
3473
- args.agent,
3497
+ agent,
3474
3498
  args.task_id,
3475
3499
  session_file,
3476
3500
  ),
3477
- args.agent,
3501
+ agent,
3478
3502
  session_file,
3479
3503
  )
3480
3504
  )
@@ -3489,7 +3513,7 @@ def main() -> int:
3489
3513
  "task_id": resolved_task_id,
3490
3514
  **evidence_fingerprints(root, resolved_task_id),
3491
3515
  },
3492
- args.agent,
3516
+ visible_agent,
3493
3517
  session_file,
3494
3518
  )
3495
3519
  )
@@ -3497,8 +3521,8 @@ def main() -> int:
3497
3521
  emit(
3498
3522
  attach_status_context(
3499
3523
  root,
3500
- set_harness_disabled(root, True, args.agent, session_file),
3501
- args.agent,
3524
+ set_harness_disabled(root, True, agent, session_file),
3525
+ agent,
3502
3526
  session_file,
3503
3527
  )
3504
3528
  )
@@ -3506,8 +3530,8 @@ def main() -> int:
3506
3530
  emit(
3507
3531
  attach_status_context(
3508
3532
  root,
3509
- set_harness_disabled(root, False, args.agent, session_file),
3510
- args.agent,
3533
+ set_harness_disabled(root, False, agent, session_file),
3534
+ agent,
3511
3535
  session_file,
3512
3536
  )
3513
3537
  )
@@ -3515,8 +3539,8 @@ def main() -> int:
3515
3539
  emit(
3516
3540
  attach_status_context(
3517
3541
  root,
3518
- handoff_task(root, args.agent, args.summary, args.task_id, session_file),
3519
- args.agent,
3542
+ handoff_task(root, agent, args.summary, args.task_id, session_file),
3543
+ agent,
3520
3544
  session_file,
3521
3545
  )
3522
3546
  )
@@ -3524,8 +3548,8 @@ def main() -> int:
3524
3548
  emit(
3525
3549
  attach_status_context(
3526
3550
  root,
3527
- claim_task(root, args.task_id, args.agent, session_file),
3528
- args.agent,
3551
+ claim_task(root, args.task_id, agent, session_file),
3552
+ agent,
3529
3553
  session_file,
3530
3554
  )
3531
3555
  )
@@ -3536,12 +3560,12 @@ def main() -> int:
3536
3560
  request_transition(
3537
3561
  root,
3538
3562
  args.stage,
3539
- args.agent,
3563
+ agent,
3540
3564
  args.task_id,
3541
3565
  session_file,
3542
3566
  args.reason,
3543
3567
  ),
3544
- args.agent,
3568
+ agent,
3545
3569
  session_file,
3546
3570
  )
3547
3571
  )
@@ -3549,8 +3573,8 @@ def main() -> int:
3549
3573
  emit(
3550
3574
  attach_status_context(
3551
3575
  root,
3552
- confirm_transition(root, args.agent, args.stage, args.task_id, session_file),
3553
- args.agent,
3576
+ confirm_transition(root, agent, args.stage, args.task_id, session_file),
3577
+ agent,
3554
3578
  session_file,
3555
3579
  )
3556
3580
  )
@@ -3558,8 +3582,8 @@ def main() -> int:
3558
3582
  emit(
3559
3583
  attach_status_context(
3560
3584
  root,
3561
- auto_transition(root, args.stage, args.agent, args.task_id, session_file),
3562
- args.agent,
3585
+ auto_transition(root, args.stage, agent, args.task_id, session_file),
3586
+ agent,
3563
3587
  session_file,
3564
3588
  )
3565
3589
  )
@@ -3567,8 +3591,8 @@ def main() -> int:
3567
3591
  emit(
3568
3592
  attach_status_context(
3569
3593
  root,
3570
- cancel_transition(root, args.agent, args.task_id, session_file),
3571
- args.agent,
3594
+ cancel_transition(root, agent, args.task_id, session_file),
3595
+ agent,
3572
3596
  session_file,
3573
3597
  )
3574
3598
  )
@@ -3581,11 +3605,11 @@ def main() -> int:
3581
3605
  memory_short_complete(
3582
3606
  root,
3583
3607
  args.file,
3584
- args.agent,
3608
+ agent,
3585
3609
  args.task_id,
3586
3610
  session_file,
3587
3611
  ),
3588
- args.agent,
3612
+ agent,
3589
3613
  session_file,
3590
3614
  )
3591
3615
  )
@@ -3605,11 +3629,11 @@ def main() -> int:
3605
3629
  memory_complete(
3606
3630
  root,
3607
3631
  args.action,
3608
- args.agent,
3632
+ agent,
3609
3633
  args.task_id,
3610
3634
  session_file,
3611
3635
  ),
3612
- args.agent,
3636
+ agent,
3613
3637
  session_file,
3614
3638
  )
3615
3639
  )
@@ -3617,8 +3641,8 @@ def main() -> int:
3617
3641
  emit(
3618
3642
  attach_status_context(
3619
3643
  root,
3620
- close_current_task(root, args.reason, args.agent, session_file),
3621
- args.agent,
3644
+ close_current_task(root, args.reason, agent, session_file),
3645
+ agent,
3622
3646
  session_file,
3623
3647
  )
3624
3648
  )
@@ -3626,8 +3650,8 @@ def main() -> int:
3626
3650
  emit(
3627
3651
  attach_status_context(
3628
3652
  root,
3629
- project_init_complete(root, args.agent),
3630
- args.agent,
3653
+ project_init_complete(root, agent),
3654
+ agent,
3631
3655
  session_file,
3632
3656
  )
3633
3657
  )