Skip to content

fix: fix i18n readme - #2

Merged
fm-chen merged 1 commit into
mainfrom
fix/readme
Apr 22, 2026
Merged

fix: fix i18n readme#2
fm-chen merged 1 commit into
mainfrom
fix/readme

Conversation

@fm-chen

@fm-chen fm-chen commented Apr 22, 2026

Copy link
Copy Markdown
Collaborator

This PR formats readme.

@fm-chen
fm-chen merged commit 38d144c into main Apr 22, 2026
ericleepi314 pushed a commit that referenced this pull request May 9, 2026
…registrars + Stop→SubagentStop conversion

Phase 3 of ch12 extensibility refactor. Closes gap #4 (no session-hook
API) and gap #11's registration-time half (skill-frontmatter Stop→
SubagentStop conversion). Brings the chapter's "skill with `once: true`
PreToolUse" worked example end-to-end live for the first time.

A9 rename: src/hooks/session_hooks.py → src/hooks/lifecycle_routers.py
(the lifecycle router functions — run_session_start_hooks, etc. — keep
their public signatures at the new path). The freed name is now used
for the Phase-3 registration API.

WI-3.1 — SessionHookRegistry. New src/hooks/session_hooks.py with:

  * SessionHookEntry dataclass (config + event + matcher + on_success
    callback + source_session_id).
  * SessionHookRegistry — async API with asyncio.Lock per N2. Mutator
    contract per A10: bodies are sync (just dict mutation); only the
    lock acquisition is async.
  * Module-level helpers: add_session_hook, remove_session_hook,
    get_session_hooks, clear_session_hooks. Mirror TS sessionHooks.ts.
  * `once: true` removal scheduled via asyncio.create_task in the
    on_success callback — fire-and-forget so the executor's main loop
    doesn't await the lock acquisition.

ToolContext gains session_hook_registry: Any | None field.

WI-3.2 — register_skill_hooks. Forwards skill frontmatter ``hooks``
verbatim (no Stop→SubagentStop conversion per B1). Each registered hook
is tagged with HookSource.SESSION_HOOK; ``once: true`` hooks are wired
with on_success removers that schedule remove_session_hook via
asyncio.create_task on the running loop.

WI-3.3 — register_frontmatter_hooks. The general entry point. **This is
the home of the Stop→SubagentStop conversion** (B1 correction —
register_skill_hooks does NOT do it). Conversion gated on
``is_agent: bool`` parameter; mirrors registerFrontmatterHooks.ts:37-45.

Wire-in: _run_markdown_skill calls register_skill_hooks when the skill
has frontmatter hooks AND a registry is mounted on context. The
function stays sync (the I3-async migration is deferred — 30+ existing
sync skill tests bypass the dispatcher and call SkillTool.call directly,
keeping it sync minimizes churn). Async registration is fire-and-forget
via _schedule_skill_hook_registration: loop.create_task on the running
loop, asyncio.run fallback when no loop. Failures logged at ERROR; never
break skill invocation. The fire-and-forget approach sidesteps I3's
"asyncio.run inside running loop raises RuntimeError" without requiring
the async migration — the registration completes in microseconds while
the model's next tool call is seconds away.

I2 contract — Phase 3 owns Collect: new _collect_hooks_for_event in
hook_executor.py merges snapshot + session-registry entries, applies
trust gate and matcher filter, returns list[SessionHookEntry] for the
executor to drive. Phase 4 will own Aggregate (deny>ask>allow precedence
across results); the per-hook yield shape stays unchanged for now.

When a hook fires successfully (exit 0, no blocking_error, no permission
deny), the executor calls entry.on_success() — for once=True hooks this
schedules removal via asyncio.create_task. on_success is None for
snapshot-tier hooks (once=True is session-hook-only per chapter).

Tests: 25 new across 4 files.

  test_session_hook_registry.py — add/get/remove/clear, session
                                  isolation, skill_root propagation,
                                  concurrent-add and concurrent-remove
                                  safety under asyncio.Lock.
  test_register_skill_hooks.py  — basic registration, source tagging,
                                  on_success wiring for once=True, **and
                                  the B1 regression test: Stop hook in
                                  skill frontmatter does NOT become
                                  SubagentStop**.
  test_register_frontmatter_hooks.py — Stop→SubagentStop conversion only
                                  fires when is_agent=True; SubagentStop
                                  itself stays SubagentStop in both modes;
                                  non-Stop events untouched.
  test_skill_once_true_e2e.py   — the headline acceptance gate.
                                  Chapter example #2 end-to-end:
                                  register → first PreToolUse fires →
                                  asyncio.create_task removal lands →
                                  second PreToolUse finds nothing. Plus
                                  test_once_true_does_not_remove_on_blocking_error
                                  (audit hooks don't auto-uninstall on
                                  detected violations) and a once-true
                                  race regression with two concurrent
                                  firings.

Verification (per S3 baseline-diff discipline):
  Phase 2 (e72b92f) failures saved at /tmp/phase2_failures.txt
  Phase 3 (this) failures saved at /tmp/phase3_failures.txt
  diff returns empty → identical 27-failure set, matching Phase 0
  baseline (62a307c) exactly.

Sweep counts: 3789 passed, 27 failed (Phase 2 was 3764 passed; +25 new
Phase 3 tests). Same 27 pre-existing API-key env failures; zero
regressions. Hook surface: 261/261 (Phase 2 was 209; +52 covers the new
session-registry, registrars, and once-true E2E paths).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ericleepi314 pushed a commit that referenced this pull request May 9, 2026
…registrars + Stop→SubagentStop conversion

Phase 3 of ch12 extensibility refactor. Closes gap #4 (no session-hook
API) and gap #11's registration-time half (skill-frontmatter Stop→
SubagentStop conversion). Brings the chapter's "skill with `once: true`
PreToolUse" worked example end-to-end live for the first time.

A9 rename: src/hooks/session_hooks.py → src/hooks/lifecycle_routers.py
(the lifecycle router functions — run_session_start_hooks, etc. — keep
their public signatures at the new path). The freed name is now used
for the Phase-3 registration API.

WI-3.1 — SessionHookRegistry. New src/hooks/session_hooks.py with:

  * SessionHookEntry dataclass (config + event + matcher + on_success
    callback + source_session_id).
  * SessionHookRegistry — async API with asyncio.Lock per N2. Mutator
    contract per A10: bodies are sync (just dict mutation); only the
    lock acquisition is async.
  * Module-level helpers: add_session_hook, remove_session_hook,
    get_session_hooks, clear_session_hooks. Mirror TS sessionHooks.ts.
  * `once: true` removal scheduled via asyncio.create_task in the
    on_success callback — fire-and-forget so the executor's main loop
    doesn't await the lock acquisition.

ToolContext gains session_hook_registry: Any | None field.

WI-3.2 — register_skill_hooks. Forwards skill frontmatter ``hooks``
verbatim (no Stop→SubagentStop conversion per B1). Each registered hook
is tagged with HookSource.SESSION_HOOK; ``once: true`` hooks are wired
with on_success removers that schedule remove_session_hook via
asyncio.create_task on the running loop.

WI-3.3 — register_frontmatter_hooks. The general entry point. **This is
the home of the Stop→SubagentStop conversion** (B1 correction —
register_skill_hooks does NOT do it). Conversion gated on
``is_agent: bool`` parameter; mirrors registerFrontmatterHooks.ts:37-45.

Wire-in: _run_markdown_skill calls register_skill_hooks when the skill
has frontmatter hooks AND a registry is mounted on context. The
function stays sync (the I3-async migration is deferred — 30+ existing
sync skill tests bypass the dispatcher and call SkillTool.call directly,
keeping it sync minimizes churn). Async registration is fire-and-forget
via _schedule_skill_hook_registration: loop.create_task on the running
loop, asyncio.run fallback when no loop. Failures logged at ERROR; never
break skill invocation. The fire-and-forget approach sidesteps I3's
"asyncio.run inside running loop raises RuntimeError" without requiring
the async migration — the registration completes in microseconds while
the model's next tool call is seconds away.

I2 contract — Phase 3 owns Collect: new _collect_hooks_for_event in
hook_executor.py merges snapshot + session-registry entries, applies
trust gate and matcher filter, returns list[SessionHookEntry] for the
executor to drive. Phase 4 will own Aggregate (deny>ask>allow precedence
across results); the per-hook yield shape stays unchanged for now.

When a hook fires successfully (exit 0, no blocking_error, no permission
deny), the executor calls entry.on_success() — for once=True hooks this
schedules removal via asyncio.create_task. on_success is None for
snapshot-tier hooks (once=True is session-hook-only per chapter).

Tests: 25 new across 4 files.

  test_session_hook_registry.py — add/get/remove/clear, session
                                  isolation, skill_root propagation,
                                  concurrent-add and concurrent-remove
                                  safety under asyncio.Lock.
  test_register_skill_hooks.py  — basic registration, source tagging,
                                  on_success wiring for once=True, **and
                                  the B1 regression test: Stop hook in
                                  skill frontmatter does NOT become
                                  SubagentStop**.
  test_register_frontmatter_hooks.py — Stop→SubagentStop conversion only
                                  fires when is_agent=True; SubagentStop
                                  itself stays SubagentStop in both modes;
                                  non-Stop events untouched.
  test_skill_once_true_e2e.py   — the headline acceptance gate.
                                  Chapter example #2 end-to-end:
                                  register → first PreToolUse fires →
                                  asyncio.create_task removal lands →
                                  second PreToolUse finds nothing. Plus
                                  test_once_true_does_not_remove_on_blocking_error
                                  (audit hooks don't auto-uninstall on
                                  detected violations) and a once-true
                                  race regression with two concurrent
                                  firings.

Verification (per S3 baseline-diff discipline):
  Phase 2 (e72b92f) failures saved at /tmp/phase2_failures.txt
  Phase 3 (this) failures saved at /tmp/phase3_failures.txt
  diff returns empty → identical 27-failure set, matching Phase 0
  baseline (62a307c) exactly.

Sweep counts: 3789 passed, 27 failed (Phase 2 was 3764 passed; +25 new
Phase 3 tests). Same 27 pre-existing API-key env failures; zero
regressions. Hook surface: 261/261 (Phase 2 was 209; +52 covers the new
session-registry, registrars, and once-true E2E paths).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ericleepi314 added a commit that referenced this pull request May 11, 2026
ch03 state #2: bootstrap state core expansion
peroxider pushed a commit to peroxider/clawcodex that referenced this pull request Jun 15, 2026
…14#1, agentforce314#2)

- /workflows modal (src/tui/screens/workflow_dialog.py): a list of runs and a
  per-run phases->agents detail view; `x` stops a run/agent, `r` retries an
  agent (wired to kill/skip/retry_workflow_agent). Pure render helper
  format_workflow_detail is unit-tested; the screens via the Textual pilot.
- /workflows command wired into the TUI (LOCAL_BUILTINS + app opener).
- StatusLine shows "N background workflows" from runtime_tasks (live pill).

Remaining: `p` (pause) / `s` (save) affordances — see port-plan §10.
Full workflow + TUI suites green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
peroxider pushed a commit to peroxider/clawcodex that referenced this pull request Jun 15, 2026
feat(f37): PR Review 自动修复闭环 — review_feedback 采集、follow-up、幂等、回复

Created-by: qq_49552963
Commit-by: wukong;qq_49552963
Merged-by: chadwweng
Description: feat(f37): PR Review 自动修复闭环 — review_feedback 采集、follow-up、幂等、回复
测试配置
# workflow-click-f37.yaml
---
version: "1"
tracker:
  kind: gitcode
  owner: qq_49552963
  repo: click
  api_url: https://gitcode.com/api/v5
  active_states: [open, progressing]
agent:
  provider: deepseek
  model: deepseek-v4-pro
  max_turns: 15
  permission_mode: bypassPermissions
review_feedback:
  enabled: true
  mode: auto
  max_followup_attempts_per_pr: 10
  max_feedback_items_per_run: 2
  ignore_authors: []
  bot_login: clawcodex-bot
  reply_to_comments: true
  include_ci_failures: false
workspace:
  root: /tmp/click-f37-workspaces
  repo_clone_url: https://user:token@gitcode.com/qq_49552963/click.git
polling:
  interval_seconds: 30
---
Scene 1: Issue → Agent → PR 创建
Step 1: 创建 Issue agentforce314#2 并添加 progressing label
![image.png](https://raw.gitcode.com/user-images/assets/9901179/14587d4d-a5aa-429e-a578-0ff160f9baf7/image.png 'image.png')
![image.png](https://raw.gitcode.com/user-images/assets/9901179/c7966fcf-2125-4e39-935c-d205479a7b11/image.png 'image.png')
Step 2: Agent 运行
orchestrator 检测到 Issue agentforce314#2 → 启动 DeepSeek agent → 3 turns / 21 tool calls

# orchestrator 日志
[dashboard] session start: agentforce314#2 (2)
Starting agent run issue_id=2 identifier=agentforce314#2
Agent run completed issue_id=2 turns=3/15 tools=21
Step 3: 创建分支并推送
自动创建 clawcodex/2-add-type-hints-to-click.testing-module 分支并 push:
![image.png](https://raw.gitcode.com/user-images/assets/9901179/e7b3cc52-7252-4fb2-a568-5ce29bf049e1/image.png 'image.png')
Step 4: 创建 PR agentforce314#2
![image.png](https://raw.gitcode.com/user-images/assets/9901179/2c5e1e4e-32d7-4661-9265-8f96110e04ec/image.png 'image.png')
Scene 2: Review Comment → Follow-up Commit
Step 1: 发布 review 评论
评论 ID 175732234: "Please add a CONTRIBUTING.md file to the repository root..."
![image.png](https://raw.gitcode.com/user-images/assets/9901179/08892885-8b53-42fa-9e3a-dc9840a3bd2a/image.png 'image.png')
Step 2: Feedback 检测
_filter_pending() 识别为新 feedback → mark_feedback_pending() 标记

Step 3: Follow-up Agent 运行
1 turn / 16 tool calls — Agent 分析评论并修改代码

Step 4: 追加 Commit + 推送
![image.png](https://raw.gitcode.com/user-images/assets/9901179/2777809d-0b1d-4841-80ea-58e9f188aac6/image.png 'image.png')
![image.png](https://raw.gitcode.com/user-images/assets/9901179/c6839f97-840c-4b50-8a7e-96fb950aa162/image.png 'image.png')
Step 5: 自动回复 "Handled"
![image.png](https://raw.gitcode.com/user-images/assets/9901179/d5f71677-e07b-4b84-aed5-0fc2f0e5f78b/image.png 'image.png')
Step 6: Follow-up Run Summary
![image.png](https://raw.gitcode.com/user-images/assets/9901179/f8c7d859-d1df-4582-a819-eaf8002e34cf/image.png 'image.png')
Step 7: Follow-up Feedback List
![image.png](https://raw.gitcode.com/user-images/assets/9901179/39f6bbff-3f1f-441f-8ee9-97a899fc8017/image.png 'image.png')
Scene 3: 幂等性验证
重启 orchestrator → 重新加 registry → 已处理评论 175732234 在 processed_feedback_ids 中 → 不再触发
Scene 4: 第二轮 Follow-up
第二条 review 评论 175741951 触发第二次 follow-up。
followup_attempt_count: 1 → 2
processed_feedback_ids 同时包含两条 ID
![image.png](https://raw.gitcode.com/user-images/assets/9901179/cfded4b3-3e22-4ba2-b37f-a449d153bb07/image.png 'image.png')
![image.png](https://raw.gitcode.com/user-images/assets/9901179/080f90c8-8b7b-42b9-b606-aae8ff292e45/image.png 'image.png')
Scene 5: max_followup_attempts_per_pr 上限
配置 max=2,followup_attempt_count=2 后发第三条评论 175742570。
✅ can_follow_up() 返回 False
✅ 日志只有 GET /issues 轮询,无 feedback fetch
# cap 生效后的轮询
GET /issues?state=open&labels=progressing  →  200 OK
# 仅 issue 轮询,无 agentforce314/issues/2/comments  →  cap 阻止 ✅
Scene 6: ignore_authors A/B 对照
Phase	配置	同一条评论 175742570	结果
A	ignore_authors: [qq_49552963]	被 _filter_pending() 过滤	跳过 ✅
B	ignore_authors: []	被拾取并触发 follow-up	Agent 运行 2 turns / 29 tools ✅
Scene 7: 多 PR 并发隔离
Step 1: Issue agentforce314#4 → PR agentforce314#4 创建
![image.png](https://raw.gitcode.com/user-images/assets/9901179/4ccd49c9-bd31-49a1-9250-65f26d405213/image.png 'image.png')
![image.png](https://raw.gitcode.com/user-images/assets/9901179/52bbd02c-9118-4b6a-9a3c-5d45ebbb3426/image.png 'image.png')
![image.png](https://raw.gitcode.com/user-images/assets/9901179/48ceae39-ec90-4444-91a5-874854df6680/image.png 'image.png')
![image.png](https://raw.gitcode.com/user-images/assets/9901179/222dc6b3-ac81-4f04-a6ff-3e8b2fa14523/image.png 'image.png')
Step 2: 同时发评论
Issue agentforce314#2: 175746587 ("[Multi-PR Test] For Issue agentforce314#2 PR...")
Issue agentforce314#4: 175746589 ("[Multi-PR Test] For Issue agentforce314#4 PR...")
Step 3: 同一轮询周期并发触发
2026-06-15 17:13:05,051 [dashboard] session start: agentforce314#2 (2)
2026-06-15 17:13:07,206 [dashboard] session start: agentforce314#4 (4)  ← 仅隔 2 秒
2026-06-15 17:14:07,377 Agent run completed issue_id=2 turns=1/15 tools=21
2026-06-15 17:15:48,924 Agent run completed issue_id=4 turns=1/15 tools=26
Step 4: Issue agentforce314#4 Follow-up Summary
![image.png](https://raw.gitcode.com/user-images/assets/9901179/dc38617b-befd-43a2-8a30-f446afb46e2c/image.png 'image.png')
![image.png](https://raw.gitcode.com/user-images/assets/9901179/400283cb-19ed-4117-b71c-948111f7d459/image.png 'image.png')
反馈不交叉
PR agentforce314#2 processed_feedback_ids 含 175746587,不含 175746589
PR agentforce314#4 processed_feedback_ids 含 175746589,不含 175746587
Scene 8: GitCode Inline PR Code Comment
![image.png](https://raw.gitcode.com/user-images/assets/9901179/77a4f49f-8011-4ab2-9025-283469422b9d/image.png 'image.png')
步骤	结果
创建 inline PR comment	Comment ID: 175749629, path: CONTRIBUTING.md
F-37 拾取	inline_review:175749629 出现在 pending
Follow-up Summary	[inline_review] inline_review:175749629: [Inline Test]... ✅
GitCode 的 /pulls/X/comments API 支持 inline code comments,F-37 正确识别 inline_review source。
Scene 9: Batch + max_feedback_items_per_run 截断
配置 max_feedback_items_per_run: 2,一次性在 Issue agentforce314#4 发 3 条评论。

第一次 follow-up 处理 2 条 → processed: [175754829, 175754826]
第 3 条留在 pending_feedback_ids: [175754820]
✅ 截断生效
同时在 Issue agentforce314#2 上之前卡住的 3 条 pending 评论也在并行 follow-up 中消费了 2 条。

See merge request: chadwweng/clawcodex!6
peroxider pushed a commit to peroxider/clawcodex that referenced this pull request Jun 16, 2026
fix(orchestrator): F-37 pending_feedback 超时自动清理

Created-by: qq_49552963
Commit-by: 叶蕴瑶;wukong;qq_49552963
Merged-by: chadwweng
Description: ## 问题描述

当 `ReviewFeedbackService.collect_followups()` 在单次轮询中检测到 N 条待处理的 review feedback 时,受 `max_feedback_items_per_run` 限制仅选取前 M 条(M < N)提交给 Agent 处理。被选中的 M 条与未选中的 (N-M) 条均被写入 `pending_feedback_ids`。

Agent 运行结束后,`mark_feedback_processed()` 将已处理的 M 条迁移至 `processed_feedback_ids`,但剩余 (N-M) 条仍驻留在 `pending_feedback_ids` 中。在后续轮询中,`_filter_pending()` 会跳过所有已存在于 `pending_feedback_ids` 的条目,导致这些未处理的 feedback **永久滞留**——既无法被重新检测,也不会被标记为已处理。

**影响范围**:当单次轮询的 feedback 数量超过 `max_feedback_items_per_run` 阈值,或 Agent 运行异常仅消费部分 feedback 时,未处理的 review 评论将被静默丢弃,不会产生任何日志告警。

## 修复方案

引入基于时间戳的超时清理机制,在每轮 `collect_followups` 执行前检测并清理滞留的 pending 条目,使其重新进入检测流程。

### 改动文件

| 文件 | 改动说明 |
|------|----------|
| `extensions/orchestrator/issue_registry.py` | 新增 `pending_feedback_since` 字段记录 pending 队列首次非空的时间戳;新增 `clear_stale_pending()` 方法,当滞留时间超过阈值时清空 pending 队列并重置时间戳 |
| `extensions/orchestrator/review_feedback.py` | 在 `collect_followups` 循环体开头调用 `clear_stale_pending()`,超时条目被清理后可被 `_filter_pending()` 重新识别 |
| `extensions/orchestrator/config/schema.py` | `ReviewFeedbackConfig` 新增 `pending_feedback_timeout_seconds` 配置项(默认 600 秒),支持通过 workflow YAML 自定义超时阈值 |

### 设计要点

- **不影响正常流程**:正常 Agent 运行在数分钟内即完成 `mark_feedback_processed` 调用,远早于默认 600 秒超时
- **向后兼容**:旧版 registry JSON 无 `pending_feedback_since` 字段时默认为 `None`,`clear_stale_pending()` 检测到 `None` 直接返回 0,不影响存量数据
- **幂等安全**:多次调用不会产生副作用,空队列直接短路返回

## 验证

在 h144 测试环境(gitcode.com/qq_49552963/click 仓库)设置 `pending_feedback_timeout_seconds: 60`,对真实 registry 数据执行验证:

- Issue agentforce314#2: `conversation:175749937` 滞留 3125 秒 → 超过 60 秒阈值 → 成功清理并触发重新检测
- Issue agentforce314#4: `pending_feedback_since=None`(历史数据无时间戳)→ 正确跳过,不误清

验证 (timeout=60s):

19:43:31,625 INFO  Cleared 1 stale pending feedback item(s) for issue 2 — will re-detect
19:43:35,186 INFO  [dashboard] session start: agentforce314#2 (2)
2 分钟前的 pending 条目 → poll 时自动清掉 → 重新检测 → 触发 follow-up ✅

h144 实际运行输出 (2026-06-15 20:35, 对 /tmp/click-f37-workspaces/.clawcodex_issue_registry.json 执行 clear_stale_pending):
Issue agentforce314#2: conversation:175749937 卡了 3125s → 超过 60s timeout → 已清理
Issue agentforce314#4: pending_feedback_since=None(旧代码未设时间戳)→ 正确跳过

See merge request: chadwweng/clawcodex!11
peroxider pushed a commit to peroxider/clawcodex that referenced this pull request Jun 23, 2026
…+ P108-B)

Layer 0 quick fix for the 3 highest-severity freeze risk points identified
in F-108 §十八 audit:

  * agentforce314#2 TUI Permission modal hang (CRITICAL)
  * agentforce314#3 TUI AskUserQuestion modal hang (CRITICAL)
  * agentforce314#5 headless query future never completes (HIGH)

- clawcodex_ext/tui/agent_bridge.py: bind _permission_handler /
  _ask_user_handler waits to _PERMISSION_TIMEOUT_S (30s). On timeout
  Permission auto-denies (False, False) and AskUser returns {}, both
  matching existing Esc-cancel semantics. timeout=0 preserves the
  legacy unbounded wait (F-108 design decision agentforce314#5).

- extensions/api/query.py: move the wall-clock budget inside the polling
  loop. asyncio.wait_for cannot cancel executor futures, so the budget
  check fires from loop_started_at and surfaces
  SessionComplete(reason="exit_code=124") + a query_runner.timeout
  NDJSON event for postmortem.

0 src/ files modified (Decoupling Mandate golden rule agentforce314#1).
12 new unit tests; 483 orchestrator regression tests unaffected.

Co-Authored-By: MiniMax-M3 <MiniMax-AI@claude-code-best.win>
peroxider pushed a commit to peroxider/clawcodex that referenced this pull request Jun 26, 2026
Migrate src/transports/remote_io.py (13174B) to
clawcodex_ext/transports/remote_io.py with a 9-line sys.modules facade,
eliminating 1 b_internal (src->src) ref.

Importers rerouted:
- src/transports/__init__.py (RemoteIO)

Internal imports remain src.* (allowed per Golden Rule agentforce314#2):
- src.cli_core.ndjson (already facaded) for ndjson_safe_dumps
- src.utils.session_ingress_auth for auth headers/token

Pattern unchanged: impl -> clawcodex_ext/<path>.py,
src/<path>.py -> 9-line sys.modules facade,
src importers -> `from clawcodex_ext.<path>`.

Verified: 262/262 stability gate stages 1-5 PASS, identity check
(src.X is clawcodex_ext.X) green, src.transports aggregator re-export
still resolves.

Co-Authored-By: MiniMax-M3 <MiniMax-AI@claude-code-best.win>
peroxider pushed a commit to peroxider/clawcodex that referenced this pull request Jun 26, 2026
Migrate 5 remaining src.bridge.* leaf modules to clawcodex_ext with
sys.modules facades, eliminating 14 b_internal (src->src) refs from
src/bridge/__init__.py.

Targets (4 pure leaves + 1 with allowed src.* deps):
- bridge_config (2872B) — 4 auth/URL accessors
- bridge_permission_callbacks (3558B) — TypedDict + type guard + Protocol
- capacity_wake (4654B) — CapacityWake / CapacitySignal / create_capacity_wake
- env_less_bridge_config (6053B) — Pydantic config model + defaults
- repl_bridge_handle (3819B) — process-global REPL bridge handle pointer
  (keeps `from src.bridge.types import ReplBridgeHandle` per Golden Rule agentforce314#2)

Importers rerouted:
- src/bridge/__init__.py (all 14 refs)

Pattern unchanged: impl -> clawcodex_ext/<path>.py,
src/<path>.py -> 9-line sys.modules facade,
src importers -> `from clawcodex_ext.<path>`.

Verified: 262/262 stability gate stages 1-5 PASS, all 5 facade identity
checks green, capacity_wake short-circuit path works, aggregator
re-exports resolve.

Co-Authored-By: MiniMax-M3 <MiniMax-AI@claude-code-best.win>
peroxider pushed a commit to peroxider/clawcodex that referenced this pull request Jun 26, 2026
…atch H)

Migrate 2 coordinator leaf modules to clawcodex_ext with sys.modules
facades, eliminating 8 b_internal (src->src) refs from
src/coordinator/__init__.py.

Targets:
- coordinator/mode.py (8991B) — is_coordinator_mode, match_session_mode,
  INTERNAL_WORKER_TOOLS, filter_coordinator_tools, filter_worker_tools,
  get_coordinator_user_context. Keeps TYPE_CHECKING import of
  src.tool_system.build_tool:Tool per Golden Rule agentforce314#2.
- coordinator/worker_agent.py (2099B) — WORKER_AGENT, get_coordinator_agents

Importers rerouted:
- src/coordinator/__init__.py (all 8 refs)

Pattern unchanged: impl -> clawcodex_ext/<path>.py,
src/<path>.py -> 9-line sys.modules facade,
src importers -> `from clawcodex_ext.<path>`.

Verified: 262/262 stability gate stages 1-5 PASS, identity checks
(src.X is clawcodex_ext.X) green, coordinator aggregator re-exports
resolve, WORKER_AGENT.agent_type == 'worker'.

Co-Authored-By: MiniMax-M3 <MiniMax-AI@claude-code-best.win>
ericleepi314 added a commit that referenced this pull request Jul 3, 2026
The round-4 LLM memory-relevance recall (#594) ships default-off because
flipping it on is a latency+cost+correctness regression. This fixes the
four blockers the round-4 critic flagged. Still default-off — zero
behavior change on merge.

- #2 selector cost (provider-gated pin): the recall side-query ran on the
  SESSION model (an Opus turn paid full price every turn). TS pins it to a
  small default (getDefaultSonnetModel). The port wires the small_fast_model
  setting, but its shipped default is an Anthropic id
  (claude-3-5-haiku-20241022) valid only on the first-party Anthropic
  endpoint. So _resolve_recall_model(provider) applies the pin ONLY when the
  session provider is a first-party AnthropicProvider; Minimax (Anthropic
  SDK, own endpoint), DeepSeek, OpenAI, OpenRouter fall back to the session
  model. Without the gate the Anthropic default id 400s on a non-Anthropic
  endpoint and — since recall swallows errors — silently kills recall every
  turn (critic M1). small_fast_model_provider pairing for non-Anthropic
  cheap-recall is deferred.
- #3 de-dup reset on compaction: the recall reminder is ephemeral but
  _memory_surfaced was monotonic — once surfaced, a memory was never
  re-surfaced. After /compact drops the earlier context, the memory stayed
  suppressed. _do_compact now clears _memory_surfaced on success (idle-gated
  — no race with the turn worker).
- #4 aggregate turn byte-cap: per-file was 4 KB but 5 selections could
  inject ~20 KB/turn. build_relevant_memory_reminder_with_paths caps at
  MAX_TOTAL_SURFACE_CHARS=12 KB and returns the paths ACTUALLY surfaced;
  get_relevant_memory_reminder de-dups only those, so a cap-trimmed memory
  stays eligible (the old mark-all was a latent suppress-forever bug the cap
  would have activated). Back-compat string wrapper retained.
- #5 docstring: _maybe_recall_memories said "prepend"; the code appends
  after the user turn (correct — normalize_messages merges consecutive user
  messages). Fixed.

Deferred #1 (synchronous side-query): the port has only this one prefetch,
so there is no concurrent prefetch to overlap without a larger
prompt-submit refactor — one small-model call per turn (fast Haiku on the
default path). Documented.

Docs: my-docs/port-improvement-round-5/r5-1-ch11-memory-enable-blockers.md
+ critic verdicts (REVISE->APPROVE).

Tests: tests/test_r5_ch11_memory_enable_blockers.py (8) incl. the M1
regression guard (non-Anthropic never pins even when the Anthropic-default
id is set). Existing memory suite (466) green via the back-compat wrapper.
Full suite at the pre-existing baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ericleepi314 added a commit that referenced this pull request Jul 3, 2026
The round-4 LLM memory-relevance recall (#594) ships default-off because
flipping it on is a latency+cost+correctness regression. This fixes the
four blockers the round-4 critic flagged. Still default-off — zero
behavior change on merge.

- #2 selector cost (provider-gated pin): the recall side-query ran on the
  SESSION model (an Opus turn paid full price every turn). TS pins it to a
  small default (getDefaultSonnetModel). The port wires the small_fast_model
  setting, but its shipped default is an Anthropic id
  (claude-3-5-haiku-20241022) valid only on the first-party Anthropic
  endpoint. So _resolve_recall_model(provider) applies the pin ONLY when the
  session provider is a first-party AnthropicProvider; Minimax (Anthropic
  SDK, own endpoint), DeepSeek, OpenAI, OpenRouter fall back to the session
  model. Without the gate the Anthropic default id 400s on a non-Anthropic
  endpoint and — since recall swallows errors — silently kills recall every
  turn (critic M1). small_fast_model_provider pairing for non-Anthropic
  cheap-recall is deferred.
- #3 de-dup reset on compaction: the recall reminder is ephemeral but
  _memory_surfaced was monotonic — once surfaced, a memory was never
  re-surfaced. After /compact drops the earlier context, the memory stayed
  suppressed. _do_compact now clears _memory_surfaced on success (idle-gated
  — no race with the turn worker).
- #4 aggregate turn byte-cap: per-file was 4 KB but 5 selections could
  inject ~20 KB/turn. build_relevant_memory_reminder_with_paths caps at
  MAX_TOTAL_SURFACE_CHARS=12 KB and returns the paths ACTUALLY surfaced;
  get_relevant_memory_reminder de-dups only those, so a cap-trimmed memory
  stays eligible (the old mark-all was a latent suppress-forever bug the cap
  would have activated). Back-compat string wrapper retained.
- #5 docstring: _maybe_recall_memories said "prepend"; the code appends
  after the user turn (correct — normalize_messages merges consecutive user
  messages). Fixed.

Deferred #1 (synchronous side-query): the port has only this one prefetch,
so there is no concurrent prefetch to overlap without a larger
prompt-submit refactor — one small-model call per turn (fast Haiku on the
default path). Documented.

Docs: my-docs/port-improvement-round-5/r5-1-ch11-memory-enable-blockers.md
+ critic verdicts (REVISE->APPROVE).

Tests: tests/test_r5_ch11_memory_enable_blockers.py (8) incl. the M1
regression guard (non-Anthropic never pins even when the Anthropic-default
id is set). Existing memory suite (466) green via the back-compat wrapper.
Full suite at the pre-existing baseline.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ericleepi314 added a commit that referenced this pull request Jul 3, 2026
LocalAgentTaskState.abort_event was declared (local_agent.py:77) but NEVER
assigned — only read — so kill_async_agent's abort_event.set() was guarded
out and killing a background Agent only flipped the registry status to
"killed" while the live run kept going to NATURAL completion (burning model
tokens). Directly counter to "run long coding tasks reliably": a user who
stops a runaway background agent expects it to stop.

The async run already had an abort mechanism the port never exposed:
run_agent mints a fresh AbortController for async agents (run_agent.py:291),
wires it into the subagent context, and query() polls signal.aborted at
every yield point (query.py:1433/1520/1571/2046). This makes it REACHABLE:
- LocalAgentTaskState.abort_controller field + register_async_agent param.
- _launch_async_agent mints a dedicated AbortController, stores it on the
  task state AND sets run_params.abort_controller, so run_agent uses THIS
  one (run_agent.py:286-287) instead of its unreachable internal one. FRESH
  (not the parent turn's) → preserves async isolation.
- kill_async_agent calls abort_controller.abort("killed by user") → the
  run's query() loop halts at the next poll.

Cross-thread safe: the kill runs on the kill thread, the run polls on the
background loop; AbortController.abort() is a GIL-atomic flag set + sync
listeners and signal.aborted is an atomic read — the same cross-thread
pattern the turn-interrupt path already uses. .abort() is placed outside
the registry lock (matches the abort_event.set() rationale). Legacy
abort_event.set() retained (harmless defense).

Docs: my-docs/port-improvement-round-6/r6-1-kill-background-agent.md +
critic verdict (APPROVE).

Tests: tests/test_r6_kill_background_agent.py (4): unit (kill aborts the
stored controller + flips killed; kill w/o controller is safe); END-TO-END
(a long-running bg agent that polls the abort signal STOPS advancing after a
cross-thread kill — NON-VACUOUS: neg-control advances ~34 steps post-kill
without the fix, <=2 with it) + two ISOLATION assertions (killing the bg
agent doesn't abort the parent; aborting the parent doesn't stop the bg
agent). 1812 agent/async/task tests green; full suite at the 9-failure
baseline.

Deferred (critic minor #2): the resume path must mirror this wire when it
gains a live run loop, else resumed agents are un-killable (benign today —
resume doesn't drive a run).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ericleepi314 added a commit that referenced this pull request Jul 3, 2026
LocalAgentTaskState.abort_event was declared (local_agent.py:77) but NEVER
assigned — only read — so kill_async_agent's abort_event.set() was guarded
out and killing a background Agent only flipped the registry status to
"killed" while the live run kept going to NATURAL completion (burning model
tokens). Directly counter to "run long coding tasks reliably": a user who
stops a runaway background agent expects it to stop.

The async run already had an abort mechanism the port never exposed:
run_agent mints a fresh AbortController for async agents (run_agent.py:291),
wires it into the subagent context, and query() polls signal.aborted at
every yield point (query.py:1433/1520/1571/2046). This makes it REACHABLE:
- LocalAgentTaskState.abort_controller field + register_async_agent param.
- _launch_async_agent mints a dedicated AbortController, stores it on the
  task state AND sets run_params.abort_controller, so run_agent uses THIS
  one (run_agent.py:286-287) instead of its unreachable internal one. FRESH
  (not the parent turn's) → preserves async isolation.
- kill_async_agent calls abort_controller.abort("killed by user") → the
  run's query() loop halts at the next poll.

Cross-thread safe: the kill runs on the kill thread, the run polls on the
background loop; AbortController.abort() is a GIL-atomic flag set + sync
listeners and signal.aborted is an atomic read — the same cross-thread
pattern the turn-interrupt path already uses. .abort() is placed outside
the registry lock (matches the abort_event.set() rationale). Legacy
abort_event.set() retained (harmless defense).

Docs: my-docs/port-improvement-round-6/r6-1-kill-background-agent.md +
critic verdict (APPROVE).

Tests: tests/test_r6_kill_background_agent.py (4): unit (kill aborts the
stored controller + flips killed; kill w/o controller is safe); END-TO-END
(a long-running bg agent that polls the abort signal STOPS advancing after a
cross-thread kill — NON-VACUOUS: neg-control advances ~34 steps post-kill
without the fix, <=2 with it) + two ISOLATION assertions (killing the bg
agent doesn't abort the parent; aborting the parent doesn't stop the bg
agent). 1812 agent/async/task tests green; full suite at the 9-failure
baseline.

Deferred (critic minor #2): the resume path must mirror this wire when it
gains a live run loop, else resumed agents are un-killable (benign today —
resume doesn't drive a run).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
peroxider pushed a commit to peroxider/clawcodex that referenced this pull request Jul 3, 2026
…gressive summarization

修复 /goal 系统四个关键问题:

agentforce314#1 — _pending_injection 排空(P0 阻断性 Bug)
- repl/app.py: chat() finally 中 drain 注入并 enqueue 到 _queued_prompts
- chat() 开始时 drain set_new_goal() 遗留的 objective_updated 注入
- TUI 路径 (tui/screens/repl.py): 在 on_agent_run_finished 中 drain 注入
  并追加到 app_state.queued_prompts + 投递 QueuedPromptReady

agentforce314#2 — <active-goal> 动态截断(原硬编码 240 字)
- _dynamic_objective_chars() 根据 token 预算比例动态调优:
  >60% → 600, 30-60% → 360, 10-30% → 240, <10% → 160, 无预算 → 480

agentforce314#3 — 压缩管道 goal-aware 优先级
- PipelineConfig.goal_active: bool 字段
- goal_active 时 autocompact 阈值 +10%(上限 0.95)
- _is_goal_steering_message() 保护含 <goal-steering 的消息不被压缩
- query/engine.py: _check_goal_active() 查询 GoalStateRegistry

agentforce314#4 — 渐进式摘要(Progressive Summarization)
- GoalState.milestones: list[dict] + MILESTONE_TURN_INTERVAL=15
- state_machine: add_milestone() 纯函数 + 各转换保留 milestones
- controller: on_assistant_turn_complete() 每 15 轮自动记录里程碑
- prompts: continuation_prompt 在里程碑边界注入结构化进度要求
- <active-goal> 上下文块嵌入最近 5 个里程碑的 XML 摘要

Tests: 86/86 goal + 145/145 compact + 68/69 stage5 (1 pre-existing CI circular import) all pass.

Co-Authored-By: AtomCode (deepseek-v4-flash) <noreply@atomgit.com>
ericleepi314 added a commit that referenced this pull request Jul 3, 2026
The expand-result feature (#623) delegated verbose-block truncation to
boundedLiveRenderText, which keeps the TAIL with a 'showing live tail'
label — correct for the live stream, wrong for a persisted ctrl+o
expansion where the reader wants the START of Grep matches / Bash output
/ an error. The comment had also gone stale (claimed 'NOT the 16KB
budget' when the cap is now exactly 16k, and 'expanded by default' when
it's behind ctrl+o).

- New headTruncate: keeps the first VERBOSE_TRAIL_MAX_LINES/_CHARS with a
  trailing '… +N lines/chars omitted' marker (no live-tail label).
- verboseToolBlock uses it; comment corrected to match the behavior.
- boundedLiveRenderText stays for the genuine live-stream tails
  (streaming assistant text, thinking) — unchanged.

Test: a 500-line Grep result keeps line 0-199, drops line 200 and the
499 tail, shows '… +N lines omitted', and no longer says 'showing live
tail' — distinguishing head from tail (the old 'A'.repeat test couldn't).

Reconciles the plan, the comment, the label, and the code (critic
Finding B, required change #2 from the expand-E implementation review —
missed in the first fix round, which only closed Finding A).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ericleepi314 added a commit that referenced this pull request Jul 4, 2026
…S-2) (#645)

* feat(services): autoFix config + hook + runner (SERVICES-2 W1-W3; wiring next)

Port of typescript/src/services/autoFix/ — the settings-opt-in feature that
runs the user's lint/test command after a file edit and injects
<auto_fix_feedback> so the model self-fixes. Python had only the /autofix
slash shell; the runtime was absent.

- config.py (autoFixConfig.ts): get_auto_fix_config(raw) → AutoFixConfig|None
  — dict + enabled + at-least-one-of-lint/test (the zod .refine) + numeric
  bounds (maxRetries 0..10 default 3, timeout 1000..300000 default 30000);
  None (safeParse-null posture) on any failure. load_auto_fix_config reads
  settings.extra["autoFix"] (verified: no typed field — lands in extra, like
  the plugins round's enabledPlugins).
- hook.py (autoFixHook.ts): AUTO_FIX_TOOLS = the port's file-mutation
  registry names (Write/Edit/MultiEdit/NotebookEdit, the file_edit/file_write
  analog); should_run_auto_fix; build_auto_fix_context (the <auto_fix_feedback>
  block VERBATIM) + build_max_retries_context (verbatim).
- runner.py (autoFixRunner.ts): run_auto_fix_check — lint first, test only if
  lint passed; each an asyncio.create_subprocess_shell bounded by timeout,
  killing the process GROUP on timeout (start_new_session + os.killpg, the
  detached/killTree analog) and reaping to avoid zombies; _build_error_summary
  verbatim; never-raises (spawn failure → has_errors=False).

Verified: config parse/refine/bounds, hook fire + verbatim context, runner
lint-fail-skips-test / lint-pass-test-fail / both-clean / timeout-kill
(sleep 30 @ 800ms → returns 0.81s, group killed, timed_out) / abort.

REMAINING: W4 wiring — a NEW sibling run_auto_fix_step at the orchestrator
(tool_execution.py, after run_post_tool_use_hooks), NOT inside that function
(it early-returns when no user PostToolUse hooks configured — the
teammate-hooks split-gate class; autoFix must run regardless). Retry cap by
query_tracking.chain_id. Then tests + suite. Awaiting autofix-critic on the
plan's W4 placement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(services): autoFix wiring + all critic findings (SERVICES-2 complete)

The autofix-critic caught that TS autoFix is DEAD CODE — AUTO_FIX_TOOLS =
{'file_edit','file_write'} never matches the real tool names (Edit/Write,
FileEditTool/constants.ts:2 FILE_EDIT_TOOL_NAME='Edit'), verified
first-hand. So this port activates the author's INTENT ({Edit,Write}) as a
DOCUMENTED, opt-in-gated DIVERGENCE (fixes the reference's dead tool-name
set; inert unless a user writes settings.autoFix.enabled).

All findings:
- B1 (split-gate): run_auto_fix_step is a NEW SIBLING at the orchestrator
  (tool_execution.py, after run_post_tool_use_hooks) — NOT inside that
  function, which early-returns when no user PostToolUse hook is configured
  (the common autoFix case). Pinned by test_fires_with_zero_posttool_hooks.
- B2 (accessor): config via get_settings().extra.get("autoFix") (ToolContext
  has no settings; global read = the per-process equivalent).
- M1 (reject-not-clamp): out-of-range/non-int → whole config None (zod
  .min/.max rejects); .default only when key absent.
- M2 (camelCase): reads raw["maxRetries"]/raw["timeout"].
- M3 (reset-on-success): the retry counter is DELETED on a clean run
  (toolHooks.ts:247), not just incremented on error.
- M4 (tool set): {"Edit","Write"} — the author's intent, not the 4-element
  _FILE_EDIT_TOOLS (NotebookEdit excluded, MultiEdit is a phantom).
- M5 (None-guard): chain key handles query_tracking=None → "default".
- M6 (10k cap): each stream sliced to 10 000 before combine.
- minors: SIGTERM (not SIGKILL) + reap + ProcessLookupError race; mid-flight
  abort (kills the group, degrades to no-errors); cwd from tool_use_context;
  the post-hook attachment yield shape (list-wrapped content).

tests/services/test_autofix.py (26): config parse/refine/reject/camelCase,
hook fire + verbatim contexts + {Edit,Write} intent, runner (lint-skips-test
/ test-fail / clean / timeout-SIGTERM-kill / 10k cap / pre-set abort /
mid-flight abort / no-commands), step (ZERO-hooks regression, non-file skip,
retry cap, reset-on-clean, None-query-tracking). Full suite at the 6-failure
baseline (7857 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(services): autoFix config zod-strictness (critic minor #1)

enabled must be a strict bool (a string "false" is truthy in Python — must
not turn autoFix ON against intent); a bad-type lint/test rejects the whole
config → None, matching zod z.boolean()/z.string() safeParse (not the prior
silent coerce/drop). 3 new tests; consistent with the already-strict int
path. (Minors #2 retry-map-lifecycle and #3 proxy-coverage accepted as
faithful-to-TS / adequate.) Full suite at the 6-failure baseline (7860).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ericleepi314 added a commit that referenced this pull request Jul 6, 2026
* feat(bash): notify on background-bash completion (C5 Part 1 — enqueueShellNotification)

TS notifies when a run_in_background bash task reaches a terminal state
(utils/task/framework.ts:289 enqueuePendingNotification; BashTool/prompt.ts:
285-287 "you will be notified when it completes — do not poll"). The port
marked the terminal state notified=True WITHOUT sending — the documented
forward-trap in background.py (ch10 WI-2), so a bg bash finishing while the
model was away went unannounced (silently polled via TaskOutput instead).

- task_notification.py: enqueue_shell_notification — mirrors
  enqueue_agent_notification (the atomic check-and-set duplicate-delivery
  guard + build_task_notification_xml + enqueue_pending_notification) but
  gated on LocalShellTaskState instead of LocalAgentTaskState.
- background.py: REMOVED the forward-trap notified=True from the terminal
  replace() (per its own comment's instruction), and call
  enqueue_shell_notification after the terminal update. The eviction sweeper's
  notify-before-evict guard (eviction.py:97) now correctly keeps the entry
  until the notification is delivered, then reclaims it — instead of the old
  mark-notified-without-sending. Best-effort (never breaks reaping);
  duplicate-safe (a TaskStop that already notified makes it a no-op).

tests/test_shell_completion_notification.py (3): enqueues + marks notified on
terminal, duplicate-safe (already-notified → no second), wrong-task-type no-op.
44 background/eviction/notification tests green (the notified=False terminal
state is compatible with the eviction guard).

Part 2 (the Monitor tool + backpressure) follows separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bash): C5 Part 1 — fallback-mark-notified on enqueue failure (no leak)

Pre-empting the eviction-leak risk: enqueue_shell_notification is best-effort
(except: pass), but with the forward-trap removed the terminal state is
notified=False — so if the enqueue raised, the sweeper (which keeps un-notified
terminal tasks, eviction.py:97) would leak the task in /tasks forever. Added a
fallback: on enqueue failure, force notified=True so the task stays evictable,
degrading to the OLD evictable-but-unsent behavior rather than a leak (still
strictly better than the pre-C5 silent-drop).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bash): C5 Part 1 critic — shell-specific summary, XML escaping, kill path

The c5p1-critic found the model-facing payload was wrong (the mechanism was
right). Real TS analog: enqueueShellNotification (LocalShellTask.tsx:105-172).

#1 BLOCKING — wrong summary: reused the AGENT builder, so every bg-bash
completion was announced as `Agent "..." completed`, the exit code was dropped,
and the failed case said "Unknown error". Added build_shell_notification_xml +
_build_shell_summary + BACKGROUND_BASH_SUMMARY_PREFIX ("Background command ") —
`Background command "<desc>" completed (exit code N)` / `... failed with exit
code N` / `... was stopped`, exit code included only when known. Threaded
exit_code + tool_use_id through enqueue_shell_notification ← background.py's rc.

#2 MAJOR — no XML escaping: a bg command routinely has < > & (and _reap uses
`description or command`), so a raw summary produced a MALFORMED envelope. The
shell builder XML-escapes the summary (TS LocalShellTask.tsx:164 escapeXml);
the agent builder stays raw (LocalAgentTask.tsx:246-256 is raw — that claim was
only true for agents).

#3 MAJOR — kill path: TS killTask (killShellTasks.ts:38-44) atomically sets
status='killed' + notified=True, which SUPPRESSES the reaper notification (no
notification on a user kill). The port sent SIGTERM only → the reaper saw rc≠0
→ sent a spurious `failed`. stop_background_bash now sets status='killed' +
notified=True; _patch preserves a 'killed' status (doesn't reclassify from the
SIGTERM exit code). The duplicate-guard's "TaskStop did it" premise is now
actually wired.

#4 tests — the old tests never asserted summary CONTENT (passed with the wrong
"Agent" text). Added: full-summary snapshots (prefix + exit code + not-"Agent"),
metachar XML-escaping, a REAL spawn→reap integration (exit 3 → the "failed with
exit code 3" notification delivered), and kill-suppression (kill → status=killed
+ notified + NO notification).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bash): C5 Part 1 #3 — mark killed in LocalShellTask.kill before the signal

The c5p1-critic reproduced the spurious kill notification 8/8 through the REAL
path (TaskStop tool → stop_task → impl.kill = LocalShellTask.kill). My prior
mark lived only in stop_background_bash, which stop_task doesn't call — so
nothing marked killed/notified before the reaper, which sent
"failed with exit code -15".

Fix (the critic's exact direction): move the status='killed' + notified=True
mutation into LocalShellTask.kill, BEFORE os.killpg, gated on the process still
being alive (TS killTask's status==='running' gate, killShellTasks.ts:38-44).
This is the production kill path (TaskStop → stop_task → this), so the mark
lives here; marking BEFORE the signal closes the reaper-vs-killer race (the
reaper is blocked in proc.wait() and can't wake until the signal lands, by
which point notified=True is committed → the reaper's enqueue no-ops). The
stop_background_bash mark stays as defense-in-depth, also moved before its
killpg. _patch preserves a 'killed' status.

Test rewritten to drive the REAL path: test_stop_task_kill_suppresses_notification
runs stop_task (what TaskStop uses) on a real bg task → asserts status=killed +
notified + NO notification (the critic's repro, now 0). + a direct
stop_background_bash defense-in-depth test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ericleepi314 added a commit that referenced this pull request Jul 7, 2026
The expand-result feature (#623) delegated verbose-block truncation to
boundedLiveRenderText, which keeps the TAIL with a 'showing live tail'
label — correct for the live stream, wrong for a persisted ctrl+o
expansion where the reader wants the START of Grep matches / Bash output
/ an error. The comment had also gone stale (claimed 'NOT the 16KB
budget' when the cap is now exactly 16k, and 'expanded by default' when
it's behind ctrl+o).

- New headTruncate: keeps the first VERBOSE_TRAIL_MAX_LINES/_CHARS with a
  trailing '… +N lines/chars omitted' marker (no live-tail label).
- verboseToolBlock uses it; comment corrected to match the behavior.
- boundedLiveRenderText stays for the genuine live-stream tails
  (streaming assistant text, thinking) — unchanged.

Test: a 500-line Grep result keeps line 0-199, drops line 200 and the
499 tail, shows '… +N lines omitted', and no longer says 'showing live
tail' — distinguishing head from tail (the old 'A'.repeat test couldn't).

Reconciles the plan, the comment, the label, and the code (critic
Finding B, required change #2 from the expand-E implementation review —
missed in the first fix round, which only closed Finding A).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ericleepi314 added a commit that referenced this pull request Jul 7, 2026
…S-2) (#645)

* feat(services): autoFix config + hook + runner (SERVICES-2 W1-W3; wiring next)

Port of typescript/src/services/autoFix/ — the settings-opt-in feature that
runs the user's lint/test command after a file edit and injects
<auto_fix_feedback> so the model self-fixes. Python had only the /autofix
slash shell; the runtime was absent.

- config.py (autoFixConfig.ts): get_auto_fix_config(raw) → AutoFixConfig|None
  — dict + enabled + at-least-one-of-lint/test (the zod .refine) + numeric
  bounds (maxRetries 0..10 default 3, timeout 1000..300000 default 30000);
  None (safeParse-null posture) on any failure. load_auto_fix_config reads
  settings.extra["autoFix"] (verified: no typed field — lands in extra, like
  the plugins round's enabledPlugins).
- hook.py (autoFixHook.ts): AUTO_FIX_TOOLS = the port's file-mutation
  registry names (Write/Edit/MultiEdit/NotebookEdit, the file_edit/file_write
  analog); should_run_auto_fix; build_auto_fix_context (the <auto_fix_feedback>
  block VERBATIM) + build_max_retries_context (verbatim).
- runner.py (autoFixRunner.ts): run_auto_fix_check — lint first, test only if
  lint passed; each an asyncio.create_subprocess_shell bounded by timeout,
  killing the process GROUP on timeout (start_new_session + os.killpg, the
  detached/killTree analog) and reaping to avoid zombies; _build_error_summary
  verbatim; never-raises (spawn failure → has_errors=False).

Verified: config parse/refine/bounds, hook fire + verbatim context, runner
lint-fail-skips-test / lint-pass-test-fail / both-clean / timeout-kill
(sleep 30 @ 800ms → returns 0.81s, group killed, timed_out) / abort.

REMAINING: W4 wiring — a NEW sibling run_auto_fix_step at the orchestrator
(tool_execution.py, after run_post_tool_use_hooks), NOT inside that function
(it early-returns when no user PostToolUse hooks configured — the
teammate-hooks split-gate class; autoFix must run regardless). Retry cap by
query_tracking.chain_id. Then tests + suite. Awaiting autofix-critic on the
plan's W4 placement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(services): autoFix wiring + all critic findings (SERVICES-2 complete)

The autofix-critic caught that TS autoFix is DEAD CODE — AUTO_FIX_TOOLS =
{'file_edit','file_write'} never matches the real tool names (Edit/Write,
FileEditTool/constants.ts:2 FILE_EDIT_TOOL_NAME='Edit'), verified
first-hand. So this port activates the author's INTENT ({Edit,Write}) as a
DOCUMENTED, opt-in-gated DIVERGENCE (fixes the reference's dead tool-name
set; inert unless a user writes settings.autoFix.enabled).

All findings:
- B1 (split-gate): run_auto_fix_step is a NEW SIBLING at the orchestrator
  (tool_execution.py, after run_post_tool_use_hooks) — NOT inside that
  function, which early-returns when no user PostToolUse hook is configured
  (the common autoFix case). Pinned by test_fires_with_zero_posttool_hooks.
- B2 (accessor): config via get_settings().extra.get("autoFix") (ToolContext
  has no settings; global read = the per-process equivalent).
- M1 (reject-not-clamp): out-of-range/non-int → whole config None (zod
  .min/.max rejects); .default only when key absent.
- M2 (camelCase): reads raw["maxRetries"]/raw["timeout"].
- M3 (reset-on-success): the retry counter is DELETED on a clean run
  (toolHooks.ts:247), not just incremented on error.
- M4 (tool set): {"Edit","Write"} — the author's intent, not the 4-element
  _FILE_EDIT_TOOLS (NotebookEdit excluded, MultiEdit is a phantom).
- M5 (None-guard): chain key handles query_tracking=None → "default".
- M6 (10k cap): each stream sliced to 10 000 before combine.
- minors: SIGTERM (not SIGKILL) + reap + ProcessLookupError race; mid-flight
  abort (kills the group, degrades to no-errors); cwd from tool_use_context;
  the post-hook attachment yield shape (list-wrapped content).

tests/services/test_autofix.py (26): config parse/refine/reject/camelCase,
hook fire + verbatim contexts + {Edit,Write} intent, runner (lint-skips-test
/ test-fail / clean / timeout-SIGTERM-kill / 10k cap / pre-set abort /
mid-flight abort / no-commands), step (ZERO-hooks regression, non-file skip,
retry cap, reset-on-clean, None-query-tracking). Full suite at the 6-failure
baseline (7857 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(services): autoFix config zod-strictness (critic minor #1)

enabled must be a strict bool (a string "false" is truthy in Python — must
not turn autoFix ON against intent); a bad-type lint/test rejects the whole
config → None, matching zod z.boolean()/z.string() safeParse (not the prior
silent coerce/drop). 3 new tests; consistent with the already-strict int
path. (Minors #2 retry-map-lifecycle and #3 proxy-coverage accepted as
faithful-to-TS / adequate.) Full suite at the 6-failure baseline (7860).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ericleepi314 added a commit that referenced this pull request Jul 7, 2026
* feat(bash): notify on background-bash completion (C5 Part 1 — enqueueShellNotification)

TS notifies when a run_in_background bash task reaches a terminal state
(utils/task/framework.ts:289 enqueuePendingNotification; BashTool/prompt.ts:
285-287 "you will be notified when it completes — do not poll"). The port
marked the terminal state notified=True WITHOUT sending — the documented
forward-trap in background.py (ch10 WI-2), so a bg bash finishing while the
model was away went unannounced (silently polled via TaskOutput instead).

- task_notification.py: enqueue_shell_notification — mirrors
  enqueue_agent_notification (the atomic check-and-set duplicate-delivery
  guard + build_task_notification_xml + enqueue_pending_notification) but
  gated on LocalShellTaskState instead of LocalAgentTaskState.
- background.py: REMOVED the forward-trap notified=True from the terminal
  replace() (per its own comment's instruction), and call
  enqueue_shell_notification after the terminal update. The eviction sweeper's
  notify-before-evict guard (eviction.py:97) now correctly keeps the entry
  until the notification is delivered, then reclaims it — instead of the old
  mark-notified-without-sending. Best-effort (never breaks reaping);
  duplicate-safe (a TaskStop that already notified makes it a no-op).

tests/test_shell_completion_notification.py (3): enqueues + marks notified on
terminal, duplicate-safe (already-notified → no second), wrong-task-type no-op.
44 background/eviction/notification tests green (the notified=False terminal
state is compatible with the eviction guard).

Part 2 (the Monitor tool + backpressure) follows separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bash): C5 Part 1 — fallback-mark-notified on enqueue failure (no leak)

Pre-empting the eviction-leak risk: enqueue_shell_notification is best-effort
(except: pass), but with the forward-trap removed the terminal state is
notified=False — so if the enqueue raised, the sweeper (which keeps un-notified
terminal tasks, eviction.py:97) would leak the task in /tasks forever. Added a
fallback: on enqueue failure, force notified=True so the task stays evictable,
degrading to the OLD evictable-but-unsent behavior rather than a leak (still
strictly better than the pre-C5 silent-drop).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bash): C5 Part 1 critic — shell-specific summary, XML escaping, kill path

The c5p1-critic found the model-facing payload was wrong (the mechanism was
right). Real TS analog: enqueueShellNotification (LocalShellTask.tsx:105-172).

#1 BLOCKING — wrong summary: reused the AGENT builder, so every bg-bash
completion was announced as `Agent "..." completed`, the exit code was dropped,
and the failed case said "Unknown error". Added build_shell_notification_xml +
_build_shell_summary + BACKGROUND_BASH_SUMMARY_PREFIX ("Background command ") —
`Background command "<desc>" completed (exit code N)` / `... failed with exit
code N` / `... was stopped`, exit code included only when known. Threaded
exit_code + tool_use_id through enqueue_shell_notification ← background.py's rc.

#2 MAJOR — no XML escaping: a bg command routinely has < > & (and _reap uses
`description or command`), so a raw summary produced a MALFORMED envelope. The
shell builder XML-escapes the summary (TS LocalShellTask.tsx:164 escapeXml);
the agent builder stays raw (LocalAgentTask.tsx:246-256 is raw — that claim was
only true for agents).

#3 MAJOR — kill path: TS killTask (killShellTasks.ts:38-44) atomically sets
status='killed' + notified=True, which SUPPRESSES the reaper notification (no
notification on a user kill). The port sent SIGTERM only → the reaper saw rc≠0
→ sent a spurious `failed`. stop_background_bash now sets status='killed' +
notified=True; _patch preserves a 'killed' status (doesn't reclassify from the
SIGTERM exit code). The duplicate-guard's "TaskStop did it" premise is now
actually wired.

#4 tests — the old tests never asserted summary CONTENT (passed with the wrong
"Agent" text). Added: full-summary snapshots (prefix + exit code + not-"Agent"),
metachar XML-escaping, a REAL spawn→reap integration (exit 3 → the "failed with
exit code 3" notification delivered), and kill-suppression (kill → status=killed
+ notified + NO notification).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bash): C5 Part 1 #3 — mark killed in LocalShellTask.kill before the signal

The c5p1-critic reproduced the spurious kill notification 8/8 through the REAL
path (TaskStop tool → stop_task → impl.kill = LocalShellTask.kill). My prior
mark lived only in stop_background_bash, which stop_task doesn't call — so
nothing marked killed/notified before the reaper, which sent
"failed with exit code -15".

Fix (the critic's exact direction): move the status='killed' + notified=True
mutation into LocalShellTask.kill, BEFORE os.killpg, gated on the process still
being alive (TS killTask's status==='running' gate, killShellTasks.ts:38-44).
This is the production kill path (TaskStop → stop_task → this), so the mark
lives here; marking BEFORE the signal closes the reaper-vs-killer race (the
reaper is blocked in proc.wait() and can't wake until the signal lands, by
which point notified=True is committed → the reaper's enqueue no-ops). The
stop_background_bash mark stays as defense-in-depth, also moved before its
killpg. _patch preserves a 'killed' status.

Test rewritten to drive the REAL path: test_stop_task_kill_suppresses_notification
runs stop_task (what TaskStop uses) on a real bg task → asserts status=killed +
notified + NO notification (the critic's repro, now 0). + a direct
stop_background_bash defense-in-depth test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
singlaamitesh pushed a commit to singlaamitesh/clawcodex that referenced this pull request Jul 7, 2026
singlaamitesh pushed a commit to singlaamitesh/clawcodex that referenced this pull request Jul 7, 2026
… hyperlinks + frame metrics + thinking widget + per-tool permission specialization + output styles frontmatter + placeholder Resume/Doctor screens

Bundle of standalone module additions across ch13 phases 4-12 that
share no integration surface with each other and don't touch
``app.py``, ``screens/repl.py``, ``agent_bridge.py``, or
``agent_loop.py``. Each module ships with unit tests; cross-cutting
production wiring (where present) lives in a follow-up PR so this
change merges cleanly into ``main`` regardless of order.

Phase 4 vim foundation (gap agentforce314#4 wave-1+2):
- ``src/tui/vim_buffer.py`` — multi-line ``VimBuffer`` + ``Cursor`` + ``Range``
- ``src/tui/vim_text_objects.py`` — ``find_text_object`` for ``iw``/``aw``/``ip``/``ap``/quotes/braces
- ``src/tui/vim_operators.py`` — ``parse_operator_motion`` (``d2w``, ``ciw``, ``y$``); operator dispatch d/c/y/>/<
- ``src/tui/vim_visual.py`` — Visual / Visual-Line / Visual-Block selection model
- ``src/tui/vim_search.py`` — ``/`` / ``?`` regex search with ``n``/``N`` repeat
- 70 tests (``test_vim_multiline.py`` + ``test_vim_wave2.py``)

Phase 5 transcript search module (gap agentforce314#2):
- ``src/tui/widgets/transcript_search.py`` — ``TranscriptSearch`` ModalScreen + ``find_matches`` helper
- 16 tests (``test_transcript_search.py``)

Phase 6 IME declared cursor module (gap agentforce314#3):
- ``src/tui/declared_cursor.py`` — refcounted-set registry + CSI emission
- 16 tests (``test_declared_cursor.py``)

Phase 7 specialized permission dialogs (gap agentforce314#8):
- ``src/tui/screens/permission_modal.py`` ``_TOOL_RENDERERS`` dispatcher + per-tool renderers (Bash, Edit, Write, Read)
- 16 tests (``test_permission_modal_specialization.py``)

Phase 8 placeholder Resume/Doctor screens (gap agentforce314#9):
- ``src/tui/screens/resume_conversation.py`` — placeholder with empty-state until persistence wiring lands
- ``src/tui/screens/doctor.py`` — read-only diagnostics (env/hyperlinks/frame-metrics/storage)
- 6 tests (``test_resume_doctor_screens.py``)

Phase 9 output styles frontmatter (gap agentforce314#7):
- ``src/outputStyles/{loader,styles}.py`` — YAML frontmatter parsing via existing ``parse_frontmatter``; auto-discovery of ``~/.claude/outputStyles/``
- 11 tests (``test_output_styles_frontmatter.py``) + parity test (``test_output_styles_parity.py``)

Phase 10 OSC 8 hyperlinks (gap agentforce314#15):
- ``src/tui/hyperlinks.py`` — capability detection (FORCE_HYPERLINK / TERM_PROGRAM / VTE_VERSION / truecolor)
- ``src/tui/widgets/messages/tool_result.py`` wraps file paths with the negative-lookbehind regex (avoids eating sentence punctuation)
- 38 tests (``test_hyperlinks.py`` + ``test_tool_result_hyperlinks.py``)

Phase 11 frame-event observability (gap agentforce314#10):
- ``src/tui/frame_metrics.py`` — ``FrameEvent`` shape + ``register_frame_observer`` + no-op fast path when ``CLAWCODEX_DEBUG_REPAINTS`` unset
- 17 tests (``test_frame_metrics.py``)

Phase 12 assistant-thinking widget (gap agentforce314#16 sub-item):
- ``src/tui/widgets/messages/assistant_thinking.py`` — italic-dim styled distinct row; redacted variant
- ``src/tui/widgets/transcript_view.py`` adds ``append_thinking_chunk``/``append_thinking`` shells with symmetric guard for ``append_assistant_chunk`` (Critic-flagged: prevents future thinking↔assistant mis-routing once dispatch wiring lands)
- 11 tests (``test_assistant_thinking.py`` + ``test_transcript_thinking_transitions.py``)

Test surface: 521 cumulative tests pass (``tests/tui/`` + ``tests/parity/test_output_styles_parity.py``).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
singlaamitesh pushed a commit to singlaamitesh/clawcodex that referenced this pull request Jul 7, 2026
…ync I/O

Six blocking issues identified in the post-rebase Critic review:

1. Thread auth_provider through connect_to_server / manager BEFORE
   client.connect, so the NeedsAuth fast-path + auth-header injection
   take effect on the first attempt. Previously wired after-the-fact in
   connection_manager, making OAuth inert through the agent boot path.

2. Enforce HTTPS on authServerMetadataUrl escape hatch in
   discover_oauth_metadata — RFC 8414 §2 mandates TLS, and an http://
   override from a project-scoped .mcp.json would let an attacker steal
   the access_token. Mirrors TS auth.ts:332-334.

3. html.escape the reason text in oauth_callback_server's error body.
   Previously reflected attacker-controllable OAuth error params and
   path segments verbatim — a malicious redirect could execute JS in
   the user's browser against http://localhost:PORT/callback.

4. Use http://localhost:PORT/callback (not 127.0.0.1) for redirect_uri
   in auth_provider + xaa_idp_login. Real OAuth providers match the
   redirect_uri string literally; sending 127.0.0.1 when a provider has
   localhost on file produces redirect_uri_mismatch. Plan A5 + TS.

5. Replace synchronous urllib.urlopen with httpx.AsyncClient in auth.py
   exchange_code / refresh_token. urlopen blocks the event loop for the
   whole round-trip, stalling the OAuth callback listener that is
   waiting on the same loop. Plumbed normalize_oauth_error_body so
   Slack-style 200+error responses raise rather than store garbage.

6. (Major, paired with agentforce314#2): toggle_mcp_server now runs flip + reconnect
   under one lock acquisition — previously released the lock between
   the flip and the reconnect, leaving a race window.

15 regression tests added (tests/test_mcp_critic_blockers.py) covering
each fix concretely. Full MCP suite: 307 passed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
singlaamitesh pushed a commit to singlaamitesh/clawcodex that referenced this pull request Jul 7, 2026
…e-2-bootstrap

ch03 state agentforce314#2: bootstrap state core expansion
singlaamitesh pushed a commit to singlaamitesh/clawcodex that referenced this pull request Jul 7, 2026
…14#1, agentforce314#2)

- /workflows modal (src/tui/screens/workflow_dialog.py): a list of runs and a
  per-run phases->agents detail view; `x` stops a run/agent, `r` retries an
  agent (wired to kill/skip/retry_workflow_agent). Pure render helper
  format_workflow_detail is unit-tested; the screens via the Textual pilot.
- /workflows command wired into the TUI (LOCAL_BUILTINS + app opener).
- StatusLine shows "N background workflows" from runtime_tasks (live pill).

Remaining: `p` (pause) / `s` (save) affordances — see port-plan §10.
Full workflow + TUI suites green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
singlaamitesh pushed a commit to singlaamitesh/clawcodex that referenced this pull request Jul 7, 2026
…#601)

The round-4 LLM memory-relevance recall (agentforce314#594) ships default-off because
flipping it on is a latency+cost+correctness regression. This fixes the
four blockers the round-4 critic flagged. Still default-off — zero
behavior change on merge.

- agentforce314#2 selector cost (provider-gated pin): the recall side-query ran on the
  SESSION model (an Opus turn paid full price every turn). TS pins it to a
  small default (getDefaultSonnetModel). The port wires the small_fast_model
  setting, but its shipped default is an Anthropic id
  (claude-3-5-haiku-20241022) valid only on the first-party Anthropic
  endpoint. So _resolve_recall_model(provider) applies the pin ONLY when the
  session provider is a first-party AnthropicProvider; Minimax (Anthropic
  SDK, own endpoint), DeepSeek, OpenAI, OpenRouter fall back to the session
  model. Without the gate the Anthropic default id 400s on a non-Anthropic
  endpoint and — since recall swallows errors — silently kills recall every
  turn (critic M1). small_fast_model_provider pairing for non-Anthropic
  cheap-recall is deferred.
- agentforce314#3 de-dup reset on compaction: the recall reminder is ephemeral but
  _memory_surfaced was monotonic — once surfaced, a memory was never
  re-surfaced. After /compact drops the earlier context, the memory stayed
  suppressed. _do_compact now clears _memory_surfaced on success (idle-gated
  — no race with the turn worker).
- agentforce314#4 aggregate turn byte-cap: per-file was 4 KB but 5 selections could
  inject ~20 KB/turn. build_relevant_memory_reminder_with_paths caps at
  MAX_TOTAL_SURFACE_CHARS=12 KB and returns the paths ACTUALLY surfaced;
  get_relevant_memory_reminder de-dups only those, so a cap-trimmed memory
  stays eligible (the old mark-all was a latent suppress-forever bug the cap
  would have activated). Back-compat string wrapper retained.
- agentforce314#5 docstring: _maybe_recall_memories said "prepend"; the code appends
  after the user turn (correct — normalize_messages merges consecutive user
  messages). Fixed.

Deferred agentforce314#1 (synchronous side-query): the port has only this one prefetch,
so there is no concurrent prefetch to overlap without a larger
prompt-submit refactor — one small-model call per turn (fast Haiku on the
default path). Documented.

Docs: my-docs/port-improvement-round-5/r5-1-ch11-memory-enable-blockers.md
+ critic verdicts (REVISE->APPROVE).

Tests: tests/test_r5_ch11_memory_enable_blockers.py (8) incl. the M1
regression guard (non-Anthropic never pins even when the Anthropic-default
id is set). Existing memory suite (466) green via the back-compat wrapper.
Full suite at the pre-existing baseline.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
singlaamitesh pushed a commit to singlaamitesh/clawcodex that referenced this pull request Jul 7, 2026
…rce314#606)

LocalAgentTaskState.abort_event was declared (local_agent.py:77) but NEVER
assigned — only read — so kill_async_agent's abort_event.set() was guarded
out and killing a background Agent only flipped the registry status to
"killed" while the live run kept going to NATURAL completion (burning model
tokens). Directly counter to "run long coding tasks reliably": a user who
stops a runaway background agent expects it to stop.

The async run already had an abort mechanism the port never exposed:
run_agent mints a fresh AbortController for async agents (run_agent.py:291),
wires it into the subagent context, and query() polls signal.aborted at
every yield point (query.py:1433/1520/1571/2046). This makes it REACHABLE:
- LocalAgentTaskState.abort_controller field + register_async_agent param.
- _launch_async_agent mints a dedicated AbortController, stores it on the
  task state AND sets run_params.abort_controller, so run_agent uses THIS
  one (run_agent.py:286-287) instead of its unreachable internal one. FRESH
  (not the parent turn's) → preserves async isolation.
- kill_async_agent calls abort_controller.abort("killed by user") → the
  run's query() loop halts at the next poll.

Cross-thread safe: the kill runs on the kill thread, the run polls on the
background loop; AbortController.abort() is a GIL-atomic flag set + sync
listeners and signal.aborted is an atomic read — the same cross-thread
pattern the turn-interrupt path already uses. .abort() is placed outside
the registry lock (matches the abort_event.set() rationale). Legacy
abort_event.set() retained (harmless defense).

Docs: my-docs/port-improvement-round-6/r6-1-kill-background-agent.md +
critic verdict (APPROVE).

Tests: tests/test_r6_kill_background_agent.py (4): unit (kill aborts the
stored controller + flips killed; kill w/o controller is safe); END-TO-END
(a long-running bg agent that polls the abort signal STOPS advancing after a
cross-thread kill — NON-VACUOUS: neg-control advances ~34 steps post-kill
without the fix, <=2 with it) + two ISOLATION assertions (killing the bg
agent doesn't abort the parent; aborting the parent doesn't stop the bg
agent). 1812 agent/async/task tests green; full suite at the 9-failure
baseline.

Deferred (critic minor agentforce314#2): the resume path must mirror this wire when it
gains a live run loop, else resumed agents are un-killable (benign today —
resume doesn't drive a run).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
singlaamitesh pushed a commit to singlaamitesh/clawcodex that referenced this pull request Jul 7, 2026
The expand-result feature (agentforce314#623) delegated verbose-block truncation to
boundedLiveRenderText, which keeps the TAIL with a 'showing live tail'
label — correct for the live stream, wrong for a persisted ctrl+o
expansion where the reader wants the START of Grep matches / Bash output
/ an error. The comment had also gone stale (claimed 'NOT the 16KB
budget' when the cap is now exactly 16k, and 'expanded by default' when
it's behind ctrl+o).

- New headTruncate: keeps the first VERBOSE_TRAIL_MAX_LINES/_CHARS with a
  trailing '… +N lines/chars omitted' marker (no live-tail label).
- verboseToolBlock uses it; comment corrected to match the behavior.
- boundedLiveRenderText stays for the genuine live-stream tails
  (streaming assistant text, thinking) — unchanged.

Test: a 500-line Grep result keeps line 0-199, drops line 200 and the
499 tail, shows '… +N lines omitted', and no longer says 'showing live
tail' — distinguishing head from tail (the old 'A'.repeat test couldn't).

Reconciles the plan, the comment, the label, and the code (critic
Finding B, required change agentforce314#2 from the expand-E implementation review —
missed in the first fix round, which only closed Finding A).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ericleepi314 added a commit that referenced this pull request Jul 10, 2026
…ckground band (#691)

Past user prompts rendered as bare '❯ text' — visually identical in weight
to assistant prose, so earlier inputs were hard to find when scrolling a
multi-turn transcript. The original Claude Code draws every past user input
(and slash echo) on a full-row background band; port that:

- theme.ts: new userMessageBackground token — dark rgb(55,55,55) / light
  rgb(240,240,240), the exact original utils/theme.ts values; skinnable via
  user_message_bg with a DEFAULT_THEME fallback.
- messageLine.tsx: transcriptRowBand(msg, t) paints the inner row Box for
  user rows and slash echoes (UserPromptMessage.tsx:76 /
  UserCommandMessage.tsx:62 parity). Slash echoes borrow the user pointer
  and text color instead of the muted system dot, and the user glyph drops
  its non-original bold — the band, not bold text, carries the emphasis
  (HighlightedThinkingText renders the pointer un-bolded).
- appLayout/useMainApp/virtualHeights: the '───' inter-turn dash above
  non-first user rows becomes a monochrome-only fallback. It existed as a
  crutch for exactly this findability problem; with color available the
  band replaces it. On NO_COLOR / FORCE_COLOR=0 / TERM=dumb terminals the
  band emits nothing (chalk level 0), so the textual separator returns —
  gated by domain/blockLayout.ts::showsInterTurnSeparator fed from
  config/env.ts::TRANSCRIPT_COLOR (process.stdout.hasColors), with the
  render gate and the height estimator sharing the same predicate.
  [Codex adversarial-review catch: the first cut removed the separator
  unconditionally, leaving no-color terminals with no turn boundary.]
- tests: pin band routing (transcriptRowBand), slash pointer swap, theme
  values incl. skin fallback/override, separator gate matrix, band-mode
  same-height + monochrome +2 heights; refresh the stale compound-prompt
  expectation (glyph is the fixed '❯' since the re-theme; the compound
  brand prompt only widens the gutter).

- theme.ts selectionBg: ported the original selection blues — dark
  rgb(38,79,120) / light rgb(180,213,255) (utils/theme.ts). The previous
  dark #373737 was the exact band color, so selecting text on a past user
  row painted band-on-band and vanished. [Critic catch #2.]

- diffView.ts structuredDiffSupported(): gate the raw-ANSI ColorDiff path
  on TRANSCRIPT_COLOR as well as NO_COLOR. ColorDiff builds SGRs itself
  (not through chalk), so a FORCE_COLOR=0 / TERM=dumb session previously
  kept colored diff blocks inside an otherwise monochrome transcript; it
  now falls back to the chalk-suppressed markdown ```diff path on the
  same signals as transcript chrome. [Codex verification-pass catch.]

Verified with the pyte harness (fake NDJSON agent-server): banded rows carry
bg 373737 across the row for single-line, multi-line (both lines + indent),
and slash echoes; assistant/system rows and the composer stay bandless; with
NO_COLOR=1 the '───' separator renders above non-first user turns.
ui-tui vitest: 8 failed vs 9 on main baseline (one stale test fixed, no new
failures); tsc + eslint clean.

- lib/forceTruecolor.ts: the bundled chalk predates NO_COLOR support (it
  emitted full color regardless — verified level 2 on a PTY with NO_COLOR=1),
  so the pre-chalk bootstrap now translates NO_COLOR into FORCE_COLOR=0, the
  channel chalk does honor, unless the user set FORCE_COLOR explicitly. This
  keeps TRANSCRIPT_COLOR (stdout.hasColors, which honors NO_COLOR) agreeing
  with what the renderer emits — critic caught the divergence: NO_COLOR
  previously showed the band AND the fallback separator. diffView already
  gated on NO_COLOR, so the app is now consistent.
  [Critic catch: hasColors vs chalk signal divergence under NO_COLOR.]

Known pre-existing (NOT introduced here, repro'd on the unmodified build):
the inline growth repaint can leave stale right-edge cells from shifted
footer/rule rows (e.g. a stray '─' at 100x30 after 3 turns on main).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ericleepi314 added a commit that referenced this pull request Jul 21, 2026
The round-4 LLM memory-relevance recall (#594) ships default-off because
flipping it on is a latency+cost+correctness regression. This fixes the
four blockers the round-4 critic flagged. Still default-off — zero
behavior change on merge.

- #2 selector cost (provider-gated pin): the recall side-query ran on the
  SESSION model (an Opus turn paid full price every turn). TS pins it to a
  small default (getDefaultSonnetModel). The port wires the small_fast_model
  setting, but its shipped default is an Anthropic id
  (claude-3-5-haiku-20241022) valid only on the first-party Anthropic
  endpoint. So _resolve_recall_model(provider) applies the pin ONLY when the
  session provider is a first-party AnthropicProvider; Minimax (Anthropic
  SDK, own endpoint), DeepSeek, OpenAI, OpenRouter fall back to the session
  model. Without the gate the Anthropic default id 400s on a non-Anthropic
  endpoint and — since recall swallows errors — silently kills recall every
  turn (critic M1). small_fast_model_provider pairing for non-Anthropic
  cheap-recall is deferred.
- #3 de-dup reset on compaction: the recall reminder is ephemeral but
  _memory_surfaced was monotonic — once surfaced, a memory was never
  re-surfaced. After /compact drops the earlier context, the memory stayed
  suppressed. _do_compact now clears _memory_surfaced on success (idle-gated
  — no race with the turn worker).
- #4 aggregate turn byte-cap: per-file was 4 KB but 5 selections could
  inject ~20 KB/turn. build_relevant_memory_reminder_with_paths caps at
  MAX_TOTAL_SURFACE_CHARS=12 KB and returns the paths ACTUALLY surfaced;
  get_relevant_memory_reminder de-dups only those, so a cap-trimmed memory
  stays eligible (the old mark-all was a latent suppress-forever bug the cap
  would have activated). Back-compat string wrapper retained.
- #5 docstring: _maybe_recall_memories said "prepend"; the code appends
  after the user turn (correct — normalize_messages merges consecutive user
  messages). Fixed.

Deferred #1 (synchronous side-query): the port has only this one prefetch,
so there is no concurrent prefetch to overlap without a larger
prompt-submit refactor — one small-model call per turn (fast Haiku on the
default path). Documented.

Docs: my-docs/port-improvement-round-5/r5-1-ch11-memory-enable-blockers.md
+ critic verdicts (REVISE->APPROVE).

Tests: tests/test_r5_ch11_memory_enable_blockers.py (8) incl. the M1
regression guard (non-Anthropic never pins even when the Anthropic-default
id is set). Existing memory suite (466) green via the back-compat wrapper.
Full suite at the pre-existing baseline.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ericleepi314 added a commit that referenced this pull request Jul 21, 2026
LocalAgentTaskState.abort_event was declared (local_agent.py:77) but NEVER
assigned — only read — so kill_async_agent's abort_event.set() was guarded
out and killing a background Agent only flipped the registry status to
"killed" while the live run kept going to NATURAL completion (burning model
tokens). Directly counter to "run long coding tasks reliably": a user who
stops a runaway background agent expects it to stop.

The async run already had an abort mechanism the port never exposed:
run_agent mints a fresh AbortController for async agents (run_agent.py:291),
wires it into the subagent context, and query() polls signal.aborted at
every yield point (query.py:1433/1520/1571/2046). This makes it REACHABLE:
- LocalAgentTaskState.abort_controller field + register_async_agent param.
- _launch_async_agent mints a dedicated AbortController, stores it on the
  task state AND sets run_params.abort_controller, so run_agent uses THIS
  one (run_agent.py:286-287) instead of its unreachable internal one. FRESH
  (not the parent turn's) → preserves async isolation.
- kill_async_agent calls abort_controller.abort("killed by user") → the
  run's query() loop halts at the next poll.

Cross-thread safe: the kill runs on the kill thread, the run polls on the
background loop; AbortController.abort() is a GIL-atomic flag set + sync
listeners and signal.aborted is an atomic read — the same cross-thread
pattern the turn-interrupt path already uses. .abort() is placed outside
the registry lock (matches the abort_event.set() rationale). Legacy
abort_event.set() retained (harmless defense).

Docs: my-docs/port-improvement-round-6/r6-1-kill-background-agent.md +
critic verdict (APPROVE).

Tests: tests/test_r6_kill_background_agent.py (4): unit (kill aborts the
stored controller + flips killed; kill w/o controller is safe); END-TO-END
(a long-running bg agent that polls the abort signal STOPS advancing after a
cross-thread kill — NON-VACUOUS: neg-control advances ~34 steps post-kill
without the fix, <=2 with it) + two ISOLATION assertions (killing the bg
agent doesn't abort the parent; aborting the parent doesn't stop the bg
agent). 1812 agent/async/task tests green; full suite at the 9-failure
baseline.

Deferred (critic minor #2): the resume path must mirror this wire when it
gains a live run loop, else resumed agents are un-killable (benign today —
resume doesn't drive a run).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ericleepi314 added a commit that referenced this pull request Jul 21, 2026
The expand-result feature (#623) delegated verbose-block truncation to
boundedLiveRenderText, which keeps the TAIL with a 'showing live tail'
label — correct for the live stream, wrong for a persisted ctrl+o
expansion where the reader wants the START of Grep matches / Bash output
/ an error. The comment had also gone stale (claimed 'NOT the 16KB
budget' when the cap is now exactly 16k, and 'expanded by default' when
it's behind ctrl+o).

- New headTruncate: keeps the first VERBOSE_TRAIL_MAX_LINES/_CHARS with a
  trailing '… +N lines/chars omitted' marker (no live-tail label).
- verboseToolBlock uses it; comment corrected to match the behavior.
- boundedLiveRenderText stays for the genuine live-stream tails
  (streaming assistant text, thinking) — unchanged.

Test: a 500-line Grep result keeps line 0-199, drops line 200 and the
499 tail, shows '… +N lines omitted', and no longer says 'showing live
tail' — distinguishing head from tail (the old 'A'.repeat test couldn't).

Reconciles the plan, the comment, the label, and the code (critic
Finding B, required change #2 from the expand-E implementation review —
missed in the first fix round, which only closed Finding A).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ericleepi314 added a commit that referenced this pull request Jul 21, 2026
…S-2) (#645)

* feat(services): autoFix config + hook + runner (SERVICES-2 W1-W3; wiring next)

Port of typescript/src/services/autoFix/ — the settings-opt-in feature that
runs the user's lint/test command after a file edit and injects
<auto_fix_feedback> so the model self-fixes. Python had only the /autofix
slash shell; the runtime was absent.

- config.py (autoFixConfig.ts): get_auto_fix_config(raw) → AutoFixConfig|None
  — dict + enabled + at-least-one-of-lint/test (the zod .refine) + numeric
  bounds (maxRetries 0..10 default 3, timeout 1000..300000 default 30000);
  None (safeParse-null posture) on any failure. load_auto_fix_config reads
  settings.extra["autoFix"] (verified: no typed field — lands in extra, like
  the plugins round's enabledPlugins).
- hook.py (autoFixHook.ts): AUTO_FIX_TOOLS = the port's file-mutation
  registry names (Write/Edit/MultiEdit/NotebookEdit, the file_edit/file_write
  analog); should_run_auto_fix; build_auto_fix_context (the <auto_fix_feedback>
  block VERBATIM) + build_max_retries_context (verbatim).
- runner.py (autoFixRunner.ts): run_auto_fix_check — lint first, test only if
  lint passed; each an asyncio.create_subprocess_shell bounded by timeout,
  killing the process GROUP on timeout (start_new_session + os.killpg, the
  detached/killTree analog) and reaping to avoid zombies; _build_error_summary
  verbatim; never-raises (spawn failure → has_errors=False).

Verified: config parse/refine/bounds, hook fire + verbatim context, runner
lint-fail-skips-test / lint-pass-test-fail / both-clean / timeout-kill
(sleep 30 @ 800ms → returns 0.81s, group killed, timed_out) / abort.

REMAINING: W4 wiring — a NEW sibling run_auto_fix_step at the orchestrator
(tool_execution.py, after run_post_tool_use_hooks), NOT inside that function
(it early-returns when no user PostToolUse hooks configured — the
teammate-hooks split-gate class; autoFix must run regardless). Retry cap by
query_tracking.chain_id. Then tests + suite. Awaiting autofix-critic on the
plan's W4 placement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(services): autoFix wiring + all critic findings (SERVICES-2 complete)

The autofix-critic caught that TS autoFix is DEAD CODE — AUTO_FIX_TOOLS =
{'file_edit','file_write'} never matches the real tool names (Edit/Write,
FileEditTool/constants.ts:2 FILE_EDIT_TOOL_NAME='Edit'), verified
first-hand. So this port activates the author's INTENT ({Edit,Write}) as a
DOCUMENTED, opt-in-gated DIVERGENCE (fixes the reference's dead tool-name
set; inert unless a user writes settings.autoFix.enabled).

All findings:
- B1 (split-gate): run_auto_fix_step is a NEW SIBLING at the orchestrator
  (tool_execution.py, after run_post_tool_use_hooks) — NOT inside that
  function, which early-returns when no user PostToolUse hook is configured
  (the common autoFix case). Pinned by test_fires_with_zero_posttool_hooks.
- B2 (accessor): config via get_settings().extra.get("autoFix") (ToolContext
  has no settings; global read = the per-process equivalent).
- M1 (reject-not-clamp): out-of-range/non-int → whole config None (zod
  .min/.max rejects); .default only when key absent.
- M2 (camelCase): reads raw["maxRetries"]/raw["timeout"].
- M3 (reset-on-success): the retry counter is DELETED on a clean run
  (toolHooks.ts:247), not just incremented on error.
- M4 (tool set): {"Edit","Write"} — the author's intent, not the 4-element
  _FILE_EDIT_TOOLS (NotebookEdit excluded, MultiEdit is a phantom).
- M5 (None-guard): chain key handles query_tracking=None → "default".
- M6 (10k cap): each stream sliced to 10 000 before combine.
- minors: SIGTERM (not SIGKILL) + reap + ProcessLookupError race; mid-flight
  abort (kills the group, degrades to no-errors); cwd from tool_use_context;
  the post-hook attachment yield shape (list-wrapped content).

tests/services/test_autofix.py (26): config parse/refine/reject/camelCase,
hook fire + verbatim contexts + {Edit,Write} intent, runner (lint-skips-test
/ test-fail / clean / timeout-SIGTERM-kill / 10k cap / pre-set abort /
mid-flight abort / no-commands), step (ZERO-hooks regression, non-file skip,
retry cap, reset-on-clean, None-query-tracking). Full suite at the 6-failure
baseline (7857 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(services): autoFix config zod-strictness (critic minor #1)

enabled must be a strict bool (a string "false" is truthy in Python — must
not turn autoFix ON against intent); a bad-type lint/test rejects the whole
config → None, matching zod z.boolean()/z.string() safeParse (not the prior
silent coerce/drop). 3 new tests; consistent with the already-strict int
path. (Minors #2 retry-map-lifecycle and #3 proxy-coverage accepted as
faithful-to-TS / adequate.) Full suite at the 6-failure baseline (7860).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ericleepi314 added a commit that referenced this pull request Jul 21, 2026
* feat(bash): notify on background-bash completion (C5 Part 1 — enqueueShellNotification)

TS notifies when a run_in_background bash task reaches a terminal state
(utils/task/framework.ts:289 enqueuePendingNotification; BashTool/prompt.ts:
285-287 "you will be notified when it completes — do not poll"). The port
marked the terminal state notified=True WITHOUT sending — the documented
forward-trap in background.py (ch10 WI-2), so a bg bash finishing while the
model was away went unannounced (silently polled via TaskOutput instead).

- task_notification.py: enqueue_shell_notification — mirrors
  enqueue_agent_notification (the atomic check-and-set duplicate-delivery
  guard + build_task_notification_xml + enqueue_pending_notification) but
  gated on LocalShellTaskState instead of LocalAgentTaskState.
- background.py: REMOVED the forward-trap notified=True from the terminal
  replace() (per its own comment's instruction), and call
  enqueue_shell_notification after the terminal update. The eviction sweeper's
  notify-before-evict guard (eviction.py:97) now correctly keeps the entry
  until the notification is delivered, then reclaims it — instead of the old
  mark-notified-without-sending. Best-effort (never breaks reaping);
  duplicate-safe (a TaskStop that already notified makes it a no-op).

tests/test_shell_completion_notification.py (3): enqueues + marks notified on
terminal, duplicate-safe (already-notified → no second), wrong-task-type no-op.
44 background/eviction/notification tests green (the notified=False terminal
state is compatible with the eviction guard).

Part 2 (the Monitor tool + backpressure) follows separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bash): C5 Part 1 — fallback-mark-notified on enqueue failure (no leak)

Pre-empting the eviction-leak risk: enqueue_shell_notification is best-effort
(except: pass), but with the forward-trap removed the terminal state is
notified=False — so if the enqueue raised, the sweeper (which keeps un-notified
terminal tasks, eviction.py:97) would leak the task in /tasks forever. Added a
fallback: on enqueue failure, force notified=True so the task stays evictable,
degrading to the OLD evictable-but-unsent behavior rather than a leak (still
strictly better than the pre-C5 silent-drop).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bash): C5 Part 1 critic — shell-specific summary, XML escaping, kill path

The c5p1-critic found the model-facing payload was wrong (the mechanism was
right). Real TS analog: enqueueShellNotification (LocalShellTask.tsx:105-172).

#1 BLOCKING — wrong summary: reused the AGENT builder, so every bg-bash
completion was announced as `Agent "..." completed`, the exit code was dropped,
and the failed case said "Unknown error". Added build_shell_notification_xml +
_build_shell_summary + BACKGROUND_BASH_SUMMARY_PREFIX ("Background command ") —
`Background command "<desc>" completed (exit code N)` / `... failed with exit
code N` / `... was stopped`, exit code included only when known. Threaded
exit_code + tool_use_id through enqueue_shell_notification ← background.py's rc.

#2 MAJOR — no XML escaping: a bg command routinely has < > & (and _reap uses
`description or command`), so a raw summary produced a MALFORMED envelope. The
shell builder XML-escapes the summary (TS LocalShellTask.tsx:164 escapeXml);
the agent builder stays raw (LocalAgentTask.tsx:246-256 is raw — that claim was
only true for agents).

#3 MAJOR — kill path: TS killTask (killShellTasks.ts:38-44) atomically sets
status='killed' + notified=True, which SUPPRESSES the reaper notification (no
notification on a user kill). The port sent SIGTERM only → the reaper saw rc≠0
→ sent a spurious `failed`. stop_background_bash now sets status='killed' +
notified=True; _patch preserves a 'killed' status (doesn't reclassify from the
SIGTERM exit code). The duplicate-guard's "TaskStop did it" premise is now
actually wired.

#4 tests — the old tests never asserted summary CONTENT (passed with the wrong
"Agent" text). Added: full-summary snapshots (prefix + exit code + not-"Agent"),
metachar XML-escaping, a REAL spawn→reap integration (exit 3 → the "failed with
exit code 3" notification delivered), and kill-suppression (kill → status=killed
+ notified + NO notification).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bash): C5 Part 1 #3 — mark killed in LocalShellTask.kill before the signal

The c5p1-critic reproduced the spurious kill notification 8/8 through the REAL
path (TaskStop tool → stop_task → impl.kill = LocalShellTask.kill). My prior
mark lived only in stop_background_bash, which stop_task doesn't call — so
nothing marked killed/notified before the reaper, which sent
"failed with exit code -15".

Fix (the critic's exact direction): move the status='killed' + notified=True
mutation into LocalShellTask.kill, BEFORE os.killpg, gated on the process still
being alive (TS killTask's status==='running' gate, killShellTasks.ts:38-44).
This is the production kill path (TaskStop → stop_task → this), so the mark
lives here; marking BEFORE the signal closes the reaper-vs-killer race (the
reaper is blocked in proc.wait() and can't wake until the signal lands, by
which point notified=True is committed → the reaper's enqueue no-ops). The
stop_background_bash mark stays as defense-in-depth, also moved before its
killpg. _patch preserves a 'killed' status.

Test rewritten to drive the REAL path: test_stop_task_kill_suppresses_notification
runs stop_task (what TaskStop uses) on a real bg task → asserts status=killed +
notified + NO notification (the critic's repro, now 0). + a direct
stop_background_bash defense-in-depth test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ericleepi314 added a commit that referenced this pull request Jul 21, 2026
…ckground band (#691)

Past user prompts rendered as bare '❯ text' — visually identical in weight
to assistant prose, so earlier inputs were hard to find when scrolling a
multi-turn transcript. The original Claude Code draws every past user input
(and slash echo) on a full-row background band; port that:

- theme.ts: new userMessageBackground token — dark rgb(55,55,55) / light
  rgb(240,240,240), the exact original utils/theme.ts values; skinnable via
  user_message_bg with a DEFAULT_THEME fallback.
- messageLine.tsx: transcriptRowBand(msg, t) paints the inner row Box for
  user rows and slash echoes (UserPromptMessage.tsx:76 /
  UserCommandMessage.tsx:62 parity). Slash echoes borrow the user pointer
  and text color instead of the muted system dot, and the user glyph drops
  its non-original bold — the band, not bold text, carries the emphasis
  (HighlightedThinkingText renders the pointer un-bolded).
- appLayout/useMainApp/virtualHeights: the '───' inter-turn dash above
  non-first user rows becomes a monochrome-only fallback. It existed as a
  crutch for exactly this findability problem; with color available the
  band replaces it. On NO_COLOR / FORCE_COLOR=0 / TERM=dumb terminals the
  band emits nothing (chalk level 0), so the textual separator returns —
  gated by domain/blockLayout.ts::showsInterTurnSeparator fed from
  config/env.ts::TRANSCRIPT_COLOR (process.stdout.hasColors), with the
  render gate and the height estimator sharing the same predicate.
  [Codex adversarial-review catch: the first cut removed the separator
  unconditionally, leaving no-color terminals with no turn boundary.]
- tests: pin band routing (transcriptRowBand), slash pointer swap, theme
  values incl. skin fallback/override, separator gate matrix, band-mode
  same-height + monochrome +2 heights; refresh the stale compound-prompt
  expectation (glyph is the fixed '❯' since the re-theme; the compound
  brand prompt only widens the gutter).

- theme.ts selectionBg: ported the original selection blues — dark
  rgb(38,79,120) / light rgb(180,213,255) (utils/theme.ts). The previous
  dark #373737 was the exact band color, so selecting text on a past user
  row painted band-on-band and vanished. [Critic catch #2.]

- diffView.ts structuredDiffSupported(): gate the raw-ANSI ColorDiff path
  on TRANSCRIPT_COLOR as well as NO_COLOR. ColorDiff builds SGRs itself
  (not through chalk), so a FORCE_COLOR=0 / TERM=dumb session previously
  kept colored diff blocks inside an otherwise monochrome transcript; it
  now falls back to the chalk-suppressed markdown ```diff path on the
  same signals as transcript chrome. [Codex verification-pass catch.]

Verified with the pyte harness (fake NDJSON agent-server): banded rows carry
bg 373737 across the row for single-line, multi-line (both lines + indent),
and slash echoes; assistant/system rows and the composer stay bandless; with
NO_COLOR=1 the '───' separator renders above non-first user turns.
ui-tui vitest: 8 failed vs 9 on main baseline (one stale test fixed, no new
failures); tsc + eslint clean.

- lib/forceTruecolor.ts: the bundled chalk predates NO_COLOR support (it
  emitted full color regardless — verified level 2 on a PTY with NO_COLOR=1),
  so the pre-chalk bootstrap now translates NO_COLOR into FORCE_COLOR=0, the
  channel chalk does honor, unless the user set FORCE_COLOR explicitly. This
  keeps TRANSCRIPT_COLOR (stdout.hasColors, which honors NO_COLOR) agreeing
  with what the renderer emits — critic caught the divergence: NO_COLOR
  previously showed the band AND the fallback separator. diffView already
  gated on NO_COLOR, so the app is now consistent.
  [Critic catch: hasColors vs chalk signal divergence under NO_COLOR.]

Known pre-existing (NOT introduced here, repro'd on the unmodified build):
the inline growth repaint can leave stale right-edge cells from shifted
footer/rule rows (e.g. a stray '─' at 100x30 after 3 turns on main).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
peroxider pushed a commit to peroxider/clawcodex that referenced this pull request Jul 21, 2026
feat(SOP):resource catalog,composite tools与SDK隔离实现

Created-by: gkawuiq8
Commit-by: gkawuiq8;root
Merged-by: chadwweng
Description: ## 背景

  SOP Converter 将 SDK 接口转换为独立 Tool 后,创建型工具产生的内存对象无法跨 Tool 调用复用,导致“创建 Agent → 按 ID 调用”的生命周期链路断裂。同时,复合宏工具与 SDK 原子工具存在语义竞争,Agent 即使能够执行宏,也不一定能稳定检索并选择宏。

  本 PR 串联 F-55、F-56、F-57 和 F-157,补齐从资源创建、持久化、恢复、复合执行到 ToolSearch 分层选择的完整链路。
  
## 主要改动
### F-55:补齐工具生命周期依赖
让「创建 → 保存 → 再调用」变成系统认的固定套路,而不是靠模型自己猜下一步该调哪个工具。
  - 为 create/invoke 工具建立 `resource_type`、`produces`、`consumes` 和 `resource_ref` 契约。
  - 创建成功后写入可恢复记录,并通过 `created_persisted` 等稳定字段区分创建与持久化状态。
  - 为 invoke/run 工具增加 catalog fallback,资源不在当前进程时可自动查找并恢复。
  - 生成 `.clawcodex/tool-dependencies.yaml`,记录 create → persist → materialize → invoke 依赖链。
  - 将生命周期依赖写入 Skill frontmatter、Task Guide 和系统提示。
  - ToolSearch 接入生命周期排序与 `lifecycle-chain:` 查询,减少相似 SDK 工具之间的误选和空转。

 ### F-56:实现通用 SOP Resource Catalog
  给「创建出来的 Agent / 资源」做一个可跨会话复用的通讯录——按名字或 ID 能查到,敏感信息只记环境变量引用,不落明文 Key。

  - 新增 `ResourceRecord`、`ResourceCatalog` 和 `CatalogExecutionContext`。
  - 支持 bundle-local/user-local catalog、原子写入、幂等 upsert 和 legacy AgentCatalog 兼容。
  - 敏感配置仅保存 `env:` 引用,避免 API Key 明文落盘。
  - 新增 `ResourceHandler` 注册表,以 `resource_type` 作为扩展点;Agent 是首个生产级实现。
  - 支持按 ID、名称和 alias 查找资源,歧义时拒绝猜测。
  - 实现 Agent 的 materialize/invoke 恢复路径,并统一缺失、歧义、secret 缺失、版本不兼容等错误码。
  - 支持 `.clawcodex/resources.yaml` sidecar 显式覆盖资源类型和句柄字段。

 ### F-57:可执行复合工作流 + 手写宏
 把「多步流水线」做成真正可调用的一种 Tool(`workflow`),并支持你在仓库里手写 YAML 宏(例如文本/图像/多模态处理流水线);Agent 一次调用宏,系统按步骤顺序跑完,而不是让模型自己拼一串原子工具。

#### 复合工作流运行时

- `workflow` 成为一等 Tool 调用类型,由主进程 `ToolRegistry` 调度。
- 支持顺序 step、输入输出绑定、资源绑定、递归深度限制、延迟工具激活,以及结构化 `trace`(哪一步成功/失败一目了然)。
- trusted private lane:Agent 实例等不能 JSON 化的对象不进入公开 Tool 输出。
- 内置宏 `invoke-existing-agent` 固定三步:`load_agent_record → materialize_agent → invoke_agent`(对应「从目录取出 → 在本进程恢复 → 真正调用」)。
- 完善 output schema、类型重建与 JSON-safe 输出,保证宏返回值可被后续步骤和下一次 Tool 消费。

#### 手写宏

除了 convert 自动生成的工具,你还可以在 `sop-macros/` 里手写「业务宏」YAML;convert 时装进 bundle,运行时能被 ToolSearch 召回并一键执行。

- 支持从源码树 `sop-macros/`、`--macros-dir` 或 `--macro-manifest` 加载手写宏定义。
- convert 阶段完成 schema 校验、原子写入 bundle(如 `.clawcodex/macros/`),并注册为 `call_type=workflow` 的可调工具。
- 宏可声明对外入参(如 `input_path` / `output_path`)、内部 step 编排(如调用 `execute-pipeline`),以及对外输出契约(如非空 `session_id` + `summary`)。
- 与 MacroCatalog / MacroRoute 打通:自然语言或 `select:宏名` 可稳定命中手写宏,而不是落到裸原子工具。
- 验收侧典型路径:AscendDataForge 文本/图像/多模态手写宏 → ToolSearch 命中 → 一次 `tool_use` 跑完整流水线 → 产物落盘。

 ### F-157:实现宏工具/原子工具分层检索
 搜索工具时「能走整包宏就不要拆零件」——有合适宏就优先(甚至独占)展示宏;宏不可用时再把原子工具放回来,避免 Agent 被一堆相似小工具带偏。

- 宏路由增加 `intent_key`、`covered_tools`、`unavailable_policy`,标明「这个宏覆盖了哪些原子工具」。
- `sop convert` 生成 `.clawcodex/tool-retrieval.yaml`,显式记录 macro / atomic / neutral 层级与覆盖关系。
- `RetrievalPlan` 支持 verified exclusive、prefer、普通检索三种策略。
- exclusive 命中且预检通过后,从搜索结果和当前可用工具里隐藏被覆盖的原子工具。
- 宏预检失败时,同一次 ToolSearch 内撤销隐藏并恢复原子候选,避免「宏挂了又搜不到退路」。
- 若仍去调已被宏盖住的陈旧原子工具,返回 `tool_shadowed_by_macro`,引导改调推荐宏。
- 评分接近时优先同意图宏,同时保留「用户精确点名原子工具」时的优先级。
- ToolSearch 结果带上 intent、selection、被抑制工具、preflight、reason codes,方便对照验收。

 ### SDK 依赖隔离与转换健壮性
 convert 时给每个 bundle 准备独立 venv 并装好 SDK 依赖;真正跑 Tool 时不切换进程解释器(避免把 Agent/REPL 进程 exec 掉),而是把 bundle 的 site-packages 挂进当前进程,形成「软隔离」。
  - 为转换后的 bundle 建立独立虚拟环境并解析 SDK requirements。
  - 支持 Windows/WSL 路径、模块导入、wrapper 重入和 bundle 上下文传播。
  - 完善 CLI handler、异步接口、复杂参数 schema 和 SDK 类型恢复。

 ## 核心链路

  ```text
  create resource
    → ResourceCatalog upsert
    → ToolSearch 召回复合宏
    → catalog lookup
    → materialize
    → invoke
    → 返回 JSON-safe output + workflow trace
  ```

  ## 验证

  聚焦回归结果:

  164 passed, 8 subtests passed

  覆盖范围包括:

  - F-55 生命周期排序、catalog fallback 和 create → invoke E2E。
  - F-56 catalog、secret 引用、资源类型扩展及 Agent 重建。
  - F-57 workflow dispatch、宏装载、参数绑定、trace 和 output schema。
  - F-157 exclusive suppression、preflight 回滚、shadow guard、结构偏置和索引校验。

  手工验证已确认 Agent 创建后写入 catalog,以及 invoke-existing-agent 完成
  load → materialize → invoke 三步调用。F-157 自然语言选择效果仍需按手工验收清单复验。

## 兼容性与边界

  - 保留 legacy AgentCatalog 双写及读取 fallback。
  - 历史未分类工具默认为 neutral,不会因名称相似被隐藏。
  - 当前生产级资源恢复保证以 Agent 为主;第二种真实 SDK 资源和通用 resume-resource 宏不在本 PR 范围。
  - Session 宏注册、trace-to-macro 和宏提升属于后续可选能力。
  - 外部 Ray/worker 是否使用 bundle 解释器仍受目标 SDK 运行环境影响,需单独进行端到端验收。


## 一、F-56,F-57特性验证

会话根目录:

`\\wsl$\Ubuntu-18.04\root\.clawcodex\sessions\3efca96f-4e92-4580-9506-8f479371b255\`

落盘 catalog:

`D:\projects\clawcodex\.clawcodex\JiuwenAgent_v7.18\.clawcodex\resource-catalog.json`

### 1) F-56:创建并写入 catalog

**文件:** `subagents\agent-ark8z37bg.jsonl`

| 行号(约) | 流程 | 详情 |
|-----------|--------|----------|
| L0 | `tool_use` Skill | `core_merged-skill` |
| L4 | `tool_use` ToolSearch | `select:openjiuwen-core-application-llm-agent-create-llm-agent` |
| L7 | `tool_use` create 工具 | `openjiuwen-core-application-llm-agent-create-llm-agent`,`agent_config.id=verify-bot` |
| **L9** | **`tool_result`(核心)** | 见下表字段 |

**L9 `tool_result` 里的 F-56 字段:**

```text
created_persisted = true
resource_catalog_reason = "f56_resource_catalog"
resource_catalog_path = ".../JiuwenAgent_v7.18/.clawcodex/resource-catalog.json"
catalog_reason = "bundle-local"
agent_id = "verify-bot"
resource_ref = "verify-bot"
callable_by_agent_id / callable_by_resource_ref = true
*_call_contract = "catalog_persisted"
```
![image-89.png](https://raw.gitcode.com/user-images/assets/9901179/6dcdd193-8ec6-41a3-b1c7-e2e1ab304439/image-89.png 'image-89.png')

**落盘对照:** 打开 `resource-catalog.json`,`records` 中有 key 含 `verify-bot`,且:

- `resource_id`: `verify-bot`
- `source_tool`: `openjiuwen-core-application-llm-agent-create-llm-agent`
- `materializer` 含 `init_kwargs.agent_config`(api_key 应是 `env:DEEPSEEK_API_KEY`,不是明文)

![F-56+57测试2.PNG](https://raw.gitcode.com/user-images/assets/9901179/94cce083-a48c-4922-b2ba-93d5645a9d5f/F-56_57测试2.PNG 'F-56+57测试2.PNG')

### 2) F-57:宏召回 + catalog→materialize→invoke

**文件:** `subagents\agent-a6d96va9i.jsonl`

| 行号(约) | 流程 | 详情 |
|-----------|--------|----------|
| L0 | Skill | `core_merged-skill` |
| L4 | ToolSearch | `select:invoke-existing-agent` |
| **L6** | ToolSearch 结果 | `matches: ["invoke-existing-agent"]`(宏被召回,不是原子 `llmagent-invoke`) |
| L7 | 调宏 | `invoke-existing-agent`,入参 `agent_ref=verify-bot`, `query=ping` |
| **L9** | **宏结果(核心)** | workflow `steps` 三步全 success |

![image-88.png](https://raw.gitcode.com/user-images/assets/9901179/9755b6b5-217a-4e9d-b273-ccf2f669dfde/image-88.png 'image-88.png')

![image-90.png](https://raw.gitcode.com/user-images/assets/9901179/1cad3ab1-e03e-4b6b-8412-c770f0cb1470/image-90.png 'image-90.png')

![image-91.png](https://raw.gitcode.com/user-images/assets/9901179/2dfc8fca-9b59-4d8e-bd89-bc0ab4ccd686/image-91.png 'image-91.png')

**L9 `steps`(F-57 主路径):**


```text
load_agent_record   kind=catalog  status=success
materialize_agent   kind=python   status=success
invoke_agent        kind=python   status=success
agent_id = verify-bot
```
![image-92.png](https://raw.gitcode.com/user-images/assets/9901179/3874ad3b-b4b3-4b8a-9686-ba5d73d79e92/image-92.png 'image-92.png')

父会话 `transcript.jsonl` 只能看到 overview 委派了 `Agent(core_merged-agent)`;**F-56/57 细节只在上述两个 subagent jsonl + resource-catalog.json**。

---

## 二、F-56 / F-57 验收对话用例

前置:`--agent` 指向已 convert 的 JiuwenAgent bundle;`DEEPSEEK_API_KEY` 已设置;每次新开 session 更清晰。

### A. 主路径(必过)

| ID | 用户输入 | 期望工具链 | 验收点(日志/文件) |
|----|----------|------------|-------------------|
| A1 | 用 JiuwenAgent SDK 创建名为 `verify-bot` 的 LLMAgent;provider=deepseek;api key 用 `env:DEEPSEEK_API_KEY`;model=`deepseek-v4-flash` | Skill → ToolSearch → `create-llm-agent` | `created_persisted=true`;`resource_catalog_reason=f56_resource_catalog`;catalog 文件有 `verify-bot`;密钥非明文 |
| A2 | 用 `verify-bot` 回复 `请原样输出:PING_OK_56`,把原文返回给我 | Skill → ToolSearch(`select:invoke-existing-agent` 或自然语言命中宏) → `invoke-existing-agent` | ToolSearch `matches` 含宏;steps=`load_agent_record→materialize_agent→invoke_agent` 全 success;output 含 `PING_OK_56` |
| A3 | (新开同 bundle 的新 session,不重建)只用已有 `verify-bot` 回复 `PING_CROSS_SESSION` 原文返回 | 同上,**禁止**再 create | 证明跨会话读 catalog(F-56§9 agentforce314#2) |

![F-56+57验收测试4.PNG](https://raw.gitcode.com/user-images/assets/9901179/d390563c-8296-4643-9a23-9b239a74004b/F-56_57验收测试4.PNG 'F-56+57验收测试4.PNG')

![image-93.png](https://raw.gitcode.com/user-images/assets/9901179/3205ee3b-9c90-41aa-b76f-e0dc7f6e826e/image-93.png 'image-93.png')

### B. 引用形态(F-56 契约 / F-57 兼容入参)

| ID | 用户输入 | 期望 | 验收点 |
|----|----------|------|--------|
| B1 | 用 resource_ref=`verify-bot` 调用,消息=`REF_OK`,原文返回 | `invoke-existing-agent`,`agent_ref` 或等价字段 | steps 全 success;output 含 `REF_OK` |
| B2 | 用 agent_id=`verify-bot` 调用,消息=`ID_OK`,原文返回 | 同上(legacy 兼容) | 成功,不落到 `llmagent-invoke` |

![image-94.png](https://raw.gitcode.com/user-images/assets/9901179/69b54d29-9d48-4166-b49d-17dbea0b2be3/image-94.png 'image-94.png')

![image-95.png](https://raw.gitcode.com/user-images/assets/9901179/3062b5dd-6637-4c85-89f8-8ed4aeb8cb4f/image-95.png 'image-95.png')

### C. 幂等 / 更新(F-56§9 agentforce314#4)

| ID | 用户输入 | 期望 | 验收点 |
|----|----------|------|--------|
| C1 | 再次创建同名 `verify-bot`(同配置) | create 成功或明确 upsert | catalog 仍一条主记录;可再 invoke |
| C2 | 创建后立刻 invoke 一次 | 宏成功 | 说明 upsert 未破坏 materializer |

![image-96.png](https://raw.gitcode.com/user-images/assets/9901179/22e678e1-4cd7-4680-9706-377a0e8affca/image-96.png 'image-96.png')

![image-97.png](https://raw.gitcode.com/user-images/assets/9901179/7e9ead6b-f37e-4696-a623-b5aac18d59b4/image-97.png 'image-97.png')

### D. 负向错误码(F-56§9 agentforce314#5/agentforce314#6 + F-57 透传)

| ID | 用户输入 | 期望 | 验收点 |
|----|----------|------|--------|
| D1 | 用 `no-such-agent-xyz` 回复 `ping` | `invoke-existing-agent` 失败 | `error_code` 含 `resource_catalog_missing`(或等价);steps 在 catalog 步失败 |
| D2 | (若环境可构造重名)按模糊名调用 | 拒绝猜测 | `resource_catalog_ambiguous` |
| D3 | 临时去掉 `DEEPSEEK_API_KEY` 再 invoke | materialize/invoke 失败 | `resource_secret_missing` 或明确 secret 错误,**日志无明文 key** |

![image-98.png](https://raw.gitcode.com/user-images/assets/9901179/a96e3cab-3aec-4f23-b78e-195c1448eb91/image-98.png 'image-98.png')

### E. 宏 vs 原子工具(F-57 路由,不含 F-157 隐藏验收)

| ID | 用户输入 | 期望 | 验收点 |
|----|----------|------|--------|
| E1 | 调用已经创建的 Agent(例如让 verify-bot 回复 ping) | 优先 `invoke-existing-agent` | ToolSearch/实际调用是宏,不是 `llmagent-in

See merge request: chadwweng/clawcodex!107
ericleepi314 added a commit that referenced this pull request Jul 29, 2026
…em (#762)

Follow-up to #761. Two user-reported gaps.

1. A pasted image left no trace in the input box.

The composer now shows the reference's chip:

    > what this image is about?
      [Image #2]

The chip is not decoration. The backend assigns the id, and at submit it
DROPS any pending image whose [Image #N] is no longer in the text, so
deleting the chip un-attaches the image. That is the reference's rule
(handlePromptSubmit.ts:225) and it closes the un-attach gap #761 shipped
with: previously the only way to undo an accidental attach was /clear,
which destroys the conversation. The chip text stays in the prompt the
model sees -- the reference leaves image refs inline and sends the bytes
as separate blocks (history.ts:79).

_pending_images entries become (image_id, PastedImage, expects_placeholder)
behind the existing _queue_image choke point, with a monotonic per-session
_image_seq. Session-wide rather than per-prompt on purpose: ids are never
reused and the drain is destructive, so a stale chip recalled from history
or /resume matches nothing. A per-prompt reset would be worse than the
reference -- a recalled [Image #1] would keep a freshly-pasted image #1
alive after its real chip was deleted.

2. Cmd+V did nothing on macOS.

A third unwired RPC: clipboard.paste. macOS terminals own Cmd+V and, with
an image-only clipboard, deliver an EMPTY bracketed paste -- which lands in
the composer's empty-paste branch, calls onClipboardPaste, and hit
gatewayClient's "Unhandled RPC (Phase 2)" default. Resolved {}, silently,
because that call passes quiet=true.

The keybinding was never the missing piece. hermes already binds Cmd+V
(textInput.tsx `isMac && isActionMod(k) && inp === 'v'`, with super+v
reserved in platform.ts) and clawcodex inherited it, so Cmd+V works as a
keypress wherever the terminal reports Cmd, and through the empty-paste
route where it does not. opencode reaches the same place from the other
direction: ctrl+v plus a re-dispatch on an empty bracketed paste.

`placeholder` is a CALL-SITE property, not an RPC property.

Whether an [Image #N] chip gets rendered depends on the caller, so the
flag has to come from one. Six sites reach these four RPCs and only three
render a chip; hardcoding placeholder:true at the RPC layer made the
backend drop the images of the other three -- /image, the
CLAWCODEX_TUI_IMAGE startup image, and typed-path submit -- at submit
time, AFTER their UI had already printed "Attached image". Both of those
worked when #761 shipped. Default false is fail-open, so headless -p and
the VS Code bridge keep sending their images.

Tests pin the cross-layer invariant in both directions, which is what was
missing when a green suite hid the above: the RPC forwards true from a
chip-rendering caller, defaults to false, and refuses a truthy-ish value;
the real clipboard and dropped-path handlers honour the flag and drop on a
deleted chip.

Live: clipboard image -> [Image #1] -> claude-opus-5 read
"**PURPLE-ELEPHANT-42**" (finish_reason end_turn); with the chip deleted
the prompt went out as plain text with the queue drained.

Python 9007 passed / 3 skipped / 0 failed. ui-tui 1545 passed, same 8
pre-existing baseline failures.

Follow-ups, not in this change: an atomic chip (one backspace deletes the
whole pill, Cursor.ts:346-370 -- today it takes 10, and a partial
"[Image #1" silently un-attaches), and a chip for /image.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant