Skip to content

fix: Fixed DeepSeek thinking-mode replay failures and noisy prompts - #4

Merged
agentforce314 merged 1 commit into
mainfrom
fix/permission_loop
Apr 27, 2026
Merged

fix: Fixed DeepSeek thinking-mode replay failures and noisy prompts#4
agentforce314 merged 1 commit into
mainfrom
fix/permission_loop

Conversation

@fm-chen

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

Copy link
Copy Markdown
Collaborator

Fixed DeepSeek thinking-mode replay failures and noisy permission prompts. reasoning_content is now preserved across both agent-loop and QueryEngine/tool-call paths, including OpenAI-compatible message conversion, preventing DeepSeek 400 errors after tool use. REPL permission prompts are now serialized to avoid prompt_toolkit races and cached per tool for the session, so repeated Grep/Glob calls only ask once. Added focused regression coverage for reasoning replay, message normalization, provider conversion, and REPL permission prompt behavior.

@fm-chen
fm-chen force-pushed the fix/permission_loop branch from 77a46c5 to 920e14d Compare April 27, 2026 02:27
@agentforce314
agentforce314 merged commit 2d13f29 into main Apr 27, 2026
ericleepi314 pushed a commit that referenced this pull request May 9, 2026
…schema validation, env injection

Phase 1 of ch12 extensibility refactor (gap analysis + plan in
my-docs/ch12-extensibility-{gap-analysis,refactoring-plan}.md). Builds
on Phase 0 (62a307c) which rewired the snapshot read path.

WI-1.1 — Expand HookEvent literal from 10 to 27 events covering the
chapter's full taxonomy. Promote SessionStart/SessionEnd/PreCompact/
PostCompact to first-class events; legacy ``Notification + matcher:
"onSessionStart"`` form is translated to first-class names by a
back-compat reader in ``load_hooks_from_settings`` with DeprecationWarning
(one CHANGELOG cycle). Lifecycle routers in session_hooks.py now read
from first-class events (``SESSION_START_EVENT = "SessionStart"`` etc.).

WI-1.2 — HookSource gets six values matching TS: USER_SETTINGS,
PROJECT_SETTINGS, LOCAL_SETTINGS, POLICY_SETTINGS, SESSION_HOOK,
PLUGIN_HOOK. Priority is an integer attribute (USER=0, …, PLUGIN_HOOK=999
sentinel) so future tiers can insert without disturbing plugin ordering.
Old names (SETTINGS, POLICY, PLUGINS) survive as deprecated aliases via
EnumMeta.__getattr__ — emits DeprecationWarning and resolves to the
canonical member. Per critic N3, this metaclass-level intercept is the
right hook point because HookSource.X is a class-attribute access, not
a module-level lookup. FRONTMATTER/SKILLS values had no producer (gap
analysis §5) and are removed outright.

WI-1.3 — HookConfig gains three fields: ``if_condition`` (permission-
rule grammar string, parsed from ``if`` or ``if_condition`` keys),
``once`` (bool; honored by Phase-3 session-hook registration), and
``skill_root`` (set at skill-hook registration time, not parsed from
settings.json). validate_hook_configs gets per-field type checks.

WI-1.4 — Hook output JSON schema validation via Pydantic (already a
transitive dep). Replaces the ad-hoc dict.get block at
_execute_command_hook ``:170-188``. Closes the headline failure mode:
``{"decision": "Deny"}`` (capital D) used to silently no-op; now it
fails schema validation, the executor logs WARNING and drops the
decision payload (exit code still honored). ``extra="forbid"`` rejects
unknown fields.

WI-1.5 — Three env vars injected at hook fire time: CLAUDE_PROJECT_DIR
(workspace_root), CLAUDE_PLUGIN_ROOT (hook.skill_root), CLAUDE_ENV_FILE
(per-fire ephemeral path, set ONLY for SessionStart/Setup/CwdChanged
per N4). Note: this WI sets the path; the source-and-apply loop is a
separate follow-up ticket (TODO in _env_file_for_event docstring).

Critic nits rolled in:
- N1 self-resolves: trust_gate.py docstring already used POLICY_SETTINGS
  (post-Phase-1 name); now matches the renamed enum.
- N3 cleanup: legacy MockCtx.options.hooks tests in test_hook_executor.py
  now wrap the deprecation warning in pytest.warns(DeprecationWarning).

Tests: 68 new tests across six files (taxonomy, source deprecation
alias, HookConfig schema, output schema, env injection, legacy
back-compat reader). All Phase-0 + Phase-1 + existing hook tests green
(166/166). Full unit sweep: 3580 passed, 27 failed — same 27
pre-existing API-key env failures as Phase 0; zero regressions.

The chapter's two worked examples remain inert (gap #14's ``if`` matcher
lands in Phase 4; gap #4's ``once`` registration lands in Phase 3) —
the field plumbing is in place but the consumers haven't been built yet.

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 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
…schema validation, env injection

Phase 1 of ch12 extensibility refactor (gap analysis + plan in
my-docs/ch12-extensibility-{gap-analysis,refactoring-plan}.md). Builds
on Phase 0 (62a307c) which rewired the snapshot read path.

WI-1.1 — Expand HookEvent literal from 10 to 27 events covering the
chapter's full taxonomy. Promote SessionStart/SessionEnd/PreCompact/
PostCompact to first-class events; legacy ``Notification + matcher:
"onSessionStart"`` form is translated to first-class names by a
back-compat reader in ``load_hooks_from_settings`` with DeprecationWarning
(one CHANGELOG cycle). Lifecycle routers in session_hooks.py now read
from first-class events (``SESSION_START_EVENT = "SessionStart"`` etc.).

WI-1.2 — HookSource gets six values matching TS: USER_SETTINGS,
PROJECT_SETTINGS, LOCAL_SETTINGS, POLICY_SETTINGS, SESSION_HOOK,
PLUGIN_HOOK. Priority is an integer attribute (USER=0, …, PLUGIN_HOOK=999
sentinel) so future tiers can insert without disturbing plugin ordering.
Old names (SETTINGS, POLICY, PLUGINS) survive as deprecated aliases via
EnumMeta.__getattr__ — emits DeprecationWarning and resolves to the
canonical member. Per critic N3, this metaclass-level intercept is the
right hook point because HookSource.X is a class-attribute access, not
a module-level lookup. FRONTMATTER/SKILLS values had no producer (gap
analysis §5) and are removed outright.

WI-1.3 — HookConfig gains three fields: ``if_condition`` (permission-
rule grammar string, parsed from ``if`` or ``if_condition`` keys),
``once`` (bool; honored by Phase-3 session-hook registration), and
``skill_root`` (set at skill-hook registration time, not parsed from
settings.json). validate_hook_configs gets per-field type checks.

WI-1.4 — Hook output JSON schema validation via Pydantic (already a
transitive dep). Replaces the ad-hoc dict.get block at
_execute_command_hook ``:170-188``. Closes the headline failure mode:
``{"decision": "Deny"}`` (capital D) used to silently no-op; now it
fails schema validation, the executor logs WARNING and drops the
decision payload (exit code still honored). ``extra="forbid"`` rejects
unknown fields.

WI-1.5 — Three env vars injected at hook fire time: CLAUDE_PROJECT_DIR
(workspace_root), CLAUDE_PLUGIN_ROOT (hook.skill_root), CLAUDE_ENV_FILE
(per-fire ephemeral path, set ONLY for SessionStart/Setup/CwdChanged
per N4). Note: this WI sets the path; the source-and-apply loop is a
separate follow-up ticket (TODO in _env_file_for_event docstring).

Critic nits rolled in:
- N1 self-resolves: trust_gate.py docstring already used POLICY_SETTINGS
  (post-Phase-1 name); now matches the renamed enum.
- N3 cleanup: legacy MockCtx.options.hooks tests in test_hook_executor.py
  now wrap the deprecation warning in pytest.warns(DeprecationWarning).

Tests: 68 new tests across six files (taxonomy, source deprecation
alias, HookConfig schema, output schema, env injection, legacy
back-compat reader). All Phase-0 + Phase-1 + existing hook tests green
(166/166). Full unit sweep: 3580 passed, 27 failed — same 27
pre-existing API-key env failures as Phase 0; zero regressions.

The chapter's two worked examples remain inert (gap #14's ``if`` matcher
lands in Phase 4; gap #4's ``once`` registration lands in Phase 3) —
the field plumbing is in place but the consumers haven't been built yet.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ericleepi314 pushed a commit that referenced this pull request May 9, 2026
…schema validation, env injection

Phase 1 of ch12 extensibility refactor (gap analysis + plan in
my-docs/ch12-extensibility-{gap-analysis,refactoring-plan}.md). Builds
on Phase 0 (62a307c) which rewired the snapshot read path.

WI-1.1 — Expand HookEvent literal from 10 to 27 events covering the
chapter's full taxonomy. Promote SessionStart/SessionEnd/PreCompact/
PostCompact to first-class events; legacy ``Notification + matcher:
"onSessionStart"`` form is translated to first-class names by a
back-compat reader in ``load_hooks_from_settings`` with DeprecationWarning
(one CHANGELOG cycle). Lifecycle routers in session_hooks.py now read
from first-class events (``SESSION_START_EVENT = "SessionStart"`` etc.).

WI-1.2 — HookSource gets six values matching TS: USER_SETTINGS,
PROJECT_SETTINGS, LOCAL_SETTINGS, POLICY_SETTINGS, SESSION_HOOK,
PLUGIN_HOOK. Priority is an integer attribute (USER=0, …, PLUGIN_HOOK=999
sentinel) so future tiers can insert without disturbing plugin ordering.
Old names (SETTINGS, POLICY, PLUGINS) survive as deprecated aliases via
EnumMeta.__getattr__ — emits DeprecationWarning and resolves to the
canonical member. Per critic N3, this metaclass-level intercept is the
right hook point because HookSource.X is a class-attribute access, not
a module-level lookup. FRONTMATTER/SKILLS values had no producer (gap
analysis §5) and are removed outright.

WI-1.3 — HookConfig gains three fields: ``if_condition`` (permission-
rule grammar string, parsed from ``if`` or ``if_condition`` keys),
``once`` (bool; honored by Phase-3 session-hook registration), and
``skill_root`` (set at skill-hook registration time, not parsed from
settings.json). validate_hook_configs gets per-field type checks.

WI-1.4 — Hook output JSON schema validation via Pydantic (already a
transitive dep). Replaces the ad-hoc dict.get block at
_execute_command_hook ``:170-188``. Closes the headline failure mode:
``{"decision": "Deny"}`` (capital D) used to silently no-op; now it
fails schema validation, the executor logs WARNING and drops the
decision payload (exit code still honored). ``extra="forbid"`` rejects
unknown fields.

WI-1.5 — Three env vars injected at hook fire time: CLAUDE_PROJECT_DIR
(workspace_root), CLAUDE_PLUGIN_ROOT (hook.skill_root), CLAUDE_ENV_FILE
(per-fire ephemeral path, set ONLY for SessionStart/Setup/CwdChanged
per N4). Note: this WI sets the path; the source-and-apply loop is a
separate follow-up ticket (TODO in _env_file_for_event docstring).

Critic nits rolled in:
- N1 self-resolves: trust_gate.py docstring already used POLICY_SETTINGS
  (post-Phase-1 name); now matches the renamed enum.
- N3 cleanup: legacy MockCtx.options.hooks tests in test_hook_executor.py
  now wrap the deprecation warning in pytest.warns(DeprecationWarning).

Tests: 68 new tests across six files (taxonomy, source deprecation
alias, HookConfig schema, output schema, env injection, legacy
back-compat reader). All Phase-0 + Phase-1 + existing hook tests green
(166/166). Full unit sweep: 3580 passed, 27 failed — same 27
pre-existing API-key env failures as Phase 0; zero regressions.

The chapter's two worked examples remain inert (gap #14's ``if`` matcher
lands in Phase 4; gap #4's ``once`` registration lands in Phase 3) —
the field plumbing is in place but the consumers haven't been built yet.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ericleepi314 added a commit that referenced this pull request May 11, 2026
ch03 state #4: AppState + on_change_app_state router
peroxider pushed a commit to peroxider/clawcodex that referenced this pull request Jun 15, 2026
…tegration test

Engine/runner/task polish (port-plan §10 remaining items, non-TUI):

- agentforce314#7 Result delivery: enqueue_workflow_notification delivers a run's terminal
  result to the model via the shared <task-notification> queue (notified guard).
- agentforce314#8 Run-file location: journals live under ~/.clawcodex/transcripts/workflows/
  (per-user session storage) via get_workflow_run_path, not the project tree.
- agentforce314#3 retry_workflow_agent: engine retry loop re-spawns a running agent (bounded);
  also fixes a latent bug where a single-agent skip propagated and aborted the run.
- agentforce314#4 ProgressTracker fed to finalize_agent_tool for accurate token totals.
- agentforce314#5 isolation="worktree": per-agent wf_<runId>-<idx> git worktrees, best-effort.
- agentforce314#6 Live integration test (fake provider) driving LiveAgentRunner -> run_agent ->
  the real query loop. It caught two real bugs, fixed here: tool DISPATCH resolves
  by name from the registry (so schema agents need a per-call registry where
  StructuredOutput is the validating tool), and the injected StructuredOutput was
  permission-blocked in the subagent (now explicitly allowed).

Full workflow suite: 144 passing.

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 17, 2026
- 新增 /dream LocalCommand 子命令 (run/once/status/help)
- 永久 cron 任务 (id=dream, 每日 03:00) + 本地 fire handler 拦截,
  避免 consolidation prompt 被路由到 model outbox
- install_and_wire_dream 启动期幂等安装 (验收 agentforce314#4)
- 锁 self-acquisition 短路: 同一进程的 record + try_acquire 不再互相阻塞
- E2E 覆盖: slash command → runner、cron fire → wire handler、
  真实 CronScheduler tick 触发 dream、跨启动幂等
- Stage 3d 增 6 个 dream 命令守卫 (含 Unknown command 不再报)
- Stage 5 dreaming 子系统覆盖

Co-Authored-By: MiniMax-M3 <MiniMax-AI@claude-code-best.win>
peroxider pushed a commit to peroxider/clawcodex that referenced this pull request Jun 24, 2026
feat: multi-model stages + F-39 intent preservation fix

Created-by: qq_49552963
Commit-by: 叶蕴瑶
Merged-by: chadwweng
Description: | 文件                                  | 改动                                                                                             |
| ----------------------------------- | ---------------------------------------------------------------------------------------------- |
| `config/schema.py`                  | `AgentConfig` 加 `stage_overrides: dict[str, dict]`,YAML `agent.stages` 解析                      |
| `api/orchestration.py`              | 遍历 stage_overrides,`dataclasses.replace` 覆写 provider/model,构建独立 AgentRunner                    |
| `orchestrator.py`                   | `__init__` 接收 `stage_runners`,`_run_issue` 里一行 `stage_runners.get(run_kind, agent_runner)` 查字典 |
| `glm_provider.py`                   | ZhipuAI SDK 不支持 `stream_options`,patch 剥掉                                                      |
| `issue_registry.py`                 | `register()` 保留 intent + feedback 字段,修 F-39 label 触发降级 bug                                     |
| `test_orchestrator_f39_followup.py` | 新增 `TestRegisterPreservesIntentAndFeedback` 3 个测试                                              |
验证
配置:
![image.png](https://raw.gitcode.com/user-images/assets/9901179/d05f2ad7-d20b-4470-8d2f-5b5f2c6ed90a/image.png 'image.png')
### Stage 1 — deepseek

新建 issue agentforce314#5,label `progressing`。orchestrator 检测到 → 真 agent run → push commit `8c23f080` → 创建 PR agentforce314#5 `[multi-model test] Add VERSION file`。
![image.png](https://raw.gitcode.com/user-images/assets/9901179/1f95e6dc-05d6-4187-a489-672dfbf27ffd/image.png 'image.png')

### Stage 2 — GLM

PR agentforce314#4 上的 inline review comments 触发 F-37。73 次 `open.bigmodel.cn` API call,5 turn agent run,PR agentforce314#4 commit `1abff964`。
![image.png](https://raw.gitcode.com/user-images/assets/9901179/cf833b99-9c8e-4fd0-8336-7f1c3c72f5d8/image.png 'image.png')

### Stage 3 — MiniMax

在 issue agentforce314#5 上打 `agent:follow-up` label 触发 F-39。59 次 `api.minimaxi.com/anthropic` API call,3 turn agent run,stage runner 正确选了 MiniMax。
![image.png](https://raw.gitcode.com/user-images/assets/9901179/ec9d6a2f-af82-4378-8b96-1be179597011/image.png 'image.png')


See merge request: chadwweng/clawcodex!25
peroxider pushed a commit to peroxider/clawcodex that referenced this pull request Jun 26, 2026
…_call

The `_task_update_call` function lives in `clawcodex_ext/tool_system/tools/tasks_v2.py`,
not `src/tool_system/tools/tasks_v2.py`. The wrong import caused every progress
heartbeat to log a warning and fail silently:

    WARNING progress_sink Failed to update task metadata for task X:
      cannot import name '_task_update_call' from 'src.tool_system.tools.tasks_v2'

This silent failure meant `query_runner.heartbeat` saw `seconds_since_last_event`
accumulate indefinitely, which triggered premature `exit_code=124` timeouts on
otherwise-healthy agent runs (e.g. agentforce314#4/agentforce314#6/agentforce314#7 had used 17/19/42 tools respectively
when killed). Fixing the import restores progress reporting so the heartbeat sees
real activity and lets agents finish their work.

This is an `extensions/`-layer fix — no upstream `src/` change — preserving the
Decoupling Mandate.

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>
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 6, 2026
…C8) (#658)

* feat(sandbox): honest unsandboxed guard — settings.sandbox no longer silent (C8)

The port has NO sandbox enforcement (TS wraps the external
@anthropic-ai/sandbox-runtime) AND SettingsSchema had no `sandbox` field — so a
user's settings.sandbox was silently DROPPED at parse AND unenforced: a false
sense of security. This is the utils/-round deferred "Sandbox chapter"'s
security-meaningful MINIMUM (enforcement itself remains deferred).

The guard maps the port's permanently-unavailable sandbox onto TS's OWN
documented sandbox-unavailable path (entrypoints/sandboxTypes.ts:96-103): when
sandbox.enabled is true but the sandbox cannot start,
  - failIfUnavailable=true  → REFUSE (managed-settings hard gate — never
    silently run unsandboxed),
  - failIfUnavailable=false  → "a warning is shown and commands run unsandboxed".

- SettingsSchema.sandbox: new SandboxSettings dataclass (enabled,
  failIfUnavailable, autoAllowBashIfSandboxed, allowUnsandboxedCommands,
  excludedCommands); from_dict accepts camelCase + snake and IGNORES the
  enforcement-only passthrough keys (network/filesystem/ripgrep/…) so a valid
  settings.json never fails to load.
- permissions/sandbox_guard.py: sandbox_hard_gate_error / sandbox_unsandboxed_
  warning / warn_if_unsandboxed_once.
- _bash_call: hard-gate → ToolPermissionError refusal; else warn-once + proceed
  (best-effort, never crashes bash). validate_settings surfaces the hard gate
  at load too.

NON-goal (deferred): native sandbox-exec/bwrap enforcement — the
sandbox-native-enforcement sub-chapter (needs critic scoping: macOS
sandbox-exec is built-in/no-dep and may be in reach).

tests/test_sandbox_guard.py (12): camelCase parse + unknown-key passthrough,
the 3-way guard mapping, validate_settings hard-gate-errors vs warning-loads,
warn-once, and _bash_call refuses under hard gate / runs+warns otherwise.
134 settings/bash tests green.

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

* docs+test(sandbox): C8 scope — bash-tool-scoped (fg+bg), matches TS BashTool

Pre-empting the critic's false-hard-gate probe: the guard is BashTool-scoped
(both foreground + background bash share _bash_call, guard runs before the
run_in_background branch), matching TS where shouldUseSandbox is BashTool-scoped
(called only from bashPermissions.ts). Hook subprocesses + MCP stdio are
user-authored config TS's sandbox doesn't wrap either → intentionally out of
scope. The hard gate is an honest "no unsandboxed BASH", not a false "nothing
runs unsandboxed". Added the scope note to the module + a background-bash
hard-gate refusal test (13 total).

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

* fix(sandbox): C8 — hard gate is a REFUSE-TO-START, not per-bash (critic MAJOR)

The first cut refused per-_bash_call, but TS's failIfUnavailable is a
refuse-to-START at the entrypoints (print.ts:600 / REPL.tsx:2362 "refusing to
start without a working sandbox"). So under sandbox.enabled+failIfUnavailable
the port STARTED and ran /bg (background/tasks.py Popen, OUTSIDE _bash_call),
MCP servers, and hooks UNSANDBOXED while _bash_call refused the same command —
a "hard gate" that /bg walked through, contradicting the chapter's no-false-
assurance thesis.

- Refuse-to-start in agent_server._build_runtime: sandbox_hard_gate_error →
  sess.init_error → the session refuses to start (the port's existing
  init-error mechanism), so /bg + MCP + hooks never run. The per-_bash_call
  guard stays as a CLI-path backstop.
- enabledPlatforms (critic minor #3): now parsed + honored — on a platform not
  in the list, sandbox is disabled (no gate/warning), matching TS
  (sandboxTypes.ts:104); the port was more aggressive (would refuse where TS
  runs).
- sandbox_guard docstring split into the two mechanisms (BashTool-scoped
  WARNING vs refuse-to-start HARD GATE).

Tests +6 (→19): _build_runtime refuses under hard gate / warning-only starts;
skill-slash !-shell fails closed (minor #4); enabledPlatforms off-platform
disables / on-platform gates / empty=all.

Enforcement scoping (critic-endorsed): guard + refuse-to-start is the MVP;
native sandbox-exec/bwrap is a separate scoped chapter (a half-working sandbox
is worse than an honest refusal — the chapter's own thesis).

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

* fix(sandbox): C8 re-review 2 — close the /bg control-path leak

The refuse-to-start (v2) closed MCP + hooks (both in _build_runtime), but the
critic found /bg + /bg-agent are CONTROL requests, not turns: send_to_agent →
_handle_control_request → _do_bgtask → subprocess.Popen(shell=True). That path
bypasses _build_runtime AND the turn-path init_error checks, and the session
stays fully alive after init_error (sess.start() + the returned handle are
unconditional). So `/bg foo` still ran UNSANDBOXED under a hard-gate config the
session was supposed to refuse — the same MAJOR, on the one path v2 missed.

Fix: guard the top of _handle_control_request — a session with init_error set
rejects every control request (replying the gate error) EXCEPT `interrupt` (a
benign abort of a non-existent turn). Mirrors the "session not ready" pattern
the permission-mode handlers already use. Now /bg + /bg-agent + MCP + hooks all
refuse; the docstring's "nothing runs, not just Bash" is truthful.

Test replaced (the old one asserted tool_registry is None — the intermediate,
not the property): test_bg_run_control_request_refused_under_init_error drives a
real bg_run control request against an init_error session and asserts NO
subprocess (Popen + _do_bgtask both tripwired to raise) + the gate-error reply;
test_interrupt_still_works_under_init_error pins the exemption. 21 sandbox tests.

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 6, 2026
…ult) (#666)

* feat(proxy): wire the CCR upstream proxy (C9 — env-gated, no-op by default)

The port ported upstream_proxy.py (init_upstream_proxy + get_upstream_proxy_env,
gated on CLAUDE_CODE_REMOTE) but left it UNWIRED — zero live callers, and
subprocess_env carried a TODO ("when it lands, merge get_upstream_proxy_env").
This wires it, mirroring TS's registerUpstreamProxyEnvFn indirection.

- src/utils/subprocess_env.py: register_upstream_proxy_env_fn(fn) + a module
  provider slot. subprocess_env merges the provider's recipe AFTER the scrub
  (so an injected HTTPS_PROXY / *_CA_BUNDLE isn't stripped by the
  anti-exfiltration pass), best-effort (a proxy-env failure can't break child
  spawning). The indirection keeps this module from statically importing the
  upstreamproxy package (asyncio/ssl/relay).
- src/entrypoints/agent_server_cli.py: _maybe_init_upstream_proxy() at _serve
  start — when CLAUDE_CODE_REMOTE is truthy, register the provider + await
  init_upstream_proxy(); FAIL-OPEN (a remote session degrades to direct rather
  than the server dying). No-op otherwise.

DEFAULT BEHAVIOUR UNCHANGED: with CLAUDE_CODE_REMOTE unset (every local build,
and the open TS build strips this path entirely), the provider is never
registered and subprocess_env is byte-for-byte the base. Low blast radius —
the value is that a CCR-remote deployment's ported proxy actually engages
instead of being dead code.

Scope: the WS mTLS/CA-context and relay.stop()-cleanup pieces from the plan live
inside the dormant relay itself (no open-build reference, untestable without CCR
infra) — deferred; this lands the reachable, testable wiring.

tests/test_c9_upstream_proxy_wiring.py (6): default no-op (env unchanged /
empty-provider no-op), proxy-var merge, survives-the-scrub, fail-open, and the
entrypoint guard is a no-op without the env. 89 subprocess/proxy tests green.

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

* fix(proxy): C9 #3 — register the proxy provider only AFTER init succeeds

Pre-addressing the c9-critic's highest-risk probe: _maybe_init_upstream_proxy
registered the env provider BEFORE await init_upstream_proxy(), so a failed init
(fail-open) could leave the provider registered. Reordered to init-first,
register-only-on-success — a failed init now leaves ZERO registration, so
subprocess_env stays a pure no-op. (Defense-in-depth: get_upstream_proxy_env
already returns {} when the proxy state is DISABLED with a clean env, verified.)

Tests: a failing init leaves _upstream_proxy_env_fn is None (no half-registered
provider); the disabled state returns an empty recipe. 8 C9 tests green.

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

* fix(proxy): C9 critic — scrub-authoritative order + register-first (TS parity)

The c9-critic corrected two real errors (and disproved my #4 premise: I'd
grepped typescript/ INSIDE the worktree where it doesn't exist — the open build
HAS this wiring at init.ts:148-149, 30 files reference CLAUDE_CODE_REMOTE. So C9
is genuine restored parity, not overreach — ship it).

MAJOR-A [subprocess_env.py] — scrub/merge order was INVERTED vs TS. TS
(subprocessEnv.ts:91-97) merges the proxy recipe THEN runs the scrub loop, so
the SCRUB is authoritative/last — no provider can re-introduce a scrubbed
secret. The port scrubbed first then merged, so a provider returning a scrub-set
key would SURVIVE into the child (a defense-in-depth hole in the exact
anti-exfiltration control), and the comment falsely claimed "TS order". Fixed:
merge proxy first, THEN scrub — exact TS order; proxy keys aren't in the scrub
set so they still survive. New test proves a provider returning ANTHROPIC_API_KEY
is scrubbed while HTTPS_PROXY survives.

MAJOR-B [agent_server_cli.py] — my earlier #3 "fix" (init-first) diverged from
TS register-first (init.ts:148-149) on a FALSE premise: the critic proved a
provider over a disabled/failed init returns {} on a clean env (safe in EITHER
order). Reverted to register-first for exact TS parity; dropped the false
"injects bad vars" rationale. Test updated to assert the real safe property (a
failed init injects nothing because the disabled state returns {}).

Also (critic notes): wired _serve_stdio too (TS inits in shared init.ts for ALL
sessions, not just HTTP — a CCR-remote stdio container would otherwise miss the
proxy); fixed the upstream_proxy docstring (9-var→8-var, dropped the nonexistent
lowercase `proxy` claim).

9 C9 tests green (default no-op, proxy-merge, scrub-authoritative, fail-open,
disabled-returns-empty, entrypoint-guard). subprocess/proxy suite green.

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
…C8) (#658)

* feat(sandbox): honest unsandboxed guard — settings.sandbox no longer silent (C8)

The port has NO sandbox enforcement (TS wraps the external
@anthropic-ai/sandbox-runtime) AND SettingsSchema had no `sandbox` field — so a
user's settings.sandbox was silently DROPPED at parse AND unenforced: a false
sense of security. This is the utils/-round deferred "Sandbox chapter"'s
security-meaningful MINIMUM (enforcement itself remains deferred).

The guard maps the port's permanently-unavailable sandbox onto TS's OWN
documented sandbox-unavailable path (entrypoints/sandboxTypes.ts:96-103): when
sandbox.enabled is true but the sandbox cannot start,
  - failIfUnavailable=true  → REFUSE (managed-settings hard gate — never
    silently run unsandboxed),
  - failIfUnavailable=false  → "a warning is shown and commands run unsandboxed".

- SettingsSchema.sandbox: new SandboxSettings dataclass (enabled,
  failIfUnavailable, autoAllowBashIfSandboxed, allowUnsandboxedCommands,
  excludedCommands); from_dict accepts camelCase + snake and IGNORES the
  enforcement-only passthrough keys (network/filesystem/ripgrep/…) so a valid
  settings.json never fails to load.
- permissions/sandbox_guard.py: sandbox_hard_gate_error / sandbox_unsandboxed_
  warning / warn_if_unsandboxed_once.
- _bash_call: hard-gate → ToolPermissionError refusal; else warn-once + proceed
  (best-effort, never crashes bash). validate_settings surfaces the hard gate
  at load too.

NON-goal (deferred): native sandbox-exec/bwrap enforcement — the
sandbox-native-enforcement sub-chapter (needs critic scoping: macOS
sandbox-exec is built-in/no-dep and may be in reach).

tests/test_sandbox_guard.py (12): camelCase parse + unknown-key passthrough,
the 3-way guard mapping, validate_settings hard-gate-errors vs warning-loads,
warn-once, and _bash_call refuses under hard gate / runs+warns otherwise.
134 settings/bash tests green.

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

* docs+test(sandbox): C8 scope — bash-tool-scoped (fg+bg), matches TS BashTool

Pre-empting the critic's false-hard-gate probe: the guard is BashTool-scoped
(both foreground + background bash share _bash_call, guard runs before the
run_in_background branch), matching TS where shouldUseSandbox is BashTool-scoped
(called only from bashPermissions.ts). Hook subprocesses + MCP stdio are
user-authored config TS's sandbox doesn't wrap either → intentionally out of
scope. The hard gate is an honest "no unsandboxed BASH", not a false "nothing
runs unsandboxed". Added the scope note to the module + a background-bash
hard-gate refusal test (13 total).

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

* fix(sandbox): C8 — hard gate is a REFUSE-TO-START, not per-bash (critic MAJOR)

The first cut refused per-_bash_call, but TS's failIfUnavailable is a
refuse-to-START at the entrypoints (print.ts:600 / REPL.tsx:2362 "refusing to
start without a working sandbox"). So under sandbox.enabled+failIfUnavailable
the port STARTED and ran /bg (background/tasks.py Popen, OUTSIDE _bash_call),
MCP servers, and hooks UNSANDBOXED while _bash_call refused the same command —
a "hard gate" that /bg walked through, contradicting the chapter's no-false-
assurance thesis.

- Refuse-to-start in agent_server._build_runtime: sandbox_hard_gate_error →
  sess.init_error → the session refuses to start (the port's existing
  init-error mechanism), so /bg + MCP + hooks never run. The per-_bash_call
  guard stays as a CLI-path backstop.
- enabledPlatforms (critic minor #3): now parsed + honored — on a platform not
  in the list, sandbox is disabled (no gate/warning), matching TS
  (sandboxTypes.ts:104); the port was more aggressive (would refuse where TS
  runs).
- sandbox_guard docstring split into the two mechanisms (BashTool-scoped
  WARNING vs refuse-to-start HARD GATE).

Tests +6 (→19): _build_runtime refuses under hard gate / warning-only starts;
skill-slash !-shell fails closed (minor #4); enabledPlatforms off-platform
disables / on-platform gates / empty=all.

Enforcement scoping (critic-endorsed): guard + refuse-to-start is the MVP;
native sandbox-exec/bwrap is a separate scoped chapter (a half-working sandbox
is worse than an honest refusal — the chapter's own thesis).

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

* fix(sandbox): C8 re-review 2 — close the /bg control-path leak

The refuse-to-start (v2) closed MCP + hooks (both in _build_runtime), but the
critic found /bg + /bg-agent are CONTROL requests, not turns: send_to_agent →
_handle_control_request → _do_bgtask → subprocess.Popen(shell=True). That path
bypasses _build_runtime AND the turn-path init_error checks, and the session
stays fully alive after init_error (sess.start() + the returned handle are
unconditional). So `/bg foo` still ran UNSANDBOXED under a hard-gate config the
session was supposed to refuse — the same MAJOR, on the one path v2 missed.

Fix: guard the top of _handle_control_request — a session with init_error set
rejects every control request (replying the gate error) EXCEPT `interrupt` (a
benign abort of a non-existent turn). Mirrors the "session not ready" pattern
the permission-mode handlers already use. Now /bg + /bg-agent + MCP + hooks all
refuse; the docstring's "nothing runs, not just Bash" is truthful.

Test replaced (the old one asserted tool_registry is None — the intermediate,
not the property): test_bg_run_control_request_refused_under_init_error drives a
real bg_run control request against an init_error session and asserts NO
subprocess (Popen + _do_bgtask both tripwired to raise) + the gate-error reply;
test_interrupt_still_works_under_init_error pins the exemption. 21 sandbox tests.

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>
ericleepi314 added a commit that referenced this pull request Jul 7, 2026
…ult) (#666)

* feat(proxy): wire the CCR upstream proxy (C9 — env-gated, no-op by default)

The port ported upstream_proxy.py (init_upstream_proxy + get_upstream_proxy_env,
gated on CLAUDE_CODE_REMOTE) but left it UNWIRED — zero live callers, and
subprocess_env carried a TODO ("when it lands, merge get_upstream_proxy_env").
This wires it, mirroring TS's registerUpstreamProxyEnvFn indirection.

- src/utils/subprocess_env.py: register_upstream_proxy_env_fn(fn) + a module
  provider slot. subprocess_env merges the provider's recipe AFTER the scrub
  (so an injected HTTPS_PROXY / *_CA_BUNDLE isn't stripped by the
  anti-exfiltration pass), best-effort (a proxy-env failure can't break child
  spawning). The indirection keeps this module from statically importing the
  upstreamproxy package (asyncio/ssl/relay).
- src/entrypoints/agent_server_cli.py: _maybe_init_upstream_proxy() at _serve
  start — when CLAUDE_CODE_REMOTE is truthy, register the provider + await
  init_upstream_proxy(); FAIL-OPEN (a remote session degrades to direct rather
  than the server dying). No-op otherwise.

DEFAULT BEHAVIOUR UNCHANGED: with CLAUDE_CODE_REMOTE unset (every local build,
and the open TS build strips this path entirely), the provider is never
registered and subprocess_env is byte-for-byte the base. Low blast radius —
the value is that a CCR-remote deployment's ported proxy actually engages
instead of being dead code.

Scope: the WS mTLS/CA-context and relay.stop()-cleanup pieces from the plan live
inside the dormant relay itself (no open-build reference, untestable without CCR
infra) — deferred; this lands the reachable, testable wiring.

tests/test_c9_upstream_proxy_wiring.py (6): default no-op (env unchanged /
empty-provider no-op), proxy-var merge, survives-the-scrub, fail-open, and the
entrypoint guard is a no-op without the env. 89 subprocess/proxy tests green.

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

* fix(proxy): C9 #3 — register the proxy provider only AFTER init succeeds

Pre-addressing the c9-critic's highest-risk probe: _maybe_init_upstream_proxy
registered the env provider BEFORE await init_upstream_proxy(), so a failed init
(fail-open) could leave the provider registered. Reordered to init-first,
register-only-on-success — a failed init now leaves ZERO registration, so
subprocess_env stays a pure no-op. (Defense-in-depth: get_upstream_proxy_env
already returns {} when the proxy state is DISABLED with a clean env, verified.)

Tests: a failing init leaves _upstream_proxy_env_fn is None (no half-registered
provider); the disabled state returns an empty recipe. 8 C9 tests green.

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

* fix(proxy): C9 critic — scrub-authoritative order + register-first (TS parity)

The c9-critic corrected two real errors (and disproved my #4 premise: I'd
grepped typescript/ INSIDE the worktree where it doesn't exist — the open build
HAS this wiring at init.ts:148-149, 30 files reference CLAUDE_CODE_REMOTE. So C9
is genuine restored parity, not overreach — ship it).

MAJOR-A [subprocess_env.py] — scrub/merge order was INVERTED vs TS. TS
(subprocessEnv.ts:91-97) merges the proxy recipe THEN runs the scrub loop, so
the SCRUB is authoritative/last — no provider can re-introduce a scrubbed
secret. The port scrubbed first then merged, so a provider returning a scrub-set
key would SURVIVE into the child (a defense-in-depth hole in the exact
anti-exfiltration control), and the comment falsely claimed "TS order". Fixed:
merge proxy first, THEN scrub — exact TS order; proxy keys aren't in the scrub
set so they still survive. New test proves a provider returning ANTHROPIC_API_KEY
is scrubbed while HTTPS_PROXY survives.

MAJOR-B [agent_server_cli.py] — my earlier #3 "fix" (init-first) diverged from
TS register-first (init.ts:148-149) on a FALSE premise: the critic proved a
provider over a disabled/failed init returns {} on a clean env (safe in EITHER
order). Reverted to register-first for exact TS parity; dropped the false
"injects bad vars" rationale. Test updated to assert the real safe property (a
failed init injects nothing because the disabled state returns {}).

Also (critic notes): wired _serve_stdio too (TS inits in shared init.ts for ALL
sessions, not just HTTP — a CCR-remote stdio container would otherwise miss the
proxy); fixed the upstream_proxy docstring (9-var→8-var, dropped the nonexistent
lowercase `proxy` claim).

9 C9 tests green (default no-op, proxy-merge, scrub-authoritative, fail-open,
disabled-returns-empty, entrypoint-guard). subprocess/proxy suite green.

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
…loop

fix: Fixed DeepSeek thinking-mode replay failures and noisy prompts
singlaamitesh pushed a commit to singlaamitesh/clawcodex that referenced this pull request Jul 7, 2026
…schema validation, env injection

Phase 1 of ch12 extensibility refactor (gap analysis + plan in
my-docs/ch12-extensibility-{gap-analysis,refactoring-plan}.md). Builds
on Phase 0 (62a307c) which rewired the snapshot read path.

WI-1.1 — Expand HookEvent literal from 10 to 27 events covering the
chapter's full taxonomy. Promote SessionStart/SessionEnd/PreCompact/
PostCompact to first-class events; legacy ``Notification + matcher:
"onSessionStart"`` form is translated to first-class names by a
back-compat reader in ``load_hooks_from_settings`` with DeprecationWarning
(one CHANGELOG cycle). Lifecycle routers in session_hooks.py now read
from first-class events (``SESSION_START_EVENT = "SessionStart"`` etc.).

WI-1.2 — HookSource gets six values matching TS: USER_SETTINGS,
PROJECT_SETTINGS, LOCAL_SETTINGS, POLICY_SETTINGS, SESSION_HOOK,
PLUGIN_HOOK. Priority is an integer attribute (USER=0, …, PLUGIN_HOOK=999
sentinel) so future tiers can insert without disturbing plugin ordering.
Old names (SETTINGS, POLICY, PLUGINS) survive as deprecated aliases via
EnumMeta.__getattr__ — emits DeprecationWarning and resolves to the
canonical member. Per critic N3, this metaclass-level intercept is the
right hook point because HookSource.X is a class-attribute access, not
a module-level lookup. FRONTMATTER/SKILLS values had no producer (gap
analysis §5) and are removed outright.

WI-1.3 — HookConfig gains three fields: ``if_condition`` (permission-
rule grammar string, parsed from ``if`` or ``if_condition`` keys),
``once`` (bool; honored by Phase-3 session-hook registration), and
``skill_root`` (set at skill-hook registration time, not parsed from
settings.json). validate_hook_configs gets per-field type checks.

WI-1.4 — Hook output JSON schema validation via Pydantic (already a
transitive dep). Replaces the ad-hoc dict.get block at
_execute_command_hook ``:170-188``. Closes the headline failure mode:
``{"decision": "Deny"}`` (capital D) used to silently no-op; now it
fails schema validation, the executor logs WARNING and drops the
decision payload (exit code still honored). ``extra="forbid"`` rejects
unknown fields.

WI-1.5 — Three env vars injected at hook fire time: CLAUDE_PROJECT_DIR
(workspace_root), CLAUDE_PLUGIN_ROOT (hook.skill_root), CLAUDE_ENV_FILE
(per-fire ephemeral path, set ONLY for SessionStart/Setup/CwdChanged
per N4). Note: this WI sets the path; the source-and-apply loop is a
separate follow-up ticket (TODO in _env_file_for_event docstring).

Critic nits rolled in:
- N1 self-resolves: trust_gate.py docstring already used POLICY_SETTINGS
  (post-Phase-1 name); now matches the renamed enum.
- N3 cleanup: legacy MockCtx.options.hooks tests in test_hook_executor.py
  now wrap the deprecation warning in pytest.warns(DeprecationWarning).

Tests: 68 new tests across six files (taxonomy, source deprecation
alias, HookConfig schema, output schema, env injection, legacy
back-compat reader). All Phase-0 + Phase-1 + existing hook tests green
(166/166). Full unit sweep: 3580 passed, 27 failed — same 27
pre-existing API-key env failures as Phase 0; zero regressions.

The chapter's two worked examples remain inert (gap agentforce314#14's ``if`` matcher
lands in Phase 4; gap agentforce314#4's ``once`` registration lands in Phase 3) —
the field plumbing is in place but the consumers haven't been built yet.

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
… setup)

Phase 5 of the Ch16 port — credential injection for CCR sessions
running inside containers. Self-contained; depends only on Phase 2's
NO_PROXY allowlist.

  * src/upstreamproxy/protobuf_codec.py — hand-encoded
    UpstreamProxyChunk codec (tag 0x0a + varint + payload). 16 LOC of
    bit manipulation replaces a protobuf runtime dep.

  * src/upstreamproxy/ptrace_guard.py — set_non_dumpable() calls
    libc.prctl(PR_SET_DUMPABLE, 0, ...) via ctypes on Linux to block
    same-UID ptrace of the agent's heap (chapter §"Apply This" rule
    agentforce314#4 — "Keep secrets heap-only in adversarial environments").
    No-op on macOS/Windows. Never raises.

  * src/upstreamproxy/ca_bundle.py — is_valid_pem_content (regex match
    for >=1 well-formed PEM block; security guard against compromised
    server returning HTML/JSON) + download_ca_bundle (5 s timeout +
    PEM validation + atomic write via tempfile + os.replace).

  * src/upstreamproxy/relay.py — single-asyncio CONNECT-over-WS relay
    (no Bun/Node fork). asyncio.start_server listens on 127.0.0.1:0;
    per-connection: parse CRLFCRLF (8 KB cap) -> websockets.connect
    upgrade with auth header -> bidirectional pump (encode_chunk on
    write, decode_chunk on read) + 30 s app-level keepalive.
    asyncio.wait(FIRST_COMPLETED) instead of TaskGroup so one pump
    exiting tears down the others (caught a hang during dev).

  * src/upstreamproxy/upstream_proxy.py — init_upstream_proxy 6-step
    ordered setup: env-var gates (CLAUDE_CODE_REMOTE +
    CCR_UPSTREAM_PROXY_ENABLED + CLAUDE_CODE_REMOTE_SESSION_ID) →
    read /run/ccr/session_token → set_non_dumpable → download CA
    bundle → start relay → unlink token file (only AFTER relay up).
    Fail-open semantics throughout (chapter §"Apply This" rule agentforce314#5).
    get_upstream_proxy_env returns the 9-var dict for child
    subprocesses, with parent-env-inheritance fallback.

64 tests pass + 1 Linux-only skip (prctl). 87% module coverage. Real
WS round-trip e2e test against an in-process echo server. Atomic-write
test simulates os.replace failure mid-write and verifies no leftover
.tmp file.

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
… 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
…e-4-appstate

ch03 state agentforce314#4: AppState + on_change_app_state router
singlaamitesh pushed a commit to singlaamitesh/clawcodex that referenced this pull request Jul 7, 2026
… docstring)

Pre-merge critic flagged 2 cleanup items on the F.4 extraction:

S2: Two parallel copies of build_effective_system_prompt — the public
one in agent_loop_compat.py and a private one in agent_loop.py used
by legacy run_agent_loop. Hot-fix risk if build_context_prompt's
contract changes during the deprecation cycle.
Fix: shim aliases the public function via 'from ..query.agent_loop_compat
import build_effective_system_prompt as _build_effective_system_prompt'.
Single body, no drift risk.

Minor agentforce314#4: agent_loop.py docstring didn't tell future maintainers that
edits to renderer bodies go in renderers.py, not here.
Fix: added 'Do not edit renderer bodies here' paragraph.

Minor agentforce314#1: stale docstring in assistant_tool_use.py:122 still referenced
agent_loop.summarize_tool_use after the actual import moved to renderers.
Fix: docstring updated to match.

Tests: 97 pass.
singlaamitesh pushed a commit to singlaamitesh/clawcodex that referenced this pull request Jul 7, 2026
…tegration test

Engine/runner/task polish (port-plan §10 remaining items, non-TUI):

- agentforce314#7 Result delivery: enqueue_workflow_notification delivers a run's terminal
  result to the model via the shared <task-notification> queue (notified guard).
- agentforce314#8 Run-file location: journals live under ~/.clawcodex/transcripts/workflows/
  (per-user session storage) via get_workflow_run_path, not the project tree.
- agentforce314#3 retry_workflow_agent: engine retry loop re-spawns a running agent (bounded);
  also fixes a latent bug where a single-agent skip propagated and aborted the run.
- agentforce314#4 ProgressTracker fed to finalize_agent_tool for accurate token totals.
- agentforce314#5 isolation="worktree": per-agent wf_<runId>-<idx> git worktrees, best-effort.
- agentforce314#6 Live integration test (fake provider) driving LiveAgentRunner -> run_agent ->
  the real query loop. It caught two real bugs, fixed here: tool DISPATCH resolves
  by name from the registry (so schema agents need a per-call registry where
  StructuredOutput is the validating tool), and the injected StructuredOutput was
  permission-blocked in the subagent (now explicitly allowed).

Full workflow suite: 144 passing.

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>
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
…C8) (#658)

* feat(sandbox): honest unsandboxed guard — settings.sandbox no longer silent (C8)

The port has NO sandbox enforcement (TS wraps the external
@anthropic-ai/sandbox-runtime) AND SettingsSchema had no `sandbox` field — so a
user's settings.sandbox was silently DROPPED at parse AND unenforced: a false
sense of security. This is the utils/-round deferred "Sandbox chapter"'s
security-meaningful MINIMUM (enforcement itself remains deferred).

The guard maps the port's permanently-unavailable sandbox onto TS's OWN
documented sandbox-unavailable path (entrypoints/sandboxTypes.ts:96-103): when
sandbox.enabled is true but the sandbox cannot start,
  - failIfUnavailable=true  → REFUSE (managed-settings hard gate — never
    silently run unsandboxed),
  - failIfUnavailable=false  → "a warning is shown and commands run unsandboxed".

- SettingsSchema.sandbox: new SandboxSettings dataclass (enabled,
  failIfUnavailable, autoAllowBashIfSandboxed, allowUnsandboxedCommands,
  excludedCommands); from_dict accepts camelCase + snake and IGNORES the
  enforcement-only passthrough keys (network/filesystem/ripgrep/…) so a valid
  settings.json never fails to load.
- permissions/sandbox_guard.py: sandbox_hard_gate_error / sandbox_unsandboxed_
  warning / warn_if_unsandboxed_once.
- _bash_call: hard-gate → ToolPermissionError refusal; else warn-once + proceed
  (best-effort, never crashes bash). validate_settings surfaces the hard gate
  at load too.

NON-goal (deferred): native sandbox-exec/bwrap enforcement — the
sandbox-native-enforcement sub-chapter (needs critic scoping: macOS
sandbox-exec is built-in/no-dep and may be in reach).

tests/test_sandbox_guard.py (12): camelCase parse + unknown-key passthrough,
the 3-way guard mapping, validate_settings hard-gate-errors vs warning-loads,
warn-once, and _bash_call refuses under hard gate / runs+warns otherwise.
134 settings/bash tests green.

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

* docs+test(sandbox): C8 scope — bash-tool-scoped (fg+bg), matches TS BashTool

Pre-empting the critic's false-hard-gate probe: the guard is BashTool-scoped
(both foreground + background bash share _bash_call, guard runs before the
run_in_background branch), matching TS where shouldUseSandbox is BashTool-scoped
(called only from bashPermissions.ts). Hook subprocesses + MCP stdio are
user-authored config TS's sandbox doesn't wrap either → intentionally out of
scope. The hard gate is an honest "no unsandboxed BASH", not a false "nothing
runs unsandboxed". Added the scope note to the module + a background-bash
hard-gate refusal test (13 total).

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

* fix(sandbox): C8 — hard gate is a REFUSE-TO-START, not per-bash (critic MAJOR)

The first cut refused per-_bash_call, but TS's failIfUnavailable is a
refuse-to-START at the entrypoints (print.ts:600 / REPL.tsx:2362 "refusing to
start without a working sandbox"). So under sandbox.enabled+failIfUnavailable
the port STARTED and ran /bg (background/tasks.py Popen, OUTSIDE _bash_call),
MCP servers, and hooks UNSANDBOXED while _bash_call refused the same command —
a "hard gate" that /bg walked through, contradicting the chapter's no-false-
assurance thesis.

- Refuse-to-start in agent_server._build_runtime: sandbox_hard_gate_error →
  sess.init_error → the session refuses to start (the port's existing
  init-error mechanism), so /bg + MCP + hooks never run. The per-_bash_call
  guard stays as a CLI-path backstop.
- enabledPlatforms (critic minor #3): now parsed + honored — on a platform not
  in the list, sandbox is disabled (no gate/warning), matching TS
  (sandboxTypes.ts:104); the port was more aggressive (would refuse where TS
  runs).
- sandbox_guard docstring split into the two mechanisms (BashTool-scoped
  WARNING vs refuse-to-start HARD GATE).

Tests +6 (→19): _build_runtime refuses under hard gate / warning-only starts;
skill-slash !-shell fails closed (minor #4); enabledPlatforms off-platform
disables / on-platform gates / empty=all.

Enforcement scoping (critic-endorsed): guard + refuse-to-start is the MVP;
native sandbox-exec/bwrap is a separate scoped chapter (a half-working sandbox
is worse than an honest refusal — the chapter's own thesis).

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

* fix(sandbox): C8 re-review 2 — close the /bg control-path leak

The refuse-to-start (v2) closed MCP + hooks (both in _build_runtime), but the
critic found /bg + /bg-agent are CONTROL requests, not turns: send_to_agent →
_handle_control_request → _do_bgtask → subprocess.Popen(shell=True). That path
bypasses _build_runtime AND the turn-path init_error checks, and the session
stays fully alive after init_error (sess.start() + the returned handle are
unconditional). So `/bg foo` still ran UNSANDBOXED under a hard-gate config the
session was supposed to refuse — the same MAJOR, on the one path v2 missed.

Fix: guard the top of _handle_control_request — a session with init_error set
rejects every control request (replying the gate error) EXCEPT `interrupt` (a
benign abort of a non-existent turn). Mirrors the "session not ready" pattern
the permission-mode handlers already use. Now /bg + /bg-agent + MCP + hooks all
refuse; the docstring's "nothing runs, not just Bash" is truthful.

Test replaced (the old one asserted tool_registry is None — the intermediate,
not the property): test_bg_run_control_request_refused_under_init_error drives a
real bg_run control request against an init_error session and asserts NO
subprocess (Popen + _do_bgtask both tripwired to raise) + the gate-error reply;
test_interrupt_still_works_under_init_error pins the exemption. 21 sandbox tests.

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
…ult) (#666)

* feat(proxy): wire the CCR upstream proxy (C9 — env-gated, no-op by default)

The port ported upstream_proxy.py (init_upstream_proxy + get_upstream_proxy_env,
gated on CLAUDE_CODE_REMOTE) but left it UNWIRED — zero live callers, and
subprocess_env carried a TODO ("when it lands, merge get_upstream_proxy_env").
This wires it, mirroring TS's registerUpstreamProxyEnvFn indirection.

- src/utils/subprocess_env.py: register_upstream_proxy_env_fn(fn) + a module
  provider slot. subprocess_env merges the provider's recipe AFTER the scrub
  (so an injected HTTPS_PROXY / *_CA_BUNDLE isn't stripped by the
  anti-exfiltration pass), best-effort (a proxy-env failure can't break child
  spawning). The indirection keeps this module from statically importing the
  upstreamproxy package (asyncio/ssl/relay).
- src/entrypoints/agent_server_cli.py: _maybe_init_upstream_proxy() at _serve
  start — when CLAUDE_CODE_REMOTE is truthy, register the provider + await
  init_upstream_proxy(); FAIL-OPEN (a remote session degrades to direct rather
  than the server dying). No-op otherwise.

DEFAULT BEHAVIOUR UNCHANGED: with CLAUDE_CODE_REMOTE unset (every local build,
and the open TS build strips this path entirely), the provider is never
registered and subprocess_env is byte-for-byte the base. Low blast radius —
the value is that a CCR-remote deployment's ported proxy actually engages
instead of being dead code.

Scope: the WS mTLS/CA-context and relay.stop()-cleanup pieces from the plan live
inside the dormant relay itself (no open-build reference, untestable without CCR
infra) — deferred; this lands the reachable, testable wiring.

tests/test_c9_upstream_proxy_wiring.py (6): default no-op (env unchanged /
empty-provider no-op), proxy-var merge, survives-the-scrub, fail-open, and the
entrypoint guard is a no-op without the env. 89 subprocess/proxy tests green.

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

* fix(proxy): C9 #3 — register the proxy provider only AFTER init succeeds

Pre-addressing the c9-critic's highest-risk probe: _maybe_init_upstream_proxy
registered the env provider BEFORE await init_upstream_proxy(), so a failed init
(fail-open) could leave the provider registered. Reordered to init-first,
register-only-on-success — a failed init now leaves ZERO registration, so
subprocess_env stays a pure no-op. (Defense-in-depth: get_upstream_proxy_env
already returns {} when the proxy state is DISABLED with a clean env, verified.)

Tests: a failing init leaves _upstream_proxy_env_fn is None (no half-registered
provider); the disabled state returns an empty recipe. 8 C9 tests green.

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

* fix(proxy): C9 critic — scrub-authoritative order + register-first (TS parity)

The c9-critic corrected two real errors (and disproved my #4 premise: I'd
grepped typescript/ INSIDE the worktree where it doesn't exist — the open build
HAS this wiring at init.ts:148-149, 30 files reference CLAUDE_CODE_REMOTE. So C9
is genuine restored parity, not overreach — ship it).

MAJOR-A [subprocess_env.py] — scrub/merge order was INVERTED vs TS. TS
(subprocessEnv.ts:91-97) merges the proxy recipe THEN runs the scrub loop, so
the SCRUB is authoritative/last — no provider can re-introduce a scrubbed
secret. The port scrubbed first then merged, so a provider returning a scrub-set
key would SURVIVE into the child (a defense-in-depth hole in the exact
anti-exfiltration control), and the comment falsely claimed "TS order". Fixed:
merge proxy first, THEN scrub — exact TS order; proxy keys aren't in the scrub
set so they still survive. New test proves a provider returning ANTHROPIC_API_KEY
is scrubbed while HTTPS_PROXY survives.

MAJOR-B [agent_server_cli.py] — my earlier #3 "fix" (init-first) diverged from
TS register-first (init.ts:148-149) on a FALSE premise: the critic proved a
provider over a disabled/failed init returns {} on a clean env (safe in EITHER
order). Reverted to register-first for exact TS parity; dropped the false
"injects bad vars" rationale. Test updated to assert the real safe property (a
failed init injects nothing because the disabled state returns {}).

Also (critic notes): wired _serve_stdio too (TS inits in shared init.ts for ALL
sessions, not just HTTP — a CCR-remote stdio container would otherwise miss the
proxy); fixed the upstream_proxy docstring (9-var→8-var, dropped the nonexistent
lowercase `proxy` claim).

9 C9 tests green (default no-op, proxy-merge, scrub-authoritative, fail-open,
disabled-returns-empty, entrypoint-guard). subprocess/proxy suite green.

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

---------

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
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.

2 participants