easy-coding-harness 0.8.2-beta.0 → 0.8.3-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,18 @@
6
6
  - `y`:常规功能升级
7
7
  - `z`:日常 bug 修复
8
8
 
9
+ ## 0.8.3-beta.0
10
+
11
+ - 修复 Codex App 未在 hook payload 中提供 `session_id` 时仍回退到 PPID、导致同一 App 进程内多个逻辑会话共享 Easy Coding session 的问题;Codex 现在会在标准 hook session ID 缺失时使用 `CODEX_THREAD_ID`,仅在两者都不可用时保留 PPID 兼容回退。
12
+ - 修复 Qoder CLI 同时暴露 Claude 兼容环境变量时被识别为 Claude Code 的问题;共享 hook 统一按脚本平台目录优先、Qoder 专属环境次之、Claude 环境最后的顺序解析 Agent,避免各入口判断逻辑漂移。
13
+ - 新增 Codex App thread fallback、标准 payload 优先级、legacy PPID 和 Qoder/Claude 双环境变量回归测试,并同步设计文档与安装后 hook 验证。
14
+
15
+ ## 0.8.2
16
+
17
+ - 正式发布原生选择超时恢复:平台明确保证永久等待时禁用或省略 timeout / auto-resolution,无法保证时在调用原生选择前预先展示持久化文本编号。
18
+ - 原生选择超时、取消或返回无效结果后保留 `pending_transition` 且不重试;用户稍后可直接回复普通门 `1/2/3` 或特殊 IMPLEMENT 门 `1/2/3/4` 继续流程。
19
+ - 汇总 `0.8.2-beta.0` 的 workflow、analysis、Claude/Codex/Qoder 主约束、文档、安装回归和 npm 包内容验证结果。
20
+
9
21
  ## 0.8.2-beta.0
10
22
 
11
23
  - 原生选择工具明确保证永久等待时,确认门禁用或省略 timeout / auto-resolution;有限超时不再视为“永久等待”。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-coding-harness",
3
- "version": "0.8.2-beta.0",
3
+ "version": "0.8.3-beta.0",
4
4
  "description": "CLI scaffold for installing Easy Coding harness files into agent-native directories.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -142,17 +142,18 @@ def normalize_session_agent(agent: str | None) -> str:
142
142
 
143
143
 
144
144
  def detect_runtime_agent() -> str:
145
- if os.environ.get("CLAUDE_PROJECT_DIR"):
146
- return "claude-code"
147
- if os.environ.get("QODER_PROJECT_DIR"):
148
- return "qoder"
149
145
  script_path = Path(sys.argv[0]).as_posix()
150
- if ".claude/" in script_path:
151
- return "claude-code"
146
+ if ".qoder/" in script_path or ".qodercn/" in script_path:
147
+ return "qoder"
152
148
  if ".codex/" in script_path:
153
149
  return "codex"
154
- if ".qoder/" in script_path or ".qodercn/" in script_path:
150
+ if ".claude/" in script_path:
151
+ return "claude-code"
152
+ # Qoder CLI 会暴露 Claude 兼容环境变量,专属信号必须优先于兼容信号。
153
+ if os.environ.get("QODER_PROJECT_DIR"):
155
154
  return "qoder"
155
+ if os.environ.get("CLAUDE_PROJECT_DIR"):
156
+ return "claude-code"
156
157
  return "unknown"
157
158
 
158
159
 
@@ -167,7 +168,7 @@ def normalize_session_component(value: str) -> str:
167
168
  return f"sha256-{digest}"
168
169
 
169
170
 
170
- # 逻辑会话由 Agent 命名空间与 hook session_id 共同标识;PPID 只用于缺少逻辑 ID 的兼容回退。
171
+ # 逻辑会话由 Agent 命名空间与平台会话 ID 共同标识;PPID 只用于缺少逻辑 ID 的兼容回退。
171
172
  def hook_session_identity(
172
173
  payload: dict,
173
174
  agent: str | None,
@@ -176,9 +177,18 @@ def hook_session_identity(
176
177
  namespace = normalize_session_agent(agent)
177
178
  raw_session_id = payload.get("session_id") or payload.get("sessionId")
178
179
  external_session_id = str(raw_session_id).strip() if raw_session_id is not None else ""
180
+ source = "hook-session-id"
181
+ if not external_session_id and namespace == "codex":
182
+ # Codex App 当前会把 thread ID 暴露在进程环境中;标准 hook session_id 仍保持最高优先级。
183
+ raw_thread_id = (
184
+ payload.get("thread_id")
185
+ or payload.get("threadId")
186
+ or os.environ.get("CODEX_THREAD_ID")
187
+ )
188
+ external_session_id = str(raw_thread_id).strip() if raw_thread_id is not None else ""
189
+ source = "codex-thread-id"
179
190
  if external_session_id:
180
191
  component = normalize_session_component(external_session_id)
181
- source = "hook-session-id"
182
192
  else:
183
193
  component = f"ppid-{ppid if ppid is not None else os.getppid()}"
184
194
  source = "legacy-ppid"
@@ -637,7 +647,8 @@ def resolve_session_path(root: Path, session_file: str | Path | None = None) ->
637
647
  f"{session_file}. Must be a file under .easy-coding/sessions/."
638
648
  )
639
649
  return resolved
640
- return sessions_dir / f"{detect_runtime_agent()}-ppid-{os.getppid()}.json"
650
+ identity = hook_session_identity({}, detect_runtime_agent())
651
+ return sessions_dir / f"{identity['session_key']}.json"
641
652
 
642
653
 
643
654
  def resolve_hook_session_path(
@@ -4,7 +4,7 @@ import os
4
4
  from pathlib import Path
5
5
  import sys
6
6
 
7
- from easy_coding_state import ensure_hook_session, snapshot_state
7
+ from easy_coding_state import detect_runtime_agent, ensure_hook_session, snapshot_state
8
8
 
9
9
 
10
10
  def configure_stdio() -> None:
@@ -30,21 +30,6 @@ def find_ec_root(start: Path) -> Path | None:
30
30
  current = current.parent
31
31
 
32
32
 
33
- def detect_agent() -> str:
34
- if os.environ.get("CLAUDE_PROJECT_DIR"):
35
- return "claude-code"
36
- if os.environ.get("QODER_PROJECT_DIR"):
37
- return "qoder"
38
- hook_path = Path(sys.argv[0]).as_posix()
39
- if ".claude/" in hook_path:
40
- return "claude-code"
41
- if ".codex/" in hook_path:
42
- return "codex"
43
- if ".qoder/" in hook_path or ".qodercn/" in hook_path:
44
- return "qoder"
45
- return "unknown"
46
-
47
-
48
33
  def emit(event_name: str, context: str) -> None:
49
34
  print(
50
35
  json.dumps(
@@ -69,7 +54,7 @@ def main() -> int:
69
54
  if root is None:
70
55
  return 0
71
56
 
72
- agent = detect_agent()
57
+ agent = detect_runtime_agent()
73
58
  session, session_path = ensure_hook_session(root, payload, agent)
74
59
  if session and session.get("harness_disabled") is True:
75
60
  return 0
@@ -4,7 +4,7 @@ import os
4
4
  from pathlib import Path
5
5
  import sys
6
6
 
7
- from easy_coding_state import ensure_hook_session
7
+ from easy_coding_state import detect_runtime_agent, ensure_hook_session
8
8
  from easy_coding_status import build_status_context
9
9
 
10
10
 
@@ -31,21 +31,6 @@ def find_ec_root(start: Path) -> Path | None:
31
31
  current = current.parent
32
32
 
33
33
 
34
- def detect_agent() -> str:
35
- if os.environ.get("CLAUDE_PROJECT_DIR"):
36
- return "claude-code"
37
- if os.environ.get("QODER_PROJECT_DIR"):
38
- return "qoder"
39
- hook_path = Path(sys.argv[0]).as_posix()
40
- if ".claude/" in hook_path:
41
- return "claude-code"
42
- if ".codex/" in hook_path:
43
- return "codex"
44
- if ".qoder/" in hook_path or ".qodercn/" in hook_path:
45
- return "qoder"
46
- return "unknown"
47
-
48
-
49
34
  def emit(event_name: str, context: str) -> None:
50
35
  print(
51
36
  json.dumps(
@@ -71,7 +56,7 @@ def main() -> int:
71
56
  return 0
72
57
 
73
58
  event_name = payload.get("hook_event_name") or payload.get("hookEventName") or "UserPromptSubmit"
74
- agent = detect_agent()
59
+ agent = detect_runtime_agent()
75
60
  session, session_path = ensure_hook_session(root, payload, agent)
76
61
  emit(event_name, build_status_context(root, session, agent, session_path))
77
62
  return 0
@@ -4,7 +4,7 @@ import os
4
4
  from pathlib import Path
5
5
  import sys
6
6
 
7
- from easy_coding_state import ensure_hook_session
7
+ from easy_coding_state import detect_runtime_agent, ensure_hook_session
8
8
  from easy_coding_status import build_status_context
9
9
 
10
10
 
@@ -31,21 +31,6 @@ def find_ec_root(start: Path) -> Path | None:
31
31
  current = current.parent
32
32
 
33
33
 
34
- def detect_agent() -> str:
35
- if os.environ.get("CLAUDE_PROJECT_DIR"):
36
- return "claude-code"
37
- if os.environ.get("QODER_PROJECT_DIR"):
38
- return "qoder"
39
- hook_path = Path(sys.argv[0]).as_posix()
40
- if ".claude/" in hook_path:
41
- return "claude-code"
42
- if ".codex/" in hook_path:
43
- return "codex"
44
- if ".qoder/" in hook_path or ".qodercn/" in hook_path:
45
- return "qoder"
46
- return "unknown"
47
-
48
-
49
34
  def emit(event_name: str, context: str) -> None:
50
35
  print(
51
36
  json.dumps(
@@ -70,7 +55,7 @@ def main() -> int:
70
55
  if root is None:
71
56
  return 0
72
57
 
73
- agent = detect_agent()
58
+ agent = detect_runtime_agent()
74
59
  event_name = payload.get("hook_event_name") or payload.get("hookEventName") or "SessionStart"
75
60
  session, session_path = ensure_hook_session(root, payload, agent)
76
61
  emit(event_name, build_status_context(root, session, agent, session_path))