easy-coding-harness 0.9.0 → 0.9.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,25 @@
|
|
|
6
6
|
- `y`:常规功能升级
|
|
7
7
|
- `z`:日常 bug 修复
|
|
8
8
|
|
|
9
|
+
## 0.9.1
|
|
10
|
+
|
|
11
|
+
- 正式发布 Codex Agent 身份归一化修复:协作路径 `/root`、`/root/...` 与平台身份
|
|
12
|
+
`codex` 视为同一所有者,不再错误展示 `Handoff -> /root`。
|
|
13
|
+
- 状态写入统一保存规范化身份,同时兼容存量任务数据;Claude Code、Codex、Qoder
|
|
14
|
+
之间的真实跨平台交接语义保持不变。
|
|
15
|
+
- 汇总 `0.9.1-beta.0` 的状态行、机器 breadcrumb、任务列表、claim 判定、回归测试与
|
|
16
|
+
架构文档验证结果。
|
|
17
|
+
|
|
18
|
+
## 0.9.1-beta.0
|
|
19
|
+
|
|
20
|
+
- 修复 Codex 根代理把协作路径 `/root` 写入 `task.json.last_agent` 后,hook 运行时身份
|
|
21
|
+
`codex` 因字符串不一致而错误展示 `Handoff -> /root` 的问题。
|
|
22
|
+
- 状态 API 统一规范化 Agent 身份,并在状态行、机器 breadcrumb、任务列表与 claim
|
|
23
|
+
判定中兼容存量 `/root`、`/root/...` 所有者;真实 Claude Code、Codex、Qoder 跨平台
|
|
24
|
+
交接语义保持不变,存量任务无需迁移或手工改写。
|
|
25
|
+
- 新增 Codex 根代理写入归一化、存量状态展示和真实跨平台 takeover 回归测试,同步架构
|
|
26
|
+
说明与介绍页版本。
|
|
27
|
+
|
|
9
28
|
## 0.9.0
|
|
10
29
|
|
|
11
30
|
- 正式发布相互独立的 `approval_mode` 与 `workflow_mode`:审批等待不再与执行深度
|
package/package.json
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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, {},
|
|
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,
|
|
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
|
-
|
|
3375
|
+
agent,
|
|
3352
3376
|
not args.no_set_current,
|
|
3353
3377
|
session_file,
|
|
3354
3378
|
),
|
|
3355
|
-
|
|
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,
|
|
3364
|
-
|
|
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,
|
|
3373
|
-
|
|
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,
|
|
3382
|
-
|
|
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,
|
|
3391
|
-
|
|
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,
|
|
3400
|
-
|
|
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,
|
|
3409
|
-
|
|
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,
|
|
3418
|
-
|
|
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,
|
|
3427
|
-
|
|
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
|
-
|
|
3466
|
+
agent,
|
|
3443
3467
|
args.task_id,
|
|
3444
3468
|
session_file,
|
|
3445
3469
|
),
|
|
3446
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3497
|
+
agent,
|
|
3474
3498
|
args.task_id,
|
|
3475
3499
|
session_file,
|
|
3476
3500
|
),
|
|
3477
|
-
|
|
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
|
-
|
|
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,
|
|
3501
|
-
|
|
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,
|
|
3510
|
-
|
|
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,
|
|
3519
|
-
|
|
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,
|
|
3528
|
-
|
|
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
|
-
|
|
3563
|
+
agent,
|
|
3540
3564
|
args.task_id,
|
|
3541
3565
|
session_file,
|
|
3542
3566
|
args.reason,
|
|
3543
3567
|
),
|
|
3544
|
-
|
|
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,
|
|
3553
|
-
|
|
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,
|
|
3562
|
-
|
|
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,
|
|
3571
|
-
|
|
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
|
-
|
|
3608
|
+
agent,
|
|
3585
3609
|
args.task_id,
|
|
3586
3610
|
session_file,
|
|
3587
3611
|
),
|
|
3588
|
-
|
|
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
|
-
|
|
3632
|
+
agent,
|
|
3609
3633
|
args.task_id,
|
|
3610
3634
|
session_file,
|
|
3611
3635
|
),
|
|
3612
|
-
|
|
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,
|
|
3621
|
-
|
|
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,
|
|
3630
|
-
|
|
3653
|
+
project_init_complete(root, agent),
|
|
3654
|
+
agent,
|
|
3631
3655
|
session_file,
|
|
3632
3656
|
)
|
|
3633
3657
|
)
|