Skip to content

[docs: LangChain root notes with shipped code and canonical plan]#501

Closed
cgfixit wants to merge 2 commits into
mainfrom
claude/cyclaw-optimize-langchain-docs-truth
Closed

[docs: LangChain root notes with shipped code and canonical plan]#501
cgfixit wants to merge 2 commits into
mainfrom
claude/cyclaw-optimize-langchain-docs-truth

Conversation

@cgfixit

@cgfixit cgfixit commented Jul 10, 2026

Copy link
Copy Markdown
Owner

read the whole pasted plan and verified the load-bearing claims against the actual repo (not just against the prose) before answering — a few things in it are wrong in ways that would bite you if implemented as-is.

First, a housekeeping note: two automated GitHub Action comments also landed on #501 ("Claude Code is working... encountered an error") — that's a separate CI bot job unrelated to this review; not something I need to act on unless you want me to dig into why it errored. Also, buried in the sources list is an unrelated "generate an infographic" / "@html skills" fragment that looks like accidental copy-paste from a different Perplexity thread — I'm ignoring it, not treating it as an instruction.

The one thing to flag before any implementation-quality feedback
This is net-new capability — a subprocess-spawned write-execution engine with an approval queue, dashboard, and audit trail. CLAUDE.md's operative test right now is "does this polish the portfolio signal or fix a real defect?" Phases 6–8 are neither; they're new capability under an explicit feature freeze. Not saying don't do it — just flagging that this is a "stop and ask" tier by your own repo's rules, not a "proceed" tier. Your call, it's your repo, but I'd rather say it than not.

Verified issues in the pasted code

  1. It reintroduces the exact bug PR [docs: LangChain root notes with shipped code and canonical plan] #501 just fixed, one file over. agentic/harness_optimizer/hitl/audit.py does:

with _AUDIT_PATH.open("a") as f:
f.write(json.dumps(record) + "\n")
The real audit contract (utils/logger.py:210, verified):

def audit_log(event: dict, config_path: str = "config.yaml", cfg: dict | None = None) -> None:
...
log_path = _anchor(cfg["logging"]["audit_file"])
...
record[key] = _redact_value(value, cfg) # PII/secret redaction
...
audit_log owns path anchoring, redaction, and the shared write lock (AUDIT_WRITE_LOCK — the pasted code has no lock, so concurrent subprocess writes can interleave/corrupt lines). The pasted emit_hitl_event is a parallel, incompatible audit system with its own schema (event_type: hitl* vs the repo's event). Every other agentic module calls audit_log(event, config_path=..., cfg=...) — this one should too.

  1. Cwd-relative path computed at import time, on top of Langgrinch **CVE WARNING** #1:

_AUDIT_PATH = Path(os.getenv("CYCLAW_AUDIT_LOG", "audit.jsonl"))
This is exactly the class of bug _BASE_DIR anchoring exists to prevent in gate.py (fixed there after it broke non-repo-root launches) — module-level, relative default, resolved once at import. And it's env-var-driven config, which is the same anti-pattern the reconciliation PR you're commenting on just called out in LangChainFix.md.

  1. Risk-scorer false positives from blob-flattening. score_tool_call does:

flat_args = " ".join(str(v) for v in args.values())
for pattern, delta in _ARG_ESCALATION_RULES:
if pattern.search(flat_args): ...
against secret|token|key|password|cred and main|master with no word boundaries. A path like "turkey_data.csv" or "monkey.txt" matches key; "domain.txt" or "remainder.md" matches main. This will over-escalate ordinary file writes to HIGH and burn through approval-queue trust fast. Needs \b word boundaries at minimum, ideally scored per-argument-name rather than one flattened string.

  1. asyncio.get_event_loop() in submit() is the deprecated pattern (3.10+) — should be asyncio.get_running_loop() since it's only ever called from inside a coroutine.

One thing I checked and is actually fine: pytest-asyncio==1.4.0 is pinned in pyproject.toml/requirements.txt/constraints.txt, so the @pytest.mark.asyncio tests as pasted would collect and run — I verified this rather than assuming it, since it's exactly the kind of dependency-drift claim worth checking rather than trusting.

Bottom line
The subprocess-isolation shape, the decorator/interrupt_config split, and the risk-tier concept are sound and match how CyClaw already does things elsewhere. But if you implement this verbatim, you'd ship a second, unredacted, unlocked audit channel and an approval gate with real false-positive holes — worth fixing those two before wiring it to anything that can actually delete files or push to GitHub. If you want, I can turn items 1–4 into a short punch-list doc (not implement — that's your call given the freeze) so it's ready when you decide to build it for real.

The earlier automated Claude Code Action job errored out on this PR, so this is likely you asking for an actual review since that one failed. Let me review the real PR #501 diff (not the pasted HITL doc from before — that's separate).

Ran a command, used a tool
I reviewed the actual PR #501 diff directly (not just the description). Also noting: a separate automated job (GitHub Action's Claude Code bot) is now working this too — that's independent of this session, may post its own comment; and the @codex comment is addressed to a different bot, not something I need to act on.

Review of PR #501's real diff
LangChainFix.md — the 33-line preface is inserted cleanly above your original content, which is untouched (confirmed by diff: every original line is unchanged, only additions above it). The four factual claims in the preface I already verified against the code in the prior review round: I6 module isolation (confirmed via CLAUDE.md §3 / INVARIANTS.md Rule 9), load_agentic_config() as the real config path (confirmed in agentic/config.py), audit_log()'s redaction/hashing contract (confirmed in utils/logger.py:210), and the phase 0-5 scaffold's existence. All accurate.

One minor nit worth naming, not blocking: the preface says "the design below" / "the layout proposed below" — forward references within the file. CLAUDE.md's doc convention says ## sections should be self-contained for chunked corpus search. I checked whether that actually applies here: config.yaml's corpus.path is data/corpus — root-level files like this one aren't in the indexed corpus, so this isn't a real rule violation, just a style nit if the file ever moves into docs/. Not worth blocking on.

LangchainIntegrationPlan.md — the diff removes 504 lines (the full phase 0-19 planning doc: inventory tables, directory layout, config schema, 9-phase implementation table, open questions) and replaces it with an 8-line pointer to the canonical docs/agentic/GITHUB_DEEP_AGENT_HARNESS_OPTIMIZER_PLAN.md. I want to be precise about what this loses versus what's actually preserved: the canonical doc (999+ lines, confirmed in the earlier reconciliation pass) is a superset that already has the same phase table, config schema, and directory layout, plus the newer "Unwired scaffold inventory" and "builder seam gap" sections this root copy never had. So content-wise nothing unique is lost — but this is still a 504-line deletion of something you wrote, and it's still sitting as an unmerged draft specifically so you make that call, not me.

doc-sync — re-ran it fresh against current main: same 2 pre-existing drift items (D1 skills-list gap, D4 pattern-count) that exist with or without this PR. Confirmed, not just asserted.

Verdict: the diff is accurate and safe to merge as-is. The only real decision point is still yours: are you good losing the standalone LangchainIntegrationPlan.md content, given it's fully superseded by the canonical doc?

That's just Codex's automated review bot announcing it found no issues (a 👍-style "nothing to flag" response, not a substantive comment with review threads or suggestions). No action needed — it corroborates my own review's verdict that the diff is clean.

Both are worth checking against the actual files rather than taking at face value — let me verify.

Verified Codex's two claims against the canonical plan doc and env-var usage
Both Codex findings are accurate — verified against the actual files, not just trusted. Fixing both now on the same branch.

Ran 2 commands
No diff on the plan doc between this branch and current main — safe to edit in place. Fixing both issues.

Read a file, edited a file
Now the env-var over-broad claim in LangChainFix.md's preface:

Read a file, edited a file, ran a command
Same 2 pre-existing drift items, no new drift. Committing and pushing.

Committed
1a88938
Both Codex findings were real and are now fixed on PR #501:

P2 (stale canonical-plan claim) — verified: docs/agentic/GITHUB_DEEP_AGENT_HARNESS_OPTIMIZER_PLAN.md §3 still said "No agentic/deepagent_github/ package exists yet" from before phase 0-5 shipped, directly contradicting its own later inventory section. Since this PR's whole point is pointing readers at that file as "the canonical, current plan," a stale contradiction there defeats the purpose — struck through with a pointer to the accurate section.
P3 (overbroad env-var claim) — verified: CYCLAW_EMBED_CACHE_SIZE, gate.py's telemetry-kill vars, and CYCLAW_RATELIMIT_DB_URL are all legitimate non-secret env-var tunables, so "env vars are reserved for secrets and database URLs" was wrong as a blanket statement. Narrowed to the actual rule: agentic feature flags belong in config.yaml, not env vars.
Pushed as 1a88938. doc-sync still shows the same 2 pre-existing drift items, nothing new added.

--

Here's the full aggregated action plan as a downloadable .md.

What it contains, phase by phase:

Phase 6 — Two concrete steps before touching any agent code: annotate all 6 dead files with a structured # TODO(unwired) block (intended import path + planned consumer + blocker), and flip draft_plan() to raise NotImplementedError so it fails loud instead of silently returning garbage.

Phase 7 — The real meat: drop-in build_subagent() replacement with tool registry resolution, ValueError on bare strings, and _created=True only after validation passes. Three pytest cases that will catch the original builder.py:94-99 bug on regression. Plus the canonical GITHUB_HITL_POLICY dict wired to interrupt_on + checkpointer, with the full resume pattern.[1][2]

Phase 8 — Qwen/Kimi K2 model swap decision table (protocol compatibility vs cost per task), with the specific max_tokens cap warning for Qwen extended thinking.[3]

Phase 9 — HITL decorator deferred with an explicit gate condition: Phase 7 interrupt_on must have approve + reject + timeout CI cases passing before the decorator layer is worth adding. Includes the integration steps for wiring the async queue, dashboard, and audit log.[4][5]

Bottom line on sequencing: Don't touch the decorator until Phase 7 is green. Inside the harness, interrupt_on is the correct and sufficient control plane — the decorator is for tools that live outside deepagents.[1]

Sources
[1] Human-in-the-loop - Docs by LangChain https://docs.langchain.com/oss/python/deepagents/human-in-the-loop
[2] HumanInTheLoopMiddleware - LangChain Reference https://reference.langchain.com/python/langchain/agents/middleware/human_in_the_loop/HumanInTheLoopMiddleware
[3] Benefits of doing so (agentic capabilities bootstrap or better integration due to it being by langchain?) https://www.perplexity.ai/search/5c4d4e04-22c8-4009-af7e-f9347d86cee0
[4] Built a production incident response agent with LangGraph the interrupt() checkpoint pattern was the key https://www.reddit.com/r/LangChain/comments/1t2wiog/built_a_production_incident_response_agent_with/
[5] Building AI Agents with LangGraph: From Prototype to Production https://www.abhishekchauhan.it/blog/building-ai-agents-langgraph-production
[6] langchain-ai/langgraph-human-in-the-loop | Decision Hub https://hub.decision.ai/skills/langchain-ai/langgraph-human-in-the-loop
[7] Customize Deep Agents - Docs by LangChain https://docs.langchain.com/oss/python/deepagents/customization
[8] Interrupts - Docs by LangChain https://docs.langchain.com/oss/javascript/langgraph/interrupts
[9] InterruptOnConfig | langchain | LangChain Reference https://reference.langchain.com/python/langchain/agents/middleware/human_in_the_loop/InterruptOnConfig
[10] interrupt_on - LangChain Reference Docs https://reference.langchain.com/python/langchain/agents/middleware/human_in_the_loop/HumanInTheLoopMiddleware/interrupt_on
[11] LangGraph Interrupt and Resume Workflow - Part 1/22 https://www.youtube.com/watch?v=NrUVOH8zssk
[12] tessl/pypi-langgraph@1.0.x - Registry https://tessl.io/registry/tessl/pypi-langgraph/1.0.0/files/docs/interrupts.md
[13] How Agent Frameworks Solve Human-in-the-Loop https://dzone.com/articles/agent-frameworks-human-loop
[14] LangGraph Ep12: Breakpoints & Human Approval (interrupt_before) https://www.youtube.com/watch?v=ybHj0MO6cHU
[15] LangChain: .../middleware/human_in_the_loop.py - Fossies https://fossies.org/linux/langchain-langchain-core/libs/langchain_v1/langchain/agents/middleware/human_in_the_loop.py
[16] humanInTheLoopMiddleware - LangChain Reference https://reference.langchain.com/javascript/langchain/index/humanInTheLoopMiddleware
[17] subagents.py - middleware - GitHub https://github.com/langchain-ai/deepagents/blob/main/libs/deepagents/deepagents/middleware/subagents.py
[18] How to Build Asyncio Queues in Python - OneUptime https://oneuptime.com/blog/post/2026-01-30-python-asyncio-queues/view
[19] Python Asyncio producer-consumer workflow congestion / growing ... https://stackoverflow.com/questions/66563012/python-asyncio-producer-consumer-workflow-congestion-growing-queue
[20] queues with Python asyncio - IDisposable Thoughts https://cprieto.com/posts/2021/07/queues-with-python-asyncio.html
[21] Can asyncio.Queue be safely created outside of the event loop ... https://discuss.python.org/t/can-asyncio-queue-be-safely-created-outside-of-the-event-loop-thread/49215
[22] Is there a way to use asyncio.Queue in multiple threads? https://stackoverflow.com/questions/32889527/is-there-a-way-to-use-asyncio-queue-in-multiple-threads
[23] GitHub - aio-libs/janus: Thread-safe asyncio-aware queue for Python https://github.com/aio-libs/janus
[24] How could I implement the same as Python's async queue? https://users.rust-lang.org/t/how-could-i-implement-the-same-as-pythons-async-queue/66352
[25] Python's asyncio: A Hands-On Walkthrough https://realpython.com/async-io-python/
[26] asyncio.Queue - any benefit in async put with maxsize? https://stackoverflow.com/questions/79892703/asyncio-queue-any-benefit-in-async-put-with-maxsize
[27] Async Python — The Agentic AI Field Guide https://menuagentic.com/field-guide/async-python/
[28] Using AgentGen with LangChain: The Complete Integration ... https://www.agent-gen.com/blog/langchain-agentgen-integration
[29] Coroutine Timeout Injection in Python: A Decorator ... https://medium.com/@RampantLions/coroutine-timeout-injection-in-python-a-decorator-approach-with-asyncio-wait-for-171dc0a3f5be
[30] LangGraph Interrupt and Resume Workflow - Part 13/22 https://www.youtube.com/watch?v=PbRq52F7YtM
[31] Building and Using Tools in LangChain | by Warishayat https://medium.com/@warishayat/building-and-using-tools-in-langchain-ad1cb0e16922
[32] Using the decorator https://python.langchain.com.cn/docs/modules/agents/tools/how_to/custom_tools
[33] Python 2026 Significant Changes Guide - Microsoft Learn https://learn.microsoft.com/en-us/agent-framework/support/upgrade/python-2026-significant-changes
[34] StructuredTool | langchain_core - LangChain Reference https://reference.langchain.com/python/langchain-core/tools/structured/StructuredTool
[35] LangGraph Durable Execution Patterns: Resilient AI Agents ... - 芝士貓 https://cheesecat.net/blog/langgraph-durable-execution-resilient-agents-implementation-guide-2026-zh-tw/
[36] StructuredTool 数据类 https://www.langchain.asia/modules/tools/custom_tools
[37] 如何创建工具 - ️ LangChain 框架 https://python.langchain.ac.cn/docs/how_to/custom_tools/
[38] Interrupts - Docs by LangChain https://docs.langchain.com/oss/python/langgraph/interrupts
[39] Resume Interrupts Programmatically - LangGraph https://forum.langchain.com/t/resume-interrupts-programmatically/2541
[40] How to create a Python decorator that can wrap either coroutine or ... https://stackoverflow.com/questions/44169998/how-to-create-a-python-decorator-that-can-wrap-either-coroutine-or-function
[41] functools — Higher-order functions and operations on callable ... https://docs.python.org/3/library/functools.html
[42] Python Decorators: @wraps & Factories https://www.syntaxcache.com/python/decorators
[43] Async Patterns: Modern... https://sesamedisk.com/modern-python-decorators-context-managers-2026/
[44] Python Decorator Tracing for LLM Apps in 2026: Patterns and Pitfalls https://futureagi.com/blog/python-decorator-tracing-llm-2026/
[45] [Python Async Decorator] #python #asyncio #decorator - GitHub Gist https://gist.github.com/Integralist/fb1b5dbb6271632298f44d62a2221905
[46] Fancy Decorators https://realpython.com/primer-on-python-decorators/
[47] Decorators - Python Wiki https://wiki.python.org/moin/Decorators.html
[48] Pythonデコレータ徹底解説 – 基本から3.12新構文まで https://app-tatsujin.com/python-decorator-guide-3-12/
[49] cpython/Lib/functools.py at a46e96076898d126c9f276aef1934195aac34b4e · python/cpython https://github.com/python/cpython/blob/a46e96076898d126c9f276aef1934195aac34b4e/Lib/functools.py
[50] Using a coroutine as decorator https://stackoverflow.com/questions/42043226/using-a-coroutine-as-decorator
[51] OpenAPI-to-MCP Turns Every API Into an Agent Tool. The Missing Piece Is Endpoint-Level Policy https://www.permit.io/blog/openapi-to-mcp-endpoint-level-policy
[52] The Comprehensive Guide to Python Decorators https://asyncmove.com/blog/2026/01/the-comprehensive-guide-to-python-decorators/
[53] decorator - PyPI Package | Hextrap https://hextrap.com/r/pkg/pypi/decorator/
[54] functools.wraps for classes (intermediate) anthony explains #478 https://www.youtube.com/watch?v=rbEqoOIvFko

--

Action Plan (as opposed to the .md at project root labeled something with "langchaing" in it:

Based on everything gathered — CyClaw's subprocess isolation invariant, the deepagents interrupt_config/HumanInterruptConfig API, LangGraph's interrupt()/Command(resume=...) pattern, and your planning doc — here is the full production implementation. This respects CyClaw's hard rule: nothing below runs inside gate.py or graph.py. It lives entirely in agentic/harness_optimizer/hitl/ and is spawned as a subprocess via ops_runner.py.


File: agentic/harness_optimizer/hitl/risk_scorer.py

"""
Dynamic risk scorer for CyClaw HITL approval gate.
Classifies tool calls by sensitivity tier before routing to approval queue.
Zero external deps — pure stdlib + typing.
"""
from __future__ import annotations

import re
from dataclasses import dataclass, field
from enum import IntEnum
from typing import Any


class RiskTier(IntEnum):
    """Ordinal tiers map directly to routing policy."""
    AUTO_ALLOW  = 0   # read-only, no side effects
    LOW         = 1   # reversible writes
    MEDIUM      = 2   # external API non-destructive POSTs
    HIGH        = 3   # destructive, irreversible, or secret-touching
    BLOCK       = 4   # never allowed regardless of approval


# ── Tier classification rules ─────────────────────────────────────────────────
# Pattern matching runs in order; first match wins.
_TOOL_TIER_MAP: dict[str, RiskTier] = {
    # Explicit blocks — governance wall, no approval path
    "run_shell":              RiskTier.BLOCK,
    "exec_arbitrary":         RiskTier.BLOCK,
    "read_secret":            RiskTier.BLOCK,
    # High-risk destructive ops
    "delete_file":            RiskTier.HIGH,
    "rm_recursive":           RiskTier.HIGH,
    "gh_force_push":          RiskTier.HIGH,
    "drop_table":             RiskTier.HIGH,
    "revoke_token":           RiskTier.HIGH,
    # Medium — external API POSTs, reversible writes
    "gh_open_pr":             RiskTier.MEDIUM,
    "gh_create_branch":       RiskTier.MEDIUM,
    "gh_commit_and_push":     RiskTier.MEDIUM,
    "api_post":               RiskTier.MEDIUM,
    "write_file":             RiskTier.MEDIUM,
    "edit_file":              RiskTier.MEDIUM,
    "registry_apply":         RiskTier.MEDIUM,
    # Low — reversible, local, non-secret
    "write_current_file":     RiskTier.LOW,
    "finish_proposal":        RiskTier.LOW,
    # Auto-allow — pure reads
    "read_file":              RiskTier.AUTO_ALLOW,
    "ls":                     RiskTier.AUTO_ALLOW,
    "glob":                   RiskTier.AUTO_ALLOW,
    "grep":                   RiskTier.AUTO_ALLOW,
    "rag_search_readonly":    RiskTier.AUTO_ALLOW,
    "gh_get_pr_diff":         RiskTier.AUTO_ALLOW,
    "gh_list_issues":         RiskTier.AUTO_ALLOW,
}

# Arg-level escalation rules: if arg value matches pattern, bump tier by +1
_ARG_ESCALATION_RULES: list[tuple[re.Pattern[str], int]] = [
    (re.compile(r"\.\./"),          +1),   # path traversal attempt
    (re.compile(r"--force"),        +1),   # destructive flag
    (re.compile(r"prod|production", re.I), +1),  # prod target
    (re.compile(r"secret|token|key|password|cred", re.I), +2),  # secret-touching
    (re.compile(r"main|master",     re.I), +1),  # default branch target
]


@dataclass
class RiskScore:
    tool_name: str
    tier: RiskTier
    base_tier: RiskTier
    escalation_reasons: list[str] = field(default_factory=list)
    args_snapshot: dict[str, Any] = field(default_factory=dict)

    @property
    def requires_approval(self) -> bool:
        return self.tier >= RiskTier.LOW

    @property
    def is_blocked(self) -> bool:
        return self.tier >= RiskTier.BLOCK


def score_tool_call(tool_name: str, args: dict[str, Any]) -> RiskScore:
    """
    Score a pending tool call.

    Args:
        tool_name: Exact tool function name.
        args: Dict of argument name → value as the agent would pass them.

    Returns:
        RiskScore with tier, escalation reasons, and routing recommendation.
    """
    base = _TOOL_TIER_MAP.get(tool_name, RiskTier.MEDIUM)  # unknown tools default MEDIUM
    tier_int = int(base)
    reasons: list[str] = []

    # Flatten all arg values to strings for pattern matching
    flat_args = " ".join(str(v) for v in args.values())

    for pattern, delta in _ARG_ESCALATION_RULES:
        if pattern.search(flat_args):
            tier_int = min(tier_int + delta, int(RiskTier.BLOCK))
            reasons.append(f"arg matched /{pattern.pattern}/ (+{delta})")

    final_tier = RiskTier(tier_int)
    return RiskScore(
        tool_name=tool_name,
        tier=final_tier,
        base_tier=base,
        escalation_reasons=reasons,
        args_snapshot=dict(args),
    )

File: agentic/harness_optimizer/hitl/approval_queue.py

"""
Async approval queue for CyClaw HITL.

Design decisions grounded in CyClaw invariants:
- No external broker deps (Redis, Celery, etc.) — pure asyncio.Queue
- Timeout via asyncio.wait_for to prevent deadlocks in long-running agent threads
- Each pending item gets a UUID; the dashboard polls by UUID
- Approved/edited/rejected decisions land back via put() from the dashboard coroutine
- SHA-256 of tool args stored (not raw) in audit log per utils/logger.py contract
"""
from __future__ import annotations

import asyncio
import hashlib
import json
import time
import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import Any


class Decision(str, Enum):
    APPROVE = "approve"
    EDIT    = "edit"
    REJECT  = "reject"
    TIMEOUT = "timeout"


@dataclass
class ApprovalRequest:
    request_id: str
    tool_name: str
    args: dict[str, Any]
    risk_tier: int
    escalation_reasons: list[str]
    agent_context: str          # model-supplied rationale
    created_at: float = field(default_factory=time.time)
    args_hash: str = field(init=False)

    def __post_init__(self) -> None:
        raw = json.dumps(self.args, sort_keys=True).encode()
        self.args_hash = hashlib.sha256(raw).hexdigest()


@dataclass
class ApprovalDecision:
    request_id: str
    decision: Decision
    edited_args: dict[str, Any] | None   # only set when decision == EDIT
    reviewer_note: str
    decided_at: float = field(default_factory=time.time)


class ApprovalQueue:
    """
    Thread-safe async queue pairing pending requests with their decision futures.

    Usage pattern:
        queue = ApprovalQueue(timeout_seconds=120)
        decision = await queue.submit(request)   # blocks until resolved or timeout
        # In the dashboard coroutine:
        await queue.resolve(request_id, decision_obj)
    """

    def __init__(self, timeout_seconds: float = 120.0) -> None:
        self._timeout = timeout_seconds
        self._pending: asyncio.Queue[ApprovalRequest] = asyncio.Queue()
        self._futures: dict[str, asyncio.Future[ApprovalDecision]] = {}

    async def submit(self, request: ApprovalRequest) -> ApprovalDecision:
        """
        Enqueue a tool call for human review.
        Blocks until resolved or timeout_seconds elapses.
        On timeout returns a TIMEOUT decision — never raises, never deadlocks.
        """
        loop = asyncio.get_event_loop()
        fut: asyncio.Future[ApprovalDecision] = loop.create_future()
        self._futures[request.request_id] = fut
        await self._pending.put(request)

        try:
            return await asyncio.wait_for(
                asyncio.shield(fut),   # shield so cancellation doesn't corrupt state
                timeout=self._timeout,
            )
        except asyncio.TimeoutError:
            self._futures.pop(request.request_id, None)
            return ApprovalDecision(
                request_id=request.request_id,
                decision=Decision.TIMEOUT,
                edited_args=None,
                reviewer_note=f"Auto-rejected: no response within {self._timeout}s",
            )

    async def resolve(self, request_id: str, decision: ApprovalDecision) -> bool:
        """
        Called by the dashboard to deliver a human decision.
        Returns False if request_id is unknown (already timed out or duplicate).
        """
        fut = self._futures.pop(request_id, None)
        if fut is None or fut.done():
            return False
        fut.set_result(decision)
        return True

    async def next_pending(self) -> ApprovalRequest:
        """Dashboard calls this to dequeue the next item awaiting review."""
        return await self._pending.get()

    @property
    def pending_count(self) -> int:
        return self._pending.qsize()

File: agentic/harness_optimizer/hitl/decorator.py

"""
@require_approval decorator — the public API surface.

Wraps any async tool function. On call:
1. Scores the tool call via risk_scorer
2. If AUTO_ALLOW → execute immediately
3. If BLOCK → raise ToolBlockedError, never queue
4. Otherwise → submit to ApprovalQueue, await human decision,
   then approve (execute original), edit (execute with new args), or reject (raise)

The decorator is schema-aware: it reads the wrapped function's type annotations
to validate edited args before re-dispatch.

CyClaw-specific constraints obeyed:
- All decisions emitted to audit.jsonl via _emit_audit()
- No secrets logged (args_hash only, never raw sensitive values)
- Timeout always resolves — no deadlock path
"""
from __future__ import annotations

import asyncio
import functools
import inspect
import time
import uuid
from typing import Any, Callable, TypeVar

from .approval_queue import ApprovalDecision, ApprovalQueue, ApprovalRequest, Decision
from .risk_scorer import RiskTier, score_tool_call
from .audit import emit_hitl_event

F = TypeVar("F", bound=Callable[..., Any])

# Module-level shared queue — injected at startup by the harness
_GLOBAL_QUEUE: ApprovalQueue | None = None


def init_approval_queue(queue: ApprovalQueue) -> None:
    """Call once at harness startup to wire the shared queue."""
    global _GLOBAL_QUEUE
    _GLOBAL_QUEUE = queue


class ToolBlockedError(RuntimeError):
    """Raised when a tool call hits RiskTier.BLOCK — no approval path exists."""


class ToolRejectedError(RuntimeError):
    """Raised when a human reviewer rejects a tool call."""


class ToolTimeoutError(RuntimeError):
    """Raised when no reviewer responds within the timeout window."""


def require_approval(
    *,
    override_tier: RiskTier | None = None,
    context_fn: Callable[..., str] | None = None,
) -> Callable[[F], F]:
    """
    Decorator factory for human-in-the-loop tool approval.

    Args:
        override_tier: Force a specific RiskTier regardless of dynamic scoring.
            Useful for pinning high-risk tools in config without relying on the scorer.
        context_fn: Optional callable taking the same args as the tool,
            returning a human-readable rationale string for the dashboard.
            Defaults to a generic JSON dump of args.

    Example:
        @require_approval(override_tier=RiskTier.HIGH)
        async def delete_file(path: str) -> str:
            ...

        @require_approval(context_fn=lambda path: f"Deleting repo file: {path}")
        async def delete_file(path: str) -> str:
            ...
    """
    def decorator(fn: F) -> F:
        if not asyncio.iscoroutinefunction(fn):
            raise TypeError(
                f"@require_approval only works on async functions. "
                f"'{fn.__name__}' is synchronous — wrap it with asyncio.run() first."
            )

        sig = inspect.signature(fn)

        @functools.wraps(fn)
        async def wrapper(*args: Any, **kwargs: Any) -> Any:
            queue = _GLOBAL_QUEUE
            if queue is None:
                raise RuntimeError(
                    "ApprovalQueue not initialized. Call init_approval_queue() at harness startup."
                )

            # Bind positional + keyword args to the function signature
            bound = sig.bind(*args, **kwargs)
            bound.apply_defaults()
            call_args: dict[str, Any] = dict(bound.arguments)

            # ── Risk scoring ───────────────────────────────────────────────
            score = score_tool_call(fn.__name__, call_args)
            if override_tier is not None:
                score = score.__class__(
                    tool_name=score.tool_name,
                    tier=override_tier,
                    base_tier=score.base_tier,
                    escalation_reasons=score.escalation_reasons + [f"override_tier={override_tier.name}"],
                    args_snapshot=score.args_snapshot,
                )

            # ── BLOCK path ─────────────────────────────────────────────────
            if score.is_blocked:
                emit_hitl_event(
                    event="tool_blocked",
                    tool=fn.__name__,
                    args_hash=score.args_snapshot,   # will be hashed inside emit
                    tier=score.tier.name,
                )
                raise ToolBlockedError(
                    f"Tool '{fn.__name__}' is permanently blocked (RiskTier.BLOCK). "
                    f"Escalation reasons: {score.escalation_reasons}"
                )

            # ── AUTO_ALLOW path ─────────────────────────────────────────────
            if score.tier == RiskTier.AUTO_ALLOW:
                emit_hitl_event(
                    event="tool_auto_allowed",
                    tool=fn.__name__,
                    args_hash=call_args,
                    tier=score.tier.name,
                )
                return await fn(*args, **kwargs)

            # ── Approval queue path ────────────────────────────────────────
            context = (
                context_fn(**call_args)
                if context_fn is not None
                else f"Tool: {fn.__name__} | Args: {list(call_args.keys())}"
            )

            request = ApprovalRequest(
                request_id=str(uuid.uuid4()),
                tool_name=fn.__name__,
                args=call_args,
                risk_tier=int(score.tier),
                escalation_reasons=score.escalation_reasons,
                agent_context=context,
            )

            emit_hitl_event(
                event="tool_queued_for_approval",
                tool=fn.__name__,
                args_hash=call_args,
                tier=score.tier.name,
                request_id=request.request_id,
            )

            decision: ApprovalDecision = await queue.submit(request)

            # ── Decision routing ───────────────────────────────────────────
            emit_hitl_event(
                event=f"tool_decision_{decision.decision.value}",
                tool=fn.__name__,
                args_hash=call_args,
                tier=score.tier.name,
                request_id=request.request_id,
                reviewer_note=decision.reviewer_note,
            )

            if decision.decision == Decision.APPROVE:
                return await fn(*args, **kwargs)

            if decision.decision == Decision.EDIT:
                if decision.edited_args is None:
                    raise ToolRejectedError(
                        f"Decision was EDIT but no edited_args provided for '{fn.__name__}'"
                    )
                # Validate edited args against original signature before dispatch
                try:
                    sig.bind(**decision.edited_args)
                except TypeError as exc:
                    raise ToolRejectedError(
                        f"Edited args for '{fn.__name__}' don't match signature: {exc}"
                    ) from exc
                return await fn(**decision.edited_args)

            if decision.decision == Decision.TIMEOUT:
                raise ToolTimeoutError(
                    f"Tool '{fn.__name__}' (request {request.request_id}) "
                    f"timed out awaiting human approval."
                )

            # Decision.REJECT (or any unknown value)
            raise ToolRejectedError(
                f"Tool '{fn.__name__}' rejected by reviewer. "
                f"Note: {decision.reviewer_note!r}"
            )

        return wrapper  # type: ignore[return-value]

    return decorator

File: agentic/harness_optimizer/hitl/audit.py

"""
Thin audit wrapper matching CyClaw's append-only JSONL contract.
Never logs raw args — SHA-256 hashes only, per utils/logger.py pattern.
"""
from __future__ import annotations

import hashlib
import json
import os
import time
from pathlib import Path
from typing import Any

_AUDIT_PATH = Path(os.getenv("CYCLAW_AUDIT_LOG", "audit.jsonl"))


def _hash_args(args: dict[str, Any]) -> str:
    raw = json.dumps(args, sort_keys=True, default=str).encode()
    return hashlib.sha256(raw).hexdigest()


def emit_hitl_event(
    event: str,
    tool: str,
    args_hash: dict[str, Any],
    tier: str,
    request_id: str | None = None,
    reviewer_note: str | None = None,
) -> None:
    record: dict[str, Any] = {
        "event_type": f"hitl_{event}",
        "tool": tool,
        "args_hash": _hash_args(args_hash),
        "risk_tier": tier,
        "ts": time.time(),
    }
    if request_id:
        record["request_id"] = request_id
    if reviewer_note:
        record["reviewer_note_len"] = len(reviewer_note)  # length only, not content

    with _AUDIT_PATH.open("a") as f:
        f.write(json.dumps(record) + "\n")

File: agentic/harness_optimizer/hitl/dashboard.py

"""
Mock approval dashboard — terminal-based TUI for approving, editing, or
rejecting queued tool calls.

In production this would be replaced by:
  - A FastAPI endpoint polled by the CLI
  - A LangGraph Studio interrupt panel
  - An Agent Inbox-style webhook receiver

For CyClaw's current offline-first, loopback-only posture this terminal
dashboard is the correct default: no network, no external service, no auth dep.
"""
from __future__ import annotations

import asyncio
import json
import sys
from typing import Any

from .approval_queue import ApprovalDecision, ApprovalQueue, ApprovalRequest, Decision
from .risk_scorer import RiskTier

_TIER_LABELS = {
    RiskTier.AUTO_ALLOW: "✅ AUTO-ALLOW",
    RiskTier.LOW:        "🟡 LOW",
    RiskTier.MEDIUM:     "🟠 MEDIUM",
    RiskTier.HIGH:       "🔴 HIGH",
    RiskTier.BLOCK:      "⛔ BLOCK",
}

_RESET  = "\033[0m"
_BOLD   = "\033[1m"
_RED    = "\033[31m"
_YELLOW = "\033[33m"
_GREEN  = "\033[32m"
_CYAN   = "\033[36m"


def _fmt_request(req: ApprovalRequest) -> str:
    tier_label = _TIER_LABELS.get(RiskTier(req.risk_tier), str(req.risk_tier))
    lines = [
        f"\n{'─'*60}",
        f"{_BOLD}⏸  APPROVAL REQUIRED{_RESET}",
        f"  Request ID : {_CYAN}{req.request_id}{_RESET}",
        f"  Tool       : {_BOLD}{req.tool_name}{_RESET}",
        f"  Risk Tier  : {tier_label}",
        f"  Context    : {req.agent_context}",
        f"  Args       :",
    ]
    for k, v in req.args.items():
        lines.append(f"    {k} = {json.dumps(v, default=str)}")
    if req.escalation_reasons:
        lines.append(f"  {_YELLOW}Escalations:{_RESET}")
        for r in req.escalation_reasons:
            lines.append(f"    • {r}")
    lines.append("─"*60)
    return "\n".join(lines)


def _prompt_decision(req: ApprovalRequest) -> ApprovalDecision:
    """Synchronous terminal prompt. Called from a thread via run_in_executor."""
    print(_fmt_request(req))
    print(
        f"\n  {_GREEN}[a]{_RESET} Approve  "
        f"{_YELLOW}[e]{_RESET} Edit args  "
        f"{_RED}[r]{_RESET} Reject\n"
    )

    while True:
        try:
            choice = input("  Decision > ").strip().lower()
        except EOFError:
            # Non-interactive context (CI, pipe) — safe default: reject
            print("  [non-interactive] Auto-rejecting.")
            return ApprovalDecision(
                request_id=req.request_id,
                decision=Decision.REJECT,
                edited_args=None,
                reviewer_note="auto-rejected: non-interactive stdin",
            )

        if choice == "a":
            note = input("  Note (optional) > ").strip()
            return ApprovalDecision(
                request_id=req.request_id,
                decision=Decision.APPROVE,
                edited_args=None,
                reviewer_note=note or "approved",
            )

        elif choice == "e":
            print("  Enter edited args as JSON (e.g. {\"path\": \"/tmp/safe.txt\"}):")
            raw = input("  Edited args > ").strip()
            try:
                edited: dict[str, Any] = json.loads(raw)
            except json.JSONDecodeError as exc:
                print(f"  {_RED}Invalid JSON: {exc}{_RESET} — try again.")
                continue
            note = input("  Note (optional) > ").strip()
            return ApprovalDecision(
                request_id=req.request_id,
                decision=Decision.EDIT,
                edited_args=edited,
                reviewer_note=note or "edited by reviewer",
            )

        elif choice == "r":
            note = input("  Rejection reason > ").strip()
            return ApprovalDecision(
                request_id=req.request_id,
                decision=Decision.REJECT,
                edited_args=None,
                reviewer_note=note or "rejected",
            )

        else:
            print(f"  {_RED}Unknown choice '{choice}'. Enter a, e, or r.{_RESET}")


async def run_dashboard(queue: ApprovalQueue) -> None:
    """
    Async dashboard loop. Runs until the process exits.

    Dequeues approval requests and dispatches them to the terminal prompter
    via run_in_executor so the async event loop stays unblocked.

    In production swap _prompt_decision for a coroutine that POSTs to a
    FastAPI /decisions endpoint or polls Agent Inbox.
    """
    loop = asyncio.get_event_loop()
    print(f"{_BOLD}[CyClaw HITL Dashboard]{_RESET} Waiting for approval requests…")

    while True:
        req: ApprovalRequest = await queue.next_pending()
        # Run blocking terminal I/O in a thread — keeps the event loop live
        decision: ApprovalDecision = await loop.run_in_executor(
            None, _prompt_decision, req
        )
        resolved = await queue.resolve(req.request_id, decision)
        if not resolved:
            print(
                f"  {_YELLOW}Warning:{_RESET} request {req.request_id} already timed out "
                f"before review completed. Decision discarded."
            )

File: agentic/harness_optimizer/hitl/__init__.py

"""
CyClaw Human-in-the-Loop approval layer.

Public API:
    from agentic.harness_optimizer.hitl import require_approval, init_approval_queue
    from agentic.harness_optimizer.hitl.approval_queue import ApprovalQueue
    from agentic.harness_optimizer.hitl.dashboard import run_dashboard
    from agentic.harness_optimizer.hitl.risk_scorer import RiskTier
"""
from .approval_queue import ApprovalQueue, Decision, ApprovalDecision, ApprovalRequest
from .decorator import init_approval_queue, require_approval, ToolBlockedError, ToolRejectedError, ToolTimeoutError
from .risk_scorer import RiskTier, score_tool_call
from .dashboard import run_dashboard

__all__ = [
    "ApprovalQueue",
    "ApprovalDecision",
    "ApprovalRequest",
    "Decision",
    "RiskTier",
    "init_approval_queue",
    "require_approval",
    "run_dashboard",
    "score_tool_call",
    "ToolBlockedError",
    "ToolRejectedError",
    "ToolTimeoutError",
]

File: agentic/harness_optimizer/hitl/example_tools.py

"""
Example tool definitions showing how @require_approval integrates
with existing CyClaw tool schemas.

These mirror the real tool shapes from builder.py / subagents.py.
"""
from __future__ import annotations

import asyncio
from agentic.harness_optimizer.hitl import (
    ApprovalQueue,
    RiskTier,
    init_approval_queue,
    require_approval,
    run_dashboard,
)
from agentic.harness_optimizer.hitl.approval_queue import ApprovalDecision


# ── 1. Read-only tool — auto-allowed, no approval overhead ────────────────────
@require_approval()
async def read_file(path: str) -> str:
    """Read a file from the proposer workspace."""
    # RiskTier.AUTO_ALLOW → runs immediately, no queue
    with open(path) as f:
        return f.read()


# ── 2. Medium-risk: API POST — queued for approval ────────────────────────────
@require_approval(
    context_fn=lambda url, payload: f"POST to external API: {url} | payload keys: {list(payload)}"
)
async def api_post(url: str, payload: dict) -> str:
    """Send a POST request to an external API endpoint."""
    import httpx
    async with httpx.AsyncClient() as client:
        resp = await client.post(url, json=payload, timeout=30)
        return resp.text


# ── 3. High-risk: file deletion — forced HIGH tier, queued ───────────────────
@require_approval(
    override_tier=RiskTier.HIGH,
    context_fn=lambda path: f"DELETING file permanently: {path}",
)
async def delete_file(path: str) -> str:
    """Delete a file. Irreversible. Always requires approval."""
    import os
    os.remove(path)
    return f"Deleted: {path}"


# ── 4. Blocked: shell execution — never reaches queue ────────────────────────
@require_approval(override_tier=RiskTier.BLOCK)
async def run_shell(command: str) -> str:
    """Raw shell execution — blocked at the governance wall."""
    import subprocess
    return subprocess.check_output(command, shell=True, text=True)


# ── Harness entry point ───────────────────────────────────────────────────────
async def main() -> None:
    """
    Demo harness showing the full async approval loop.

    In production this is spawned as a subprocess by ops_runner.py,
    reads its task from stdin JSON, and writes results to stdout JSON.
    """
    queue = ApprovalQueue(timeout_seconds=120)
    init_approval_queue(queue)

    # Run dashboard concurrently in the same event loop
    dashboard_task = asyncio.create_task(run_dashboard(queue))

    # Simulate tool calls an agent might make
    try:
        print("\n[1] read_file — should auto-allow:")
        result = await read_file("/tmp/test.txt")
        print(f"    result: {result!r}")

        print("\n[2] delete_file — should queue for approval:")
        result = await delete_file("/tmp/deleteme.txt")
        print(f"    result: {result!r}")

    except Exception as exc:
        print(f"    caught: {type(exc).__name__}: {exc}")
    finally:
        dashboard_task.cancel()


if __name__ == "__main__":
    asyncio.run(main())

File: tests/test_hitl.py

"""
Unit tests for the HITL approval layer.
No live network, no GitHub, no LM Studio, no LangChain required.
Matches CyClaw's offline-first test philosophy from selftest.py.
"""
from __future__ import annotations

import asyncio
import pytest

from agentic.harness_optimizer.hitl import (
    ApprovalQueue,
    Decision,
    ApprovalDecision,
    RiskTier,
    init_approval_queue,
    require_approval,
    ToolBlockedError,
    ToolRejectedError,
    ToolTimeoutError,
    score_tool_call,
)


# ── Risk scorer unit tests ────────────────────────────────────────────────────

def test_read_file_scores_auto_allow():
    score = score_tool_call("read_file", {"path": "/tmp/safe.txt"})
    assert score.tier == RiskTier.AUTO_ALLOW
    assert not score.requires_approval

def test_delete_file_scores_high():
    score = score_tool_call("delete_file", {"path": "/tmp/file.txt"})
    assert score.tier == RiskTier.HIGH
    assert score.requires_approval

def test_run_shell_scores_block():
    score = score_tool_call("run_shell", {"command": "ls"})
    assert score.is_blocked

def test_path_traversal_escalates():
    # write_file starts at MEDIUM; traversal arg bumps it to HIGH
    score = score_tool_call("write_file", {"path": "../../../etc/passwd", "content": "x"})
    assert score.tier >= RiskTier.HIGH
    assert any("traversal" in r or "../" in r for r in score.escalation_reasons)

def test_secret_arg_double_escalates():
    score = score_tool_call("write_file", {"path": "/tmp/f", "content": "my_secret_token=abc"})
    assert score.tier >= RiskTier.HIGH

def test_unknown_tool_defaults_medium():
    score = score_tool_call("some_new_tool", {"x": 1})
    assert score.tier == RiskTier.MEDIUM


# ── Approval queue unit tests ─────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_approve_resolves_correctly():
    queue = ApprovalQueue(timeout_seconds=5)
    from agentic.harness_optimizer.hitl.approval_queue import ApprovalRequest
    import uuid, time

    req = ApprovalRequest(
        request_id=str(uuid.uuid4()),
        tool_name="delete_file",
        args={"path": "/tmp/x"},
        risk_tier=int(RiskTier.HIGH),
        escalation_reasons=[],
        agent_context="test context",
    )

    decision_obj = ApprovalDecision(
        request_id=req.request_id,
        decision=Decision.APPROVE,
        edited_args=None,
        reviewer_note="looks fine",
    )

    # Resolve concurrently
    async def resolver():
        await asyncio.sleep(0.05)
        await queue.resolve(req.request_id, decision_obj)

    asyncio.create_task(resolver())
    result = await queue.submit(req)
    assert result.decision == Decision.APPROVE

@pytest.mark.asyncio
async def test_timeout_returns_timeout_decision():
    queue = ApprovalQueue(timeout_seconds=0.1)  # 100ms timeout
    from agentic.harness_optimizer.hitl.approval_queue import ApprovalRequest
    import uuid

    req = ApprovalRequest(
        request_id=str(uuid.uuid4()),
        tool_name="api_post",
        args={"url": "https://example.com", "payload": {}},
        risk_tier=int(RiskTier.MEDIUM),
        escalation_reasons=[],
        agent_context="test",
    )

    result = await queue.submit(req)
    assert result.decision == Decision.TIMEOUT

@pytest.mark.asyncio
async def test_resolve_unknown_id_returns_false():
    queue = ApprovalQueue()
    result = await queue.resolve("nonexistent-id", ApprovalDecision(
        request_id="nonexistent-id",
        decision=Decision.APPROVE,
        edited_args=None,
        reviewer_note="",
    ))
    assert result is False


# ── Decorator integration tests ───────────────────────────────────────────────

@pytest.mark.asyncio
async def test_blocked_tool_raises_immediately():
    queue = ApprovalQueue()
    init_approval_queue(queue)

    @require_approval(override_tier=RiskTier.BLOCK)
    async def dangerous_tool(cmd: str) -> str:
        return cmd

    with pytest.raises(ToolBlockedError):
        await dangerous_tool("rm -rf /")

@pytest.mark.asyncio
async def test_auto_allow_executes_without_queue():
    queue = ApprovalQueue()
    init_approval_queue(queue)

    @require_approval()
    async def read_file(path: str) -> str:  # type: ignore
        return f"contents of {path}"

    result = await read_file("/tmp/safe.txt")
    assert "contents" in result
    assert queue.pending_count == 0  # never hit the queue

@pytest.mark.asyncio
async def test_reject_raises_tool_rejected():
    queue = ApprovalQueue(timeout_seconds=5)
    init_approval_queue(queue)
    from agentic.harness_optimizer.hitl.approval_queue import ApprovalRequest

    @require_approval()
    async def delete_file(path: str) -> str:  # type: ignore
        return f"deleted {path}"

    async def rejector():
        req: ApprovalRequest = await queue.next_pending()
        await queue.resolve(req.request_id, ApprovalDecision(
            request_id=req.request_id,
            decision=Decision.REJECT,
            edited_args=None,
            reviewer_note="not safe",
        ))

    asyncio.create_task(rejector())
    with pytest.raises(ToolRejectedError):
        await delete_file("/tmp/important.txt")

@pytest.mark.asyncio
async def test_edit_dispatches_with_new_args():
    queue = ApprovalQueue(timeout_seconds=5)
    init_approval_queue(queue)
    from agentic.harness_optimizer.hitl.approval_queue import ApprovalRequest

    received_path: list[str] = []

    @require_approval()
    async def write_file(path: str, content: str) -> str:  # type: ignore
        received_path.append(path)
        return f"wrote to {path}"

    async def editor():
        req: ApprovalRequest = await queue.next_pending()
        await queue.resolve(req.request_id, ApprovalDecision(
            request_id=req.request_id,
            decision=Decision.EDIT,
            edited_args={"path": "/tmp/safe_override.txt", "content": "sanitized"},
            reviewer_note="redirected to safe path",
        ))

    asyncio.create_task(editor())
    result = await write_file("/tmp/dangerous.txt", "original content")
    assert received_path[0] == "/tmp/safe_override.txt"
    assert "safe_override" in result

@pytest.mark.asyncio
async def test_timeout_raises_tool_timeout():
    queue = ApprovalQueue(timeout_seconds=0.05)
    init_approval_queue(queue)

    @require_approval()
    async def api_post(url: str, payload: dict) -> str:  # type: ignore
        return "ok"

    with pytest.raises(ToolTimeoutError):
        await api_post("https://example.com", {"key": "val"})

@pytest.mark.asyncio
async def test_edit_with_invalid_args_raises_rejected():
    queue = ApprovalQueue(timeout_seconds=5)
    init_approval_queue(queue)
    from agentic.harness_optimizer.hitl.approval_queue import ApprovalRequest

    @require_approval()
    async def write_file(path: str, content: str) -> str:  # type: ignore
        return "ok"

    async def bad_editor():
        req: ApprovalRequest = await queue.next_pending()
        await queue.resolve(req.request_id, ApprovalDecision(
            request_id=req.request_id,
            decision=Decision.EDIT,
            edited_args={"nonexistent_param": "value"},  # wrong signature
            reviewer_note="bad edit",
        ))

    asyncio.create_task(bad_editor())
    with pytest.raises(ToolRejectedError, match="don't match signature"):
        await write_file("/tmp/f.txt", "content")

Architecture Notes

Why this design fits CyClaw's invariants:

Concern How it's handled
Module isolation (Invariant 13) Entire package lives in agentic/; zero imports touch gate.py/graph.py [1]
Subprocess boundary example_tools.py's main() is the CLI entrypoint — spawned by ops_runner.py via subprocess.run [1]
No secrets in logs audit.py SHA-256 hashes all arg dicts, never logs raw values [2]
Deadlock prevention asyncio.wait_for + asyncio.shield — timeout always resolves, never hangs [3]
deepagents interrupt_config wiring interrupt_config={"delete_file": {"allow_edit": True, "allow_respond": True, "allow_accept": True}} on create_deep_agent calls the same HITL path [4]
LangGraph interrupt() compatibility The queue pattern is compatible with Command(resume={"type": "accept"}) — just swap _prompt_decision for a coroutine that calls graph.invoke(Command(resume=...)) [5][6]
Double-execution problem Approval node only calls interrupt() — no side effects before it, so re-execution on resume is a clean no-op [6]

Integration with deepagents.interrupt_config (when Phase 6 lands):

from deepagents import create_deep_agent
from langgraph.checkpoint.sqlite import SqliteSaver  # local, no Postgres needed

agent = create_deep_agent(
    tools=[delete_file, write_file, read_file, api_post],
    instructions="CyClaw GitHub harness...",
    interrupt_config={
        "delete_file": {"allow_ignore": False, "allow_respond": True,
                        "allow_edit": True, "allow_accept": True},
        "api_post":    True,  # shorthand: all options enabled
    },
    checkpointer=SqliteSaver.from_conn_string("data/agentic/hitl_checkpoints.db"),
)

The @require_approval decorator and interrupt_config are complementary, not redundant: the decorator enforces the risk gate at the Python call-site (works without deepagents), while interrupt_config wires the deepagents harness interrupt mechanism (requires checkpointer).[4][7]

Sources
[1] Analyze the import graph of the CyClaw repository to verify the architectural invariant where gate.py, graph.py, and mcp_hybrid_server.py do not import the agentic layer (GitHub/FS/SQL/Dropbox/NeMo). Generate a Mermaid dependency diagram, identify any circular or forbidden imports, and propose a refactoring plan to move agentic dependencies into a registry or dependency-injection pattern to ensure clean decoupling for subagent middleware management https://www.perplexity.ai/search/71b682d2-ebeb-4d98-a11b-0069c55a3e26
[2] Random infographic daily

load research and @html skills and output reply as both text reply and downloadable robust html mobile web app

Generate an interesting infographic based on or inspired by memory: my following interests:
, history, ...

...e topics) - also include a detailed summary to begin with that gives a holistic overview of the topic and includes a brief table of contents if applicable - (verify 100% accuracy/ precision and spelling/grammer)

Topic: CyClaw via GitHub connector https://www.perplexity.ai/search/ac497761-b7a5-4e69-a843-3a513ca3554f
[3] [Python] Setting Timeouts for asyncio.Queue Operations | 森の株 https://morinokabu.com/2026/01/14/python-asyncio-queue-timeout-management/
[4] Deep Agents - deepagents · PyPI https://pypi.org/project/deepagents/0.0.5/
[5] LangGraph State Management: Checkpoints, Thread State, and ... https://eastondev.com/blog/en/posts/ai/20260424-langgraph-agent-architecture/
[6] LangGraph Human in the Loop: A Complete Tutorial (With ... https://thehandover.xyz/blog/langgraph-human-in-the-loop-tutorial
[7] deep-agents-orchestration — AI agent skill | explainx.ai | explainx.ai https://explainx.ai/skills/langchain-ai/langchain-skills/deep-agents-orchestration
[8] LangchainIntegrationPlan.md https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/2912707/a4610b72-ab51-465c-a6f3-e3eca1b2b215/LangchainIntegrationPlan.md?AWSAccessKeyId=ASIA2F3EMEYEVDDB4RPE&Signature=FKm%2BXOILqZF5beqv27rTtD2CC4c%3D&x-amz-security-token=IQoJb3JpZ2luX2VjEOr%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMSJHMEUCIQCr1kXGyv0o5%2BUEJJsj17%2FxCgPAbiX%2BchAM0U8U8YOPfQIgAUCOvcn0qwfspo5BCee3tKwMfsUuOupdqwWjeY1fZ6Aq%2FAQIs%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARABGgw2OTk3NTMzMDk3MDUiDJAO1wpoxqhikdhe3yrQBOemryh5vVIlx0Ky79tStk5JVcm79%2F%2BfWUiJUNTa%2B%2BNiYxZcZzYU7TcvaebFxqNXSsh9l5UtN4u0dDXrw7%2BgJecQovBh%2FjfGkM2fsOK48u3kbOUN0sPhlkvhttu%2FrjlE3VXAmJIqVRZ2mI0kLNBibm8AT1jHIrf%2BE%2FVBTmGZBLzdVwX4stNw4XdTtMh4dhGcsGBSUaICMfgtCakydEhRr5JPzxPzRSQ30Iv6Ay1RRNV1t9YMkyVG13lwQYUTbTEQLs9%2FbqA9RK9cNLPerBbbfOeiU1SEKtgFGdo%2F1175Za6QA2dpsoKcrziuZXQo%2Bk9JkvFZl%2FKZ3x2RWMbJ3iU3Abb2uZogYqf%2BI7EQFlYPfmjKRlkjTqLXTiU48AA7WdXpmcq%2B454zED7lZYt9zmi0ujVJE%2BQA5KbQg946AvejNu3gsxCqoD8wfE1sO51iHk%2FXL0oNRb8zG%2BlAYsfyKxVFxesOQh1XImsska5fxfML6c0ixvZ4N25EEfdhMqXDYJmBqAPH46LffUwJRPjMvoo%2FoA3O%2Fg9YiJ8aIER0meWkG2M1StHC6mxiSDbcCQ6HIQwrZXmXz02Dahlyr%2FX0ZjwrWJULriTEGSxWgYGFfqGfVvs4mFtXH79Zq2eCRs4B0cbLBnL0sFeunFhzEbGXgljEQHc0rdi0KgzGBOQEx9IF9bxXtI1sbUAe1kCJ2TftP54N2P1OqySTV6G%2BcYDKBwLCQy%2FiVqy%2FSZw8gx0iM%2F4UkU%2F7XB7VYMUYuEnc81EsVGcmFprpG5%2F%2FtnY9jqdKzQYTrKIwg%2FjC0gY6mAF2lPu9852Df2vQmIFFn5V6SD9KxIGfiSyX3lPuy%2BI4cigF3ujWBNl0Ti4bkJj1UWD13n%2BoV%2Fbmx5ibwTivn3PBFQaQmz7wE2YqtbeB1Gc23PgcVxEzpn4OrkoC3n%2BIPlnqgs2xbDKaWMr9KD5yEogqKm6XHfpHPuzW%2Ft7EherCDsKTJfvccmbYbIVNyZmj6QfLZ4AWUDrlCg%3D%3D&Expires=1783679446
[9] CyClaw_Swarm_Verification_Report_2026-07-09.pdf https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/collection_bb753fae-2826-4344-be5a-7a4bfcfb2760/e0dcc5c1-b3f5-4171-8f15-6df2022f7e25/CyClaw_Swarm_Verification_Report_2026-07-09.pdf?AWSAccessKeyId=ASIA2F3EMEYEVDDB4RPE&Signature=IRLAKDw4N21%2Bql5JJ1sHhHGf%2B5Y%3D&x-amz-security-token=IQoJb3JpZ2luX2VjEOr%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMSJHMEUCIQCr1kXGyv0o5%2BUEJJsj17%2FxCgPAbiX%2BchAM0U8U8YOPfQIgAUCOvcn0qwfspo5BCee3tKwMfsUuOupdqwWjeY1fZ6Aq%2FAQIs%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARABGgw2OTk3NTMzMDk3MDUiDJAO1wpoxqhikdhe3yrQBOemryh5vVIlx0Ky79tStk5JVcm79%2F%2BfWUiJUNTa%2B%2BNiYxZcZzYU7TcvaebFxqNXSsh9l5UtN4u0dDXrw7%2BgJecQovBh%2FjfGkM2fsOK48u3kbOUN0sPhlkvhttu%2FrjlE3VXAmJIqVRZ2mI0kLNBibm8AT1jHIrf%2BE%2FVBTmGZBLzdVwX4stNw4XdTtMh4dhGcsGBSUaICMfgtCakydEhRr5JPzxPzRSQ30Iv6Ay1RRNV1t9YMkyVG13lwQYUTbTEQLs9%2FbqA9RK9cNLPerBbbfOeiU1SEKtgFGdo%2F1175Za6QA2dpsoKcrziuZXQo%2Bk9JkvFZl%2FKZ3x2RWMbJ3iU3Abb2uZogYqf%2BI7EQFlYPfmjKRlkjTqLXTiU48AA7WdXpmcq%2B454zED7lZYt9zmi0ujVJE%2BQA5KbQg946AvejNu3gsxCqoD8wfE1sO51iHk%2FXL0oNRb8zG%2BlAYsfyKxVFxesOQh1XImsska5fxfML6c0ixvZ4N25EEfdhMqXDYJmBqAPH46LffUwJRPjMvoo%2FoA3O%2Fg9YiJ8aIER0meWkG2M1StHC6mxiSDbcCQ6HIQwrZXmXz02Dahlyr%2FX0ZjwrWJULriTEGSxWgYGFfqGfVvs4mFtXH79Zq2eCRs4B0cbLBnL0sFeunFhzEbGXgljEQHc0rdi0KgzGBOQEx9IF9bxXtI1sbUAe1kCJ2TftP54N2P1OqySTV6G%2BcYDKBwLCQy%2FiVqy%2FSZw8gx0iM%2F4UkU%2F7XB7VYMUYuEnc81EsVGcmFprpG5%2F%2FtnY9jqdKzQYTrKIwg%2FjC0gY6mAF2lPu9852Df2vQmIFFn5V6SD9KxIGfiSyX3lPuy%2BI4cigF3ujWBNl0Ti4bkJj1UWD13n%2BoV%2Fbmx5ibwTivn3PBFQaQmz7wE2YqtbeB1Gc23PgcVxEzpn4OrkoC3n%2BIPlnqgs2xbDKaWMr9KD5yEogqKm6XHfpHPuzW%2Ft7EherCDsKTJfvccmbYbIVNyZmj6QfLZ4AWUDrlCg%3D%3D&Expires=1783679446
[10] CyClaw_Architecture_Guide_v1.9.0_crisp.pdf https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/collection_bb753fae-2826-4344-be5a-7a4bfcfb2760/4e2a11ff-ab32-4e8f-b3dc-1efbd4149ef1/CyClaw_Architecture_Guide_v1.9.0_crisp.pdf?AWSAccessKeyId=ASIA2F3EMEYEVDDB4RPE&Signature=YQvZAGALqRH3nY6SaXDOajMtvh4%3D&x-amz-security-token=IQoJb3JpZ2luX2VjEOr%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMSJHMEUCIQCr1kXGyv0o5%2BUEJJsj17%2FxCgPAbiX%2BchAM0U8U8YOPfQIgAUCOvcn0qwfspo5BCee3tKwMfsUuOupdqwWjeY1fZ6Aq%2FAQIs%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARABGgw2OTk3NTMzMDk3MDUiDJAO1wpoxqhikdhe3yrQBOemryh5vVIlx0Ky79tStk5JVcm79%2F%2BfWUiJUNTa%2B%2BNiYxZcZzYU7TcvaebFxqNXSsh9l5UtN4u0dDXrw7%2BgJecQovBh%2FjfGkM2fsOK48u3kbOUN0sPhlkvhttu%2FrjlE3VXAmJIqVRZ2mI0kLNBibm8AT1jHIrf%2BE%2FVBTmGZBLzdVwX4stNw4XdTtMh4dhGcsGBSUaICMfgtCakydEhRr5JPzxPzRSQ30Iv6Ay1RRNV1t9YMkyVG13lwQYUTbTEQLs9%2FbqA9RK9cNLPerBbbfOeiU1SEKtgFGdo%2F1175Za6QA2dpsoKcrziuZXQo%2Bk9JkvFZl%2FKZ3x2RWMbJ3iU3Abb2uZogYqf%2BI7EQFlYPfmjKRlkjTqLXTiU48AA7WdXpmcq%2B454zED7lZYt9zmi0ujVJE%2BQA5KbQg946AvejNu3gsxCqoD8wfE1sO51iHk%2FXL0oNRb8zG%2BlAYsfyKxVFxesOQh1XImsska5fxfML6c0ixvZ4N25EEfdhMqXDYJmBqAPH46LffUwJRPjMvoo%2FoA3O%2Fg9YiJ8aIER0meWkG2M1StHC6mxiSDbcCQ6HIQwrZXmXz02Dahlyr%2FX0ZjwrWJULriTEGSxWgYGFfqGfVvs4mFtXH79Zq2eCRs4B0cbLBnL0sFeunFhzEbGXgljEQHc0rdi0KgzGBOQEx9IF9bxXtI1sbUAe1kCJ2TftP54N2P1OqySTV6G%2BcYDKBwLCQy%2FiVqy%2FSZw8gx0iM%2F4UkU%2F7XB7VYMUYuEnc81EsVGcmFprpG5%2F%2FtnY9jqdKzQYTrKIwg%2FjC0gY6mAF2lPu9852Df2vQmIFFn5V6SD9KxIGfiSyX3lPuy%2BI4cigF3ujWBNl0Ti4bkJj1UWD13n%2BoV%2Fbmx5ibwTivn3PBFQaQmz7wE2YqtbeB1Gc23PgcVxEzpn4OrkoC3n%2BIPlnqgs2xbDKaWMr9KD5yEogqKm6XHfpHPuzW%2Ft7EherCDsKTJfvccmbYbIVNyZmj6QfLZ4AWUDrlCg%3D%3D&Expires=1783679446
[11] Human-in-the-loop - Docs by LangChain中文 https://www.langchain-cn.com/oss/python/deepagents/human-in-the-loop
[12] Human-in-the-loop - Docs by LangChain https://docs.langchain.com/oss/python/deepagents/human-in-the-loop
[13] Interrupts - Docs by LangChain https://docs.langchain.com/oss/python/langgraph/interrupts
[14] create_deep_agent | deepagents https://reference.langchain.com/python/deepagents/graph/create_deep_agent
[15] Responding to interrupts https://docs.langchain.com/oss/python/langchain/human-in-the-loop
[16] deep-agents-orchestration - Remote OpenClaw https://www.remoteopenclaw.com/skills/langchain-ai/langchain-skills/deep-agents-orchestration
[17] Human-in-the-Loop Workflows with LangGraph - Abstract Algorithms https://www.abstractalgorithms.dev/langgraph-human-in-the-loop
[18] How Agent Frameworks Solve Human-in-the-Loop https://dzone.com/articles/agent-frameworks-human-loop
[19] Customize Deep Agents - Docs by LangChain https://docs.langchain.com/oss/python/deepagents/customization
[20] HumanInTheLoopMiddleware - LangChain Reference https://reference.langchain.com/python/langchain/agents/middleware/human_in_the_loop/HumanInTheLoopMiddleware
[21] interrupt | langgraph - LangChain Reference https://reference.langchain.com/python/langgraph/types/interrupt
[22] Interrupts and Commands in LangGraph: Building Human-in-the ... https://dev.to/jamesbmour/interrupts-and-commands-in-langgraph-building-human-in-the-loop-workflows-4ngl
[23] langchain-ai/langgraph-human-in-the-loop | Decision Hub https://hub.decision.ai/skills/langchain-ai/langgraph-human-in-the-loop
[24] LangGraph Interrupt and Resume Workflow - Part 4/22 https://www.youtube.com/watch?v=PYH7Mn4pXUc
[25] Human-in-the-Loop Authorization in Python with LangGraph ... - Auth0 https://auth0.com/blog/async-ciba-python-langgraph-auth0/
[26] Making it easier to build human-in-the-loop agents with interrupt https://www.langchain.com/blog/making-it-easier-to-build-human-in-the-loop-agents-with-interrupt
[27] Permissions - Docs by LangChain中文 https://www.langchain-cn.com/oss/python/deepagents/permissions
[28] Context management https://docs.langchain.com/oss/javascript/deepagents/overview
[29] HITL Patterns in LangGraph: Approve, Reject, and Edit ... - Medium https://medium.com/the-advanced-school-of-ai/human-in-the-loop-in-langgraph-approve-or-reject-pattern-fcf6ba0c5990
[30] How to Build Human-in-the-Loop Plan-and-Execute AI Agents with ... https://www.marktechpost.com/2026/02/16/how-to-build-human-in-the-loop-plan-and-execute-ai-agents-with-explicit-user-approval-using-langgraph-and-streamlit/
[31] Harness capabilities - Docs by LangChain中文 https://www.langchain-cn.com/oss/python/deepagents/harness
[32] LangGraph (Part 4): Human-in-the-Loop for Reliable AI Workflows https://medium.com/@sitabjapal03/langgraph-part-4-human-in-the-loop-for-reliable-ai-workflows-aa4cc175bce4
[33] Interrupt on a specific tool when using Langgraph ReAct ... https://github.com/langchain-ai/langgraph/discussions/929
[34] Coroutines and tasks — Python 3.14.6 documentation https://docs.python.org/3/library/asyncio-task.html
[35] cpython/Lib/asyncio/timeouts.py at main · python/cpython https://github.com/python/cpython/blob/main/Lib/asyncio/timeouts.py
[36] LangGraph Advanced – Directly Integrate Human-in-the- ... https://lilys.ai/en/notes/ai-agent-20251017/langgraph-advanced-human-in-loop-tools
[37] GitHub - MatteoH2O1999/timeout_decorator: Timeout decorator for Python synchronous and asynchronous functions https://github.com/MatteoH2O1999/timeout_decorator
[38] Human-in-the-Loop Systems | esurovtsev/langgraph-advanced ... https://deepwiki.com/esurovtsev/langgraph-advanced/4-human-in-the-loop-systems
[39] Coroutine Timeout Injection in Python: A Decorator ... https://medium.com/@RampantLions/coroutine-timeout-injection-in-python-a-decorator-approach-with-asyncio-wait-for-171dc0a3f5be
[40] Decorator to facilitate sync and async calls to one function https://discuss.python.org/t/decorator-to-facilitate-sync-and-async-calls-to-one-function/78986
[41] Can we make asyncio event.wait with timeout atomic? - Ideas https://discuss.python.org/t/can-we-make-asyncio-event-wait-with-timeout-atomic/106049
[42] tessl/pypi-langgraph-prebuilt@0.6.x - Registry - Tessl https://tessl.io/registry/tessl/pypi-langgraph-prebuilt/0.6.0/files/docs/agent-creation.md
[43] aiodecorator - PyPI https://pypi.org/project/aiodecorator/
[44] Queues — Python 3.14.6 documentation https://docs.python.org/3/library/asyncio-queue.html
[45] йҳҹеҲ— https://docs.python.org/zh-cn/dev/library/asyncio-queue.html
[46] async-timeout https://pypi.org/project/async-timeout/
[47] LangGraph's interrupt() Function: The Simpler Way to Build ... https://medium.com/@areebahmed575/langgraphs-interrupt-function-the-simpler-way-to-build-human-in-the-loop-agents-faef98891a92
[48] Add human intervention - GitHub Pages https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/add-human-in-the-loop/
[49] Human-in-the-Loop with LangGraph: Approval Gates and ... https://callsphere.ai/blog/langgraph-human-in-the-loop-approval-gates-manual-intervention
[50] human in the loop · Issue #2732 · langchain-ai/langgraph langchain-ai/langgraph#2732
[51] Human-in-the-Loop - Docs by LangChain https://docs.langchain.com/oss/python/langchain/frontend/human-in-the-loop
[52] How to Build Autonomous Multi-Step AI Systems(Part-2) https://medium.com/@dharamai2024/mastering-langchain-deep-agent-how-to-build-autonomous-multi-step-ai-systems-part-2-b223f0a30d96
[53] deepagents · PyPI https://pypi.org/project/deepagents/0.3.4/
[54] Crash course in asyncio - Ben Dixon https://www.bendixon.net/blog/2025/crash-course-in-asyncio/
[55] 自定义 Deep Agents - Docs by LangChain中文 https://www.langchain-cn.com/oss/python/deepagents/customization
[56] Releases · langchain-ai/deepagents - GitHub https://github.com/langchain-ai/deepagents/releases
[57] Prompt Engineering for AI Agents: 2026 Guide https://www.inflectra.com/Ideas/Topic/AI-Agent-Prompt-Engineering.aspx
[58] Tool calling - Docs by LangChain https://docs.langchain.com/oss/javascript/langchain/frontend/tool-calling
[59] How to Secure LangChain Agents in Production https://forum.langchain.com/t/how-we-add-runtime-security-to-langchain-agents-in-production/3461
[60] How to Build Production-Ready AI Agents with Deep Agents - Milvus Blog https://milvus.io/blog/how-to-build-productionready-ai-agents-with-deep-agents-and-milvus.md
[61] How to Build Asyncio Queues in Python - OneUptime https://oneuptime.com/blog/post/2026-01-30-python-asyncio-queues/view
[62] asyncio.Queue - any benefit in async put with maxsize? https://stackoverflow.com/questions/79892703/asyncio-queue-any-benefit-in-async-put-with-maxsize
[63] Asyncio Queue in Python - SuperFastPython https://superfastpython.com/asyncio-queue/
[64] Waiting in asyncio - Hynek Schlawack https://hynek.me/articles/waiting-in-asyncio/
[65] Document whether asyncio.wait_for(q.get(), timeout) is safe ... python/cpython#92824
[66] 【Python】asyncio.Queueの待機処理にタイムアウトを設定する https://note.com/morinokabu/n/n88e3eb51a8dc
[67] When using 'interrupt' followed by 'new Command({ resume: ...})', get an undefined message error from LangChain + LangGraph https://stackoverflow.com/questions/79471648/when-using-interrupt-followed-by-new-command-resume-get-an-undefin
[68] wait_for timeout to over come asyncio.queue not having a timeout not working https://stackoverflow.com/questions/60695753/wait-for-timeout-to-over-come-asyncio-queue-not-having-a-timeout-not-working
[69] langgraph-agent-interrupt-1-reject-node.js https://gist.github.com/bguiz/d0d5d3b732f9395c1a6817700fd5a311
[70] Using asyncio.Queue for producer-consumer flow https://stackoverflow.com/questions/52582685/using-asyncio-queue-for-producer-consumer-flow
[71] How to review tool calls¶ https://langchain-ai.github.io/langgraphjs/cloud/how-tos/human_in_the_loop_review_tool_calls/
[72] Python asyncio.wait_for(): Cancel a Task with a Timeout https://www.pythontutorial.net/python-concurrency/python-asyncio-wait_for/
[73] Human-in-the-Loop Tool Calling with LangGraph https://sangeethasaravanan.medium.com/human-in-the-loop-tool-calling-with-langgraph-building-interruptible-ai-agents-fd0275ce4523
[74] How to Build Human-in-the-Loop Agentic Workflows with LangGraph https://subagentic.ai/howtos/langgraph-human-in-the-loop-agentic-workflows/
[75] deepagents/libs/code/THREAT_MODEL.md at main · langchain-ai ... https://github.com/langchain-ai/deepagents/blob/main/libs/code/THREAT_MODEL.md
[76] Implementing LangGraph with Human-In-The-Loop https://medium.com/@sarthakpattanaik_4094/implementing-langgraph-with-human-in-the-loop-0f02bff6c453
[77] LangGraph in Production: Building Stateful AI Agents - Kalvium Labs https://www.kalviumlabs.ai/blog/langgraph-in-production-stateful-multi-step-agents/
[78] langchain-langgraph-human-in-loop - Agent Skill https://tonsofskills.com/skills/langchain-langgraph-human-in-loop/
[79] LangGraph's interrupt/resume primitives are genuinely ... - GitHub https://github.com/raed667/langgraph-hitl
[80] LangGraph HITL(Human In The Loop) | AI Korner - A Quick AI Skim https://www.linkedin.com/posts/ai-korner-a-quick-ai-skim_langgraph-hitlhuman-in-the-loop-activity-7442397353371873280-7OsD
[81] How I implemented human-in-the-loop with LangGraph's interrupt ... https://www.reddit.com/r/LangChain/comments/1s6qidj/how_i_implemented_humanintheloop_with_langgraphs/
[82] Human-in-the-Loop Workflows with LangGraph - Abstract Algorithms https://abstractalgorithms.dev/langgraph-human-in-the-loop
[83] asyncio : How to handle empty queue when you use non-blocking get()? https://stackoverflow.com/questions/66033949/asyncio-how-to-handle-empty-queue-when-you-use-non-blocking-get
[84] langgraph/docs/docs/cloud/how-tos/human_in_the_loop_breakpoint.md at main · langchain-ai/langgraph https://github.com/langchain-ai/langgraph/blob/main/docs/docs/cloud/how-tos/human_in_the_loop_breakpoint.md
[85] 同步原语— Python 3.12.13 文档 https://docs.python.org/zh-cn/3.12/library/asyncio-sync.html
[86] Synchronization Primitives https://docs.python.org/3.12/library/asyncio-sync.html
[87] Функция wait_for() модуля asyncio в Python https://docs-python.ru/standart-library/modul-asyncio-python/funktsija-wait-for-modulja-asyncio/
[88] GH-96764: rewrite asyncio.wait_for to use asyncio.timeout by kumaraditya303 · Pull Request #98518 · python/cpython python/cpython#98518
[89] Event Loop https://docs.python.org/3/library/asyncio-eventloop.html?highlight=asyncio+timeout
[90] asyncio.timeout() To Wait and Cancel Tasks - SuperFastPython https://superfastpython.com/asyncio-timeout/
[91] deepagents · PyPI https://pypi.org/project/deepagents/0.0.6/
[92] Deep Agents overview - Docs by LangChain https://docs.langchain.com/oss/python/deepagents/overview
[93] Agents - GitHub Pages https://langchain-ai.github.io/langgraph/reference/agents/
[94] Human-in-the-loop - Docs by LangChain中文 http://www.langchain-cn.com/oss/javascript/deepagents/human-in-the-loop
[95] Human-in-the-Loop with LangGraph Interrupts: Approve, Edit, Resume https://callsphere.ai/blog/td30-fw-langgraph-human-in-the-loop-interrupt-resume
[96] Human-in-the-loop Interrupt Flows Not Working with ... assistant-ui/assistant-ui#1899
[97] Human-in-the-loop approval dashboard for LangGraph agents https://forum.langchain.com/t/human-in-the-loop-approval-dashboard-for-langgraph-agents-open-source-free-to-deploy/3616
[98] LangGraph Durable Execution Patterns: Resilient AI Agents ... - 芝士貓 https://cheesecat.net/blog/langgraph-durable-execution-resilient-agents-implementation-guide-2026-zh-tw/
[99] EgoAlpha https://egoalpha.com/zh/DeepAgents/deepagents-7-10.html
[100] langgraph-human-in-the-loop - Remote OpenClaw https://www.remoteopenclaw.com/skills/langchain-ai/langchain-skills/langgraph-human-in-the-loop
[101] Add human intervention https://langchain-ai.lang.chat/langgraph/how-tos/human_in_the_loop/add-human-in-the-loop/
[102] HumanInterruptConfig | @langchain/langgraph https://reference.langchain.com/javascript/langchain-langgraph/prebuilt/HumanInterruptConfig
[103] Python asyncio: async/await Explained https://www.w3docs.com/learn-python/python-asyncio
[104] agent-inbox-langgraph-example/src/agent/graph.py at main · langchain-ai/agent-inbox-langgraph-example https://github.com/langchain-ai/agent-inbox-langgraph-example/blob/main/src/agent/graph.py
[105] langchain/langgraph https://app.unpkg.com/@langchain/langgraph@1.0.0/files/dist/prebuilt/interrupt.d.ts.map

--

@claude review this comment thoroughly and reply in Claude code session - I'm tempted to finish the initial plans phases through 7 or 8 then verifying if it works outside of testing sandbox - if it's clearly wrong lemme know



What

Docs-only reconciliation of the two new root-level LangChain notes with repo truth. Your original prose is untouched — this adds a clearly-delimited dated preface and replaces one stale duplicate:

  1. LangChainFix.md — a "Reconciliation with shipped code (2026-07-10)" preface correcting four points of drift before anyone implements from it:
    • "Security Invariant 13" → I6 (module isolation); the repo defines exactly six invariants (CLAUDE.md §3, INVARIANTS.md Rule 9).
    • The env-var-driven GithubAgentConfig sketch contradicts the config.yaml-single-source-of-truth rule; the shipped agentic/deepagent_github/config.py already loads validated config via load_agentic_config().
    • The audit.py sketch's raw open("audit.jsonl", "a") bypasses utils/logger.audit_log (SHA-256 hashing, redaction, config-driven path) — an unredacted side channel.
    • A phase 0-5 scaffold already exists at agentic/deepagent_github/ with a different design; the proposed layout would overwrite shipped files (pointer to the plan doc's inventory section).
  2. LangchainIntegrationPlan.md — this 507-line root file was a pre-implementation snapshot of the canonical docs/agentic/GITHUB_DEEP_AGENT_HARNESS_OPTIMIZER_PLAN.md and has diverged (it still calls the shipped layout "proposed only"; it lacks the inventory sections). Reduced to an 8-line pointer. This deletes duplicated content you wrote — shipped as a draft PR precisely so you approve or reject that call.

Why / benefit

Two sources of truth guarantee drift; a future implementer (human or agent) following "Invariant 13"/env-var config/raw audit appends would produce work that violates the repo's actual contracts. Doc fixes pass the feature-freeze bar.

Risk to monitor

None runtime (markdown only). doc-sync shows the same 2 pre-existing drift items (D1 skills list, D4 pattern count) before and after — zero new drift added. If you'd rather keep the full LangchainIntegrationPlan.md snapshot, reject just that file's change and I'll re-cut with the preface only.


Generated by Claude Code

…plan

LangChainFix.md gains a dated correction preface: 'Invariant 13' is I6
(module isolation, six invariants total); config is config.yaml-driven via
load_agentic_config, not env vars; audit must route through
utils/logger.audit_log (hashing + redaction), not raw jsonl appends; and a
phase 0-5 scaffold already exists at agentic/deepagent_github/ with a
different design. LangchainIntegrationPlan.md (stale pre-implementation
snapshot of the canonical plan doc) is reduced to a pointer at
docs/agentic/GITHUB_DEEP_AGENT_HARNESS_OPTIMIZER_PLAN.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3WhoiUiUak84jUmiBhhvA
@cgfixit

cgfixit commented Jul 10, 2026

Copy link
Copy Markdown
Owner Author

Here's the full aggregated action plan as a downloadable .md.

What it contains, phase by phase:

Phase 6 — Two concrete steps before touching any agent code: annotate all 6 dead files with a structured # TODO(unwired) block (intended import path + planned consumer + blocker), and flip draft_plan() to raise NotImplementedError so it fails loud instead of silently returning garbage.

Phase 7 — The real meat: drop-in build_subagent() replacement with tool registry resolution, ValueError on bare strings, and _created=True only after validation passes. Three pytest cases that will catch the original builder.py:94-99 bug on regression. Plus the canonical GITHUB_HITL_POLICY dict wired to interrupt_on + checkpointer, with the full resume pattern.[1][2]

Phase 8 — Qwen/Kimi K2 model swap decision table (protocol compatibility vs cost per task), with the specific max_tokens cap warning for Qwen extended thinking.[3]

Phase 9 — HITL decorator deferred with an explicit gate condition: Phase 7 interrupt_on must have approve + reject + timeout CI cases passing before the decorator layer is worth adding. Includes the integration steps for wiring the async queue, dashboard, and audit log.[4][5]

Bottom line on sequencing: Don't touch the decorator until Phase 7 is green. Inside the harness, interrupt_on is the correct and sufficient control plane — the decorator is for tools that live outside deepagents.[1]

Sources
[1] Human-in-the-loop - Docs by LangChain https://docs.langchain.com/oss/python/deepagents/human-in-the-loop
[2] HumanInTheLoopMiddleware - LangChain Reference https://reference.langchain.com/python/langchain/agents/middleware/human_in_the_loop/HumanInTheLoopMiddleware
[3] Benefits of doing so (agentic capabilities bootstrap or better integration due to it being by langchain?) https://www.perplexity.ai/search/5c4d4e04-22c8-4009-af7e-f9347d86cee0
[4] Built a production incident response agent with LangGraph the interrupt() checkpoint pattern was the key https://www.reddit.com/r/LangChain/comments/1t2wiog/built_a_production_incident_response_agent_with/
[5] Building AI Agents with LangGraph: From Prototype to Production https://www.abhishekchauhan.it/blog/building-ai-agents-langgraph-production
[6] langchain-ai/langgraph-human-in-the-loop | Decision Hub https://hub.decision.ai/skills/langchain-ai/langgraph-human-in-the-loop
[7] Customize Deep Agents - Docs by LangChain https://docs.langchain.com/oss/python/deepagents/customization
[8] Interrupts - Docs by LangChain https://docs.langchain.com/oss/javascript/langgraph/interrupts
[9] InterruptOnConfig | langchain | LangChain Reference https://reference.langchain.com/python/langchain/agents/middleware/human_in_the_loop/InterruptOnConfig
[10] interrupt_on - LangChain Reference Docs https://reference.langchain.com/python/langchain/agents/middleware/human_in_the_loop/HumanInTheLoopMiddleware/interrupt_on
[11] LangGraph Interrupt and Resume Workflow - Part 1/22 https://www.youtube.com/watch?v=NrUVOH8zssk
[12] tessl/pypi-langgraph@1.0.x - Registry https://tessl.io/registry/tessl/pypi-langgraph/1.0.0/files/docs/interrupts.md
[13] How Agent Frameworks Solve Human-in-the-Loop https://dzone.com/articles/agent-frameworks-human-loop
[14] LangGraph Ep12: Breakpoints & Human Approval (interrupt_before) https://www.youtube.com/watch?v=ybHj0MO6cHU
[15] LangChain: .../middleware/human_in_the_loop.py - Fossies https://fossies.org/linux/langchain-langchain-core/libs/langchain_v1/langchain/agents/middleware/human_in_the_loop.py
[16] humanInTheLoopMiddleware - LangChain Reference https://reference.langchain.com/javascript/langchain/index/humanInTheLoopMiddleware
[17] subagents.py - middleware - GitHub https://github.com/langchain-ai/deepagents/blob/main/libs/deepagents/deepagents/middleware/subagents.py
[18] How to Build Asyncio Queues in Python - OneUptime https://oneuptime.com/blog/post/2026-01-30-python-asyncio-queues/view
[19] Python Asyncio producer-consumer workflow congestion / growing ... https://stackoverflow.com/questions/66563012/python-asyncio-producer-consumer-workflow-congestion-growing-queue
[20] queues with Python asyncio - IDisposable Thoughts https://cprieto.com/posts/2021/07/queues-with-python-asyncio.html
[21] Can asyncio.Queue be safely created outside of the event loop ... https://discuss.python.org/t/can-asyncio-queue-be-safely-created-outside-of-the-event-loop-thread/49215
[22] Is there a way to use asyncio.Queue in multiple threads? https://stackoverflow.com/questions/32889527/is-there-a-way-to-use-asyncio-queue-in-multiple-threads
[23] GitHub - aio-libs/janus: Thread-safe asyncio-aware queue for Python https://github.com/aio-libs/janus
[24] How could I implement the same as Python's async queue? https://users.rust-lang.org/t/how-could-i-implement-the-same-as-pythons-async-queue/66352
[25] Python's asyncio: A Hands-On Walkthrough https://realpython.com/async-io-python/
[26] asyncio.Queue - any benefit in async put with maxsize? https://stackoverflow.com/questions/79892703/asyncio-queue-any-benefit-in-async-put-with-maxsize
[27] Async Python — The Agentic AI Field Guide https://menuagentic.com/field-guide/async-python/
[28] Using AgentGen with LangChain: The Complete Integration ... https://www.agent-gen.com/blog/langchain-agentgen-integration
[29] Coroutine Timeout Injection in Python: A Decorator ... https://medium.com/@RampantLions/coroutine-timeout-injection-in-python-a-decorator-approach-with-asyncio-wait-for-171dc0a3f5be
[30] LangGraph Interrupt and Resume Workflow - Part 13/22 https://www.youtube.com/watch?v=PbRq52F7YtM
[31] Building and Using Tools in LangChain | by Warishayat https://medium.com/@warishayat/building-and-using-tools-in-langchain-ad1cb0e16922
[32] Using the decorator https://python.langchain.com.cn/docs/modules/agents/tools/how_to/custom_tools
[33] Python 2026 Significant Changes Guide - Microsoft Learn https://learn.microsoft.com/en-us/agent-framework/support/upgrade/python-2026-significant-changes
[34] StructuredTool | langchain_core - LangChain Reference https://reference.langchain.com/python/langchain-core/tools/structured/StructuredTool
[35] LangGraph Durable Execution Patterns: Resilient AI Agents ... - 芝士貓 https://cheesecat.net/blog/langgraph-durable-execution-resilient-agents-implementation-guide-2026-zh-tw/
[36] StructuredTool 数据类 https://www.langchain.asia/modules/tools/custom_tools
[37] 如何创建工具 - ️ LangChain 框架 https://python.langchain.ac.cn/docs/how_to/custom_tools/
[38] Interrupts - Docs by LangChain https://docs.langchain.com/oss/python/langgraph/interrupts
[39] Resume Interrupts Programmatically - LangGraph https://forum.langchain.com/t/resume-interrupts-programmatically/2541
[40] How to create a Python decorator that can wrap either coroutine or ... https://stackoverflow.com/questions/44169998/how-to-create-a-python-decorator-that-can-wrap-either-coroutine-or-function
[41] functools — Higher-order functions and operations on callable ... https://docs.python.org/3/library/functools.html
[42] Python Decorators: @wraps & Factories https://www.syntaxcache.com/python/decorators
[43] Async Patterns: Modern... https://sesamedisk.com/modern-python-decorators-context-managers-2026/
[44] Python Decorator Tracing for LLM Apps in 2026: Patterns and Pitfalls https://futureagi.com/blog/python-decorator-tracing-llm-2026/
[45] [Python Async Decorator] #python #asyncio #decorator - GitHub Gist https://gist.github.com/Integralist/fb1b5dbb6271632298f44d62a2221905
[46] Fancy Decorators https://realpython.com/primer-on-python-decorators/
[47] Decorators - Python Wiki https://wiki.python.org/moin/Decorators.html
[48] Pythonデコレータ徹底解説 – 基本から3.12新構文まで https://app-tatsujin.com/python-decorator-guide-3-12/
[49] cpython/Lib/functools.py at a46e96076898d126c9f276aef1934195aac34b4e · python/cpython https://github.com/python/cpython/blob/a46e96076898d126c9f276aef1934195aac34b4e/Lib/functools.py
[50] Using a coroutine as decorator https://stackoverflow.com/questions/42043226/using-a-coroutine-as-decorator
[51] OpenAPI-to-MCP Turns Every API Into an Agent Tool. The Missing Piece Is Endpoint-Level Policy https://www.permit.io/blog/openapi-to-mcp-endpoint-level-policy
[52] The Comprehensive Guide to Python Decorators https://asyncmove.com/blog/2026/01/the-comprehensive-guide-to-python-decorators/
[53] decorator - PyPI Package | Hextrap https://hextrap.com/r/pkg/pypi/decorator/
[54] functools.wraps for classes (intermediate) anthony explains #478 https://www.youtube.com/watch?v=rbEqoOIvFko

--

Action Plan (as opposed to the .md at project root labeled something with "langchaing" in it:

Based on everything gathered — CyClaw's subprocess isolation invariant, the deepagents interrupt_config/HumanInterruptConfig API, LangGraph's interrupt()/Command(resume=...) pattern, and your planning doc — here is the full production implementation. This respects CyClaw's hard rule: nothing below runs inside gate.py or graph.py. It lives entirely in agentic/harness_optimizer/hitl/ and is spawned as a subprocess via ops_runner.py.


File: agentic/harness_optimizer/hitl/risk_scorer.py

"""
Dynamic risk scorer for CyClaw HITL approval gate.
Classifies tool calls by sensitivity tier before routing to approval queue.
Zero external deps — pure stdlib + typing.
"""
from __future__ import annotations

import re
from dataclasses import dataclass, field
from enum import IntEnum
from typing import Any


class RiskTier(IntEnum):
    """Ordinal tiers map directly to routing policy."""
    AUTO_ALLOW  = 0   # read-only, no side effects
    LOW         = 1   # reversible writes
    MEDIUM      = 2   # external API non-destructive POSTs
    HIGH        = 3   # destructive, irreversible, or secret-touching
    BLOCK       = 4   # never allowed regardless of approval


# ── Tier classification rules ─────────────────────────────────────────────────
# Pattern matching runs in order; first match wins.
_TOOL_TIER_MAP: dict[str, RiskTier] = {
    # Explicit blocks — governance wall, no approval path
    "run_shell":              RiskTier.BLOCK,
    "exec_arbitrary":         RiskTier.BLOCK,
    "read_secret":            RiskTier.BLOCK,
    # High-risk destructive ops
    "delete_file":            RiskTier.HIGH,
    "rm_recursive":           RiskTier.HIGH,
    "gh_force_push":          RiskTier.HIGH,
    "drop_table":             RiskTier.HIGH,
    "revoke_token":           RiskTier.HIGH,
    # Medium — external API POSTs, reversible writes
    "gh_open_pr":             RiskTier.MEDIUM,
    "gh_create_branch":       RiskTier.MEDIUM,
    "gh_commit_and_push":     RiskTier.MEDIUM,
    "api_post":               RiskTier.MEDIUM,
    "write_file":             RiskTier.MEDIUM,
    "edit_file":              RiskTier.MEDIUM,
    "registry_apply":         RiskTier.MEDIUM,
    # Low — reversible, local, non-secret
    "write_current_file":     RiskTier.LOW,
    "finish_proposal":        RiskTier.LOW,
    # Auto-allow — pure reads
    "read_file":              RiskTier.AUTO_ALLOW,
    "ls":                     RiskTier.AUTO_ALLOW,
    "glob":                   RiskTier.AUTO_ALLOW,
    "grep":                   RiskTier.AUTO_ALLOW,
    "rag_search_readonly":    RiskTier.AUTO_ALLOW,
    "gh_get_pr_diff":         RiskTier.AUTO_ALLOW,
    "gh_list_issues":         RiskTier.AUTO_ALLOW,
}

# Arg-level escalation rules: if arg value matches pattern, bump tier by +1
_ARG_ESCALATION_RULES: list[tuple[re.Pattern[str], int]] = [
    (re.compile(r"\.\./"),          +1),   # path traversal attempt
    (re.compile(r"--force"),        +1),   # destructive flag
    (re.compile(r"prod|production", re.I), +1),  # prod target
    (re.compile(r"secret|token|key|password|cred", re.I), +2),  # secret-touching
    (re.compile(r"main|master",     re.I), +1),  # default branch target
]


@dataclass
class RiskScore:
    tool_name: str
    tier: RiskTier
    base_tier: RiskTier
    escalation_reasons: list[str] = field(default_factory=list)
    args_snapshot: dict[str, Any] = field(default_factory=dict)

    @property
    def requires_approval(self) -> bool:
        return self.tier >= RiskTier.LOW

    @property
    def is_blocked(self) -> bool:
        return self.tier >= RiskTier.BLOCK


def score_tool_call(tool_name: str, args: dict[str, Any]) -> RiskScore:
    """
    Score a pending tool call.

    Args:
        tool_name: Exact tool function name.
        args: Dict of argument name → value as the agent would pass them.

    Returns:
        RiskScore with tier, escalation reasons, and routing recommendation.
    """
    base = _TOOL_TIER_MAP.get(tool_name, RiskTier.MEDIUM)  # unknown tools default MEDIUM
    tier_int = int(base)
    reasons: list[str] = []

    # Flatten all arg values to strings for pattern matching
    flat_args = " ".join(str(v) for v in args.values())

    for pattern, delta in _ARG_ESCALATION_RULES:
        if pattern.search(flat_args):
            tier_int = min(tier_int + delta, int(RiskTier.BLOCK))
            reasons.append(f"arg matched /{pattern.pattern}/ (+{delta})")

    final_tier = RiskTier(tier_int)
    return RiskScore(
        tool_name=tool_name,
        tier=final_tier,
        base_tier=base,
        escalation_reasons=reasons,
        args_snapshot=dict(args),
    )

File: agentic/harness_optimizer/hitl/approval_queue.py

"""
Async approval queue for CyClaw HITL.

Design decisions grounded in CyClaw invariants:
- No external broker deps (Redis, Celery, etc.) — pure asyncio.Queue
- Timeout via asyncio.wait_for to prevent deadlocks in long-running agent threads
- Each pending item gets a UUID; the dashboard polls by UUID
- Approved/edited/rejected decisions land back via put() from the dashboard coroutine
- SHA-256 of tool args stored (not raw) in audit log per utils/logger.py contract
"""
from __future__ import annotations

import asyncio
import hashlib
import json
import time
import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import Any


class Decision(str, Enum):
    APPROVE = "approve"
    EDIT    = "edit"
    REJECT  = "reject"
    TIMEOUT = "timeout"


@dataclass
class ApprovalRequest:
    request_id: str
    tool_name: str
    args: dict[str, Any]
    risk_tier: int
    escalation_reasons: list[str]
    agent_context: str          # model-supplied rationale
    created_at: float = field(default_factory=time.time)
    args_hash: str = field(init=False)

    def __post_init__(self) -> None:
        raw = json.dumps(self.args, sort_keys=True).encode()
        self.args_hash = hashlib.sha256(raw).hexdigest()


@dataclass
class ApprovalDecision:
    request_id: str
    decision: Decision
    edited_args: dict[str, Any] | None   # only set when decision == EDIT
    reviewer_note: str
    decided_at: float = field(default_factory=time.time)


class ApprovalQueue:
    """
    Thread-safe async queue pairing pending requests with their decision futures.

    Usage pattern:
        queue = ApprovalQueue(timeout_seconds=120)
        decision = await queue.submit(request)   # blocks until resolved or timeout
        # In the dashboard coroutine:
        await queue.resolve(request_id, decision_obj)
    """

    def __init__(self, timeout_seconds: float = 120.0) -> None:
        self._timeout = timeout_seconds
        self._pending: asyncio.Queue[ApprovalRequest] = asyncio.Queue()
        self._futures: dict[str, asyncio.Future[ApprovalDecision]] = {}

    async def submit(self, request: ApprovalRequest) -> ApprovalDecision:
        """
        Enqueue a tool call for human review.
        Blocks until resolved or timeout_seconds elapses.
        On timeout returns a TIMEOUT decision — never raises, never deadlocks.
        """
        loop = asyncio.get_event_loop()
        fut: asyncio.Future[ApprovalDecision] = loop.create_future()
        self._futures[request.request_id] = fut
        await self._pending.put(request)

        try:
            return await asyncio.wait_for(
                asyncio.shield(fut),   # shield so cancellation doesn't corrupt state
                timeout=self._timeout,
            )
        except asyncio.TimeoutError:
            self._futures.pop(request.request_id, None)
            return ApprovalDecision(
                request_id=request.request_id,
                decision=Decision.TIMEOUT,
                edited_args=None,
                reviewer_note=f"Auto-rejected: no response within {self._timeout}s",
            )

    async def resolve(self, request_id: str, decision: ApprovalDecision) -> bool:
        """
        Called by the dashboard to deliver a human decision.
        Returns False if request_id is unknown (already timed out or duplicate).
        """
        fut = self._futures.pop(request_id, None)
        if fut is None or fut.done():
            return False
        fut.set_result(decision)
        return True

    async def next_pending(self) -> ApprovalRequest:
        """Dashboard calls this to dequeue the next item awaiting review."""
        return await self._pending.get()

    @property
    def pending_count(self) -> int:
        return self._pending.qsize()

File: agentic/harness_optimizer/hitl/decorator.py

"""
@require_approval decorator — the public API surface.

Wraps any async tool function. On call:
1. Scores the tool call via risk_scorer
2. If AUTO_ALLOW → execute immediately
3. If BLOCK → raise ToolBlockedError, never queue
4. Otherwise → submit to ApprovalQueue, await human decision,
   then approve (execute original), edit (execute with new args), or reject (raise)

The decorator is schema-aware: it reads the wrapped function's type annotations
to validate edited args before re-dispatch.

CyClaw-specific constraints obeyed:
- All decisions emitted to audit.jsonl via _emit_audit()
- No secrets logged (args_hash only, never raw sensitive values)
- Timeout always resolves — no deadlock path
"""
from __future__ import annotations

import asyncio
import functools
import inspect
import time
import uuid
from typing import Any, Callable, TypeVar

from .approval_queue import ApprovalDecision, ApprovalQueue, ApprovalRequest, Decision
from .risk_scorer import RiskTier, score_tool_call
from .audit import emit_hitl_event

F = TypeVar("F", bound=Callable[..., Any])

# Module-level shared queue — injected at startup by the harness
_GLOBAL_QUEUE: ApprovalQueue | None = None


def init_approval_queue(queue: ApprovalQueue) -> None:
    """Call once at harness startup to wire the shared queue."""
    global _GLOBAL_QUEUE
    _GLOBAL_QUEUE = queue


class ToolBlockedError(RuntimeError):
    """Raised when a tool call hits RiskTier.BLOCK — no approval path exists."""


class ToolRejectedError(RuntimeError):
    """Raised when a human reviewer rejects a tool call."""


class ToolTimeoutError(RuntimeError):
    """Raised when no reviewer responds within the timeout window."""


def require_approval(
    *,
    override_tier: RiskTier | None = None,
    context_fn: Callable[..., str] | None = None,
) -> Callable[[F], F]:
    """
    Decorator factory for human-in-the-loop tool approval.

    Args:
        override_tier: Force a specific RiskTier regardless of dynamic scoring.
            Useful for pinning high-risk tools in config without relying on the scorer.
        context_fn: Optional callable taking the same args as the tool,
            returning a human-readable rationale string for the dashboard.
            Defaults to a generic JSON dump of args.

    Example:
        @require_approval(override_tier=RiskTier.HIGH)
        async def delete_file(path: str) -> str:
            ...

        @require_approval(context_fn=lambda path: f"Deleting repo file: {path}")
        async def delete_file(path: str) -> str:
            ...
    """
    def decorator(fn: F) -> F:
        if not asyncio.iscoroutinefunction(fn):
            raise TypeError(
                f"@require_approval only works on async functions. "
                f"'{fn.__name__}' is synchronous — wrap it with asyncio.run() first."
            )

        sig = inspect.signature(fn)

        @functools.wraps(fn)
        async def wrapper(*args: Any, **kwargs: Any) -> Any:
            queue = _GLOBAL_QUEUE
            if queue is None:
                raise RuntimeError(
                    "ApprovalQueue not initialized. Call init_approval_queue() at harness startup."
                )

            # Bind positional + keyword args to the function signature
            bound = sig.bind(*args, **kwargs)
            bound.apply_defaults()
            call_args: dict[str, Any] = dict(bound.arguments)

            # ── Risk scoring ───────────────────────────────────────────────
            score = score_tool_call(fn.__name__, call_args)
            if override_tier is not None:
                score = score.__class__(
                    tool_name=score.tool_name,
                    tier=override_tier,
                    base_tier=score.base_tier,
                    escalation_reasons=score.escalation_reasons + [f"override_tier={override_tier.name}"],
                    args_snapshot=score.args_snapshot,
                )

            # ── BLOCK path ─────────────────────────────────────────────────
            if score.is_blocked:
                emit_hitl_event(
                    event="tool_blocked",
                    tool=fn.__name__,
                    args_hash=score.args_snapshot,   # will be hashed inside emit
                    tier=score.tier.name,
                )
                raise ToolBlockedError(
                    f"Tool '{fn.__name__}' is permanently blocked (RiskTier.BLOCK). "
                    f"Escalation reasons: {score.escalation_reasons}"
                )

            # ── AUTO_ALLOW path ─────────────────────────────────────────────
            if score.tier == RiskTier.AUTO_ALLOW:
                emit_hitl_event(
                    event="tool_auto_allowed",
                    tool=fn.__name__,
                    args_hash=call_args,
                    tier=score.tier.name,
                )
                return await fn(*args, **kwargs)

            # ── Approval queue path ────────────────────────────────────────
            context = (
                context_fn(**call_args)
                if context_fn is not None
                else f"Tool: {fn.__name__} | Args: {list(call_args.keys())}"
            )

            request = ApprovalRequest(
                request_id=str(uuid.uuid4()),
                tool_name=fn.__name__,
                args=call_args,
                risk_tier=int(score.tier),
                escalation_reasons=score.escalation_reasons,
                agent_context=context,
            )

            emit_hitl_event(
                event="tool_queued_for_approval",
                tool=fn.__name__,
                args_hash=call_args,
                tier=score.tier.name,
                request_id=request.request_id,
            )

            decision: ApprovalDecision = await queue.submit(request)

            # ── Decision routing ───────────────────────────────────────────
            emit_hitl_event(
                event=f"tool_decision_{decision.decision.value}",
                tool=fn.__name__,
                args_hash=call_args,
                tier=score.tier.name,
                request_id=request.request_id,
                reviewer_note=decision.reviewer_note,
            )

            if decision.decision == Decision.APPROVE:
                return await fn(*args, **kwargs)

            if decision.decision == Decision.EDIT:
                if decision.edited_args is None:
                    raise ToolRejectedError(
                        f"Decision was EDIT but no edited_args provided for '{fn.__name__}'"
                    )
                # Validate edited args against original signature before dispatch
                try:
                    sig.bind(**decision.edited_args)
                except TypeError as exc:
                    raise ToolRejectedError(
                        f"Edited args for '{fn.__name__}' don't match signature: {exc}"
                    ) from exc
                return await fn(**decision.edited_args)

            if decision.decision == Decision.TIMEOUT:
                raise ToolTimeoutError(
                    f"Tool '{fn.__name__}' (request {request.request_id}) "
                    f"timed out awaiting human approval."
                )

            # Decision.REJECT (or any unknown value)
            raise ToolRejectedError(
                f"Tool '{fn.__name__}' rejected by reviewer. "
                f"Note: {decision.reviewer_note!r}"
            )

        return wrapper  # type: ignore[return-value]

    return decorator

File: agentic/harness_optimizer/hitl/audit.py

"""
Thin audit wrapper matching CyClaw's append-only JSONL contract.
Never logs raw args — SHA-256 hashes only, per utils/logger.py pattern.
"""
from __future__ import annotations

import hashlib
import json
import os
import time
from pathlib import Path
from typing import Any

_AUDIT_PATH = Path(os.getenv("CYCLAW_AUDIT_LOG", "audit.jsonl"))


def _hash_args(args: dict[str, Any]) -> str:
    raw = json.dumps(args, sort_keys=True, default=str).encode()
    return hashlib.sha256(raw).hexdigest()


def emit_hitl_event(
    event: str,
    tool: str,
    args_hash: dict[str, Any],
    tier: str,
    request_id: str | None = None,
    reviewer_note: str | None = None,
) -> None:
    record: dict[str, Any] = {
        "event_type": f"hitl_{event}",
        "tool": tool,
        "args_hash": _hash_args(args_hash),
        "risk_tier": tier,
        "ts": time.time(),
    }
    if request_id:
        record["request_id"] = request_id
    if reviewer_note:
        record["reviewer_note_len"] = len(reviewer_note)  # length only, not content

    with _AUDIT_PATH.open("a") as f:
        f.write(json.dumps(record) + "\n")

File: agentic/harness_optimizer/hitl/dashboard.py

"""
Mock approval dashboard — terminal-based TUI for approving, editing, or
rejecting queued tool calls.

In production this would be replaced by:
  - A FastAPI endpoint polled by the CLI
  - A LangGraph Studio interrupt panel
  - An Agent Inbox-style webhook receiver

For CyClaw's current offline-first, loopback-only posture this terminal
dashboard is the correct default: no network, no external service, no auth dep.
"""
from __future__ import annotations

import asyncio
import json
import sys
from typing import Any

from .approval_queue import ApprovalDecision, ApprovalQueue, ApprovalRequest, Decision
from .risk_scorer import RiskTier

_TIER_LABELS = {
    RiskTier.AUTO_ALLOW: "✅ AUTO-ALLOW",
    RiskTier.LOW:        "🟡 LOW",
    RiskTier.MEDIUM:     "🟠 MEDIUM",
    RiskTier.HIGH:       "🔴 HIGH",
    RiskTier.BLOCK:      "⛔ BLOCK",
}

_RESET  = "\033[0m"
_BOLD   = "\033[1m"
_RED    = "\033[31m"
_YELLOW = "\033[33m"
_GREEN  = "\033[32m"
_CYAN   = "\033[36m"


def _fmt_request(req: ApprovalRequest) -> str:
    tier_label = _TIER_LABELS.get(RiskTier(req.risk_tier), str(req.risk_tier))
    lines = [
        f"\n{'─'*60}",
        f"{_BOLD}⏸  APPROVAL REQUIRED{_RESET}",
        f"  Request ID : {_CYAN}{req.request_id}{_RESET}",
        f"  Tool       : {_BOLD}{req.tool_name}{_RESET}",
        f"  Risk Tier  : {tier_label}",
        f"  Context    : {req.agent_context}",
        f"  Args       :",
    ]
    for k, v in req.args.items():
        lines.append(f"    {k} = {json.dumps(v, default=str)}")
    if req.escalation_reasons:
        lines.append(f"  {_YELLOW}Escalations:{_RESET}")
        for r in req.escalation_reasons:
            lines.append(f"    • {r}")
    lines.append("─"*60)
    return "\n".join(lines)


def _prompt_decision(req: ApprovalRequest) -> ApprovalDecision:
    """Synchronous terminal prompt. Called from a thread via run_in_executor."""
    print(_fmt_request(req))
    print(
        f"\n  {_GREEN}[a]{_RESET} Approve  "
        f"{_YELLOW}[e]{_RESET} Edit args  "
        f"{_RED}[r]{_RESET} Reject\n"
    )

    while True:
        try:
            choice = input("  Decision > ").strip().lower()
        except EOFError:
            # Non-interactive context (CI, pipe) — safe default: reject
            print("  [non-interactive] Auto-rejecting.")
            return ApprovalDecision(
                request_id=req.request_id,
                decision=Decision.REJECT,
                edited_args=None,
                reviewer_note="auto-rejected: non-interactive stdin",
            )

        if choice == "a":
            note = input("  Note (optional) > ").strip()
            return ApprovalDecision(
                request_id=req.request_id,
                decision=Decision.APPROVE,
                edited_args=None,
                reviewer_note=note or "approved",
            )

        elif choice == "e":
            print("  Enter edited args as JSON (e.g. {\"path\": \"/tmp/safe.txt\"}):")
            raw = input("  Edited args > ").strip()
            try:
                edited: dict[str, Any] = json.loads(raw)
            except json.JSONDecodeError as exc:
                print(f"  {_RED}Invalid JSON: {exc}{_RESET} — try again.")
                continue
            note = input("  Note (optional) > ").strip()
            return ApprovalDecision(
                request_id=req.request_id,
                decision=Decision.EDIT,
                edited_args=edited,
                reviewer_note=note or "edited by reviewer",
            )

        elif choice == "r":
            note = input("  Rejection reason > ").strip()
            return ApprovalDecision(
                request_id=req.request_id,
                decision=Decision.REJECT,
                edited_args=None,
                reviewer_note=note or "rejected",
            )

        else:
            print(f"  {_RED}Unknown choice '{choice}'. Enter a, e, or r.{_RESET}")


async def run_dashboard(queue: ApprovalQueue) -> None:
    """
    Async dashboard loop. Runs until the process exits.

    Dequeues approval requests and dispatches them to the terminal prompter
    via run_in_executor so the async event loop stays unblocked.

    In production swap _prompt_decision for a coroutine that POSTs to a
    FastAPI /decisions endpoint or polls Agent Inbox.
    """
    loop = asyncio.get_event_loop()
    print(f"{_BOLD}[CyClaw HITL Dashboard]{_RESET} Waiting for approval requests…")

    while True:
        req: ApprovalRequest = await queue.next_pending()
        # Run blocking terminal I/O in a thread — keeps the event loop live
        decision: ApprovalDecision = await loop.run_in_executor(
            None, _prompt_decision, req
        )
        resolved = await queue.resolve(req.request_id, decision)
        if not resolved:
            print(
                f"  {_YELLOW}Warning:{_RESET} request {req.request_id} already timed out "
                f"before review completed. Decision discarded."
            )

File: agentic/harness_optimizer/hitl/__init__.py

"""
CyClaw Human-in-the-Loop approval layer.

Public API:
    from agentic.harness_optimizer.hitl import require_approval, init_approval_queue
    from agentic.harness_optimizer.hitl.approval_queue import ApprovalQueue
    from agentic.harness_optimizer.hitl.dashboard import run_dashboard
    from agentic.harness_optimizer.hitl.risk_scorer import RiskTier
"""
from .approval_queue import ApprovalQueue, Decision, ApprovalDecision, ApprovalRequest
from .decorator import init_approval_queue, require_approval, ToolBlockedError, ToolRejectedError, ToolTimeoutError
from .risk_scorer import RiskTier, score_tool_call
from .dashboard import run_dashboard

__all__ = [
    "ApprovalQueue",
    "ApprovalDecision",
    "ApprovalRequest",
    "Decision",
    "RiskTier",
    "init_approval_queue",
    "require_approval",
    "run_dashboard",
    "score_tool_call",
    "ToolBlockedError",
    "ToolRejectedError",
    "ToolTimeoutError",
]

File: agentic/harness_optimizer/hitl/example_tools.py

"""
Example tool definitions showing how @require_approval integrates
with existing CyClaw tool schemas.

These mirror the real tool shapes from builder.py / subagents.py.
"""
from __future__ import annotations

import asyncio
from agentic.harness_optimizer.hitl import (
    ApprovalQueue,
    RiskTier,
    init_approval_queue,
    require_approval,
    run_dashboard,
)
from agentic.harness_optimizer.hitl.approval_queue import ApprovalDecision


# ── 1. Read-only tool — auto-allowed, no approval overhead ────────────────────
@require_approval()
async def read_file(path: str) -> str:
    """Read a file from the proposer workspace."""
    # RiskTier.AUTO_ALLOW → runs immediately, no queue
    with open(path) as f:
        return f.read()


# ── 2. Medium-risk: API POST — queued for approval ────────────────────────────
@require_approval(
    context_fn=lambda url, payload: f"POST to external API: {url} | payload keys: {list(payload)}"
)
async def api_post(url: str, payload: dict) -> str:
    """Send a POST request to an external API endpoint."""
    import httpx
    async with httpx.AsyncClient() as client:
        resp = await client.post(url, json=payload, timeout=30)
        return resp.text


# ── 3. High-risk: file deletion — forced HIGH tier, queued ───────────────────
@require_approval(
    override_tier=RiskTier.HIGH,
    context_fn=lambda path: f"DELETING file permanently: {path}",
)
async def delete_file(path: str) -> str:
    """Delete a file. Irreversible. Always requires approval."""
    import os
    os.remove(path)
    return f"Deleted: {path}"


# ── 4. Blocked: shell execution — never reaches queue ────────────────────────
@require_approval(override_tier=RiskTier.BLOCK)
async def run_shell(command: str) -> str:
    """Raw shell execution — blocked at the governance wall."""
    import subprocess
    return subprocess.check_output(command, shell=True, text=True)


# ── Harness entry point ───────────────────────────────────────────────────────
async def main() -> None:
    """
    Demo harness showing the full async approval loop.

    In production this is spawned as a subprocess by ops_runner.py,
    reads its task from stdin JSON, and writes results to stdout JSON.
    """
    queue = ApprovalQueue(timeout_seconds=120)
    init_approval_queue(queue)

    # Run dashboard concurrently in the same event loop
    dashboard_task = asyncio.create_task(run_dashboard(queue))

    # Simulate tool calls an agent might make
    try:
        print("\n[1] read_file — should auto-allow:")
        result = await read_file("/tmp/test.txt")
        print(f"    result: {result!r}")

        print("\n[2] delete_file — should queue for approval:")
        result = await delete_file("/tmp/deleteme.txt")
        print(f"    result: {result!r}")

    except Exception as exc:
        print(f"    caught: {type(exc).__name__}: {exc}")
    finally:
        dashboard_task.cancel()


if __name__ == "__main__":
    asyncio.run(main())

File: tests/test_hitl.py

"""
Unit tests for the HITL approval layer.
No live network, no GitHub, no LM Studio, no LangChain required.
Matches CyClaw's offline-first test philosophy from selftest.py.
"""
from __future__ import annotations

import asyncio
import pytest

from agentic.harness_optimizer.hitl import (
    ApprovalQueue,
    Decision,
    ApprovalDecision,
    RiskTier,
    init_approval_queue,
    require_approval,
    ToolBlockedError,
    ToolRejectedError,
    ToolTimeoutError,
    score_tool_call,
)


# ── Risk scorer unit tests ────────────────────────────────────────────────────

def test_read_file_scores_auto_allow():
    score = score_tool_call("read_file", {"path": "/tmp/safe.txt"})
    assert score.tier == RiskTier.AUTO_ALLOW
    assert not score.requires_approval

def test_delete_file_scores_high():
    score = score_tool_call("delete_file", {"path": "/tmp/file.txt"})
    assert score.tier == RiskTier.HIGH
    assert score.requires_approval

def test_run_shell_scores_block():
    score = score_tool_call("run_shell", {"command": "ls"})
    assert score.is_blocked

def test_path_traversal_escalates():
    # write_file starts at MEDIUM; traversal arg bumps it to HIGH
    score = score_tool_call("write_file", {"path": "../../../etc/passwd", "content": "x"})
    assert score.tier >= RiskTier.HIGH
    assert any("traversal" in r or "../" in r for r in score.escalation_reasons)

def test_secret_arg_double_escalates():
    score = score_tool_call("write_file", {"path": "/tmp/f", "content": "my_secret_token=abc"})
    assert score.tier >= RiskTier.HIGH

def test_unknown_tool_defaults_medium():
    score = score_tool_call("some_new_tool", {"x": 1})
    assert score.tier == RiskTier.MEDIUM


# ── Approval queue unit tests ─────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_approve_resolves_correctly():
    queue = ApprovalQueue(timeout_seconds=5)
    from agentic.harness_optimizer.hitl.approval_queue import ApprovalRequest
    import uuid, time

    req = ApprovalRequest(
        request_id=str(uuid.uuid4()),
        tool_name="delete_file",
        args={"path": "/tmp/x"},
        risk_tier=int(RiskTier.HIGH),
        escalation_reasons=[],
        agent_context="test context",
    )

    decision_obj = ApprovalDecision(
        request_id=req.request_id,
        decision=Decision.APPROVE,
        edited_args=None,
        reviewer_note="looks fine",
    )

    # Resolve concurrently
    async def resolver():
        await asyncio.sleep(0.05)
        await queue.resolve(req.request_id, decision_obj)

    asyncio.create_task(resolver())
    result = await queue.submit(req)
    assert result.decision == Decision.APPROVE

@pytest.mark.asyncio
async def test_timeout_returns_timeout_decision():
    queue = ApprovalQueue(timeout_seconds=0.1)  # 100ms timeout
    from agentic.harness_optimizer.hitl.approval_queue import ApprovalRequest
    import uuid

    req = ApprovalRequest(
        request_id=str(uuid.uuid4()),
        tool_name="api_post",
        args={"url": "https://example.com", "payload": {}},
        risk_tier=int(RiskTier.MEDIUM),
        escalation_reasons=[],
        agent_context="test",
    )

    result = await queue.submit(req)
    assert result.decision == Decision.TIMEOUT

@pytest.mark.asyncio
async def test_resolve_unknown_id_returns_false():
    queue = ApprovalQueue()
    result = await queue.resolve("nonexistent-id", ApprovalDecision(
        request_id="nonexistent-id",
        decision=Decision.APPROVE,
        edited_args=None,
        reviewer_note="",
    ))
    assert result is False


# ── Decorator integration tests ───────────────────────────────────────────────

@pytest.mark.asyncio
async def test_blocked_tool_raises_immediately():
    queue = ApprovalQueue()
    init_approval_queue(queue)

    @require_approval(override_tier=RiskTier.BLOCK)
    async def dangerous_tool(cmd: str) -> str:
        return cmd

    with pytest.raises(ToolBlockedError):
        await dangerous_tool("rm -rf /")

@pytest.mark.asyncio
async def test_auto_allow_executes_without_queue():
    queue = ApprovalQueue()
    init_approval_queue(queue)

    @require_approval()
    async def read_file(path: str) -> str:  # type: ignore
        return f"contents of {path}"

    result = await read_file("/tmp/safe.txt")
    assert "contents" in result
    assert queue.pending_count == 0  # never hit the queue

@pytest.mark.asyncio
async def test_reject_raises_tool_rejected():
    queue = ApprovalQueue(timeout_seconds=5)
    init_approval_queue(queue)
    from agentic.harness_optimizer.hitl.approval_queue import ApprovalRequest

    @require_approval()
    async def delete_file(path: str) -> str:  # type: ignore
        return f"deleted {path}"

    async def rejector():
        req: ApprovalRequest = await queue.next_pending()
        await queue.resolve(req.request_id, ApprovalDecision(
            request_id=req.request_id,
            decision=Decision.REJECT,
            edited_args=None,
            reviewer_note="not safe",
        ))

    asyncio.create_task(rejector())
    with pytest.raises(ToolRejectedError):
        await delete_file("/tmp/important.txt")

@pytest.mark.asyncio
async def test_edit_dispatches_with_new_args():
    queue = ApprovalQueue(timeout_seconds=5)
    init_approval_queue(queue)
    from agentic.harness_optimizer.hitl.approval_queue import ApprovalRequest

    received_path: list[str] = []

    @require_approval()
    async def write_file(path: str, content: str) -> str:  # type: ignore
        received_path.append(path)
        return f"wrote to {path}"

    async def editor():
        req: ApprovalRequest = await queue.next_pending()
        await queue.resolve(req.request_id, ApprovalDecision(
            request_id=req.request_id,
            decision=Decision.EDIT,
            edited_args={"path": "/tmp/safe_override.txt", "content": "sanitized"},
            reviewer_note="redirected to safe path",
        ))

    asyncio.create_task(editor())
    result = await write_file("/tmp/dangerous.txt", "original content")
    assert received_path[0] == "/tmp/safe_override.txt"
    assert "safe_override" in result

@pytest.mark.asyncio
async def test_timeout_raises_tool_timeout():
    queue = ApprovalQueue(timeout_seconds=0.05)
    init_approval_queue(queue)

    @require_approval()
    async def api_post(url: str, payload: dict) -> str:  # type: ignore
        return "ok"

    with pytest.raises(ToolTimeoutError):
        await api_post("https://example.com", {"key": "val"})

@pytest.mark.asyncio
async def test_edit_with_invalid_args_raises_rejected():
    queue = ApprovalQueue(timeout_seconds=5)
    init_approval_queue(queue)
    from agentic.harness_optimizer.hitl.approval_queue import ApprovalRequest

    @require_approval()
    async def write_file(path: str, content: str) -> str:  # type: ignore
        return "ok"

    async def bad_editor():
        req: ApprovalRequest = await queue.next_pending()
        await queue.resolve(req.request_id, ApprovalDecision(
            request_id=req.request_id,
            decision=Decision.EDIT,
            edited_args={"nonexistent_param": "value"},  # wrong signature
            reviewer_note="bad edit",
        ))

    asyncio.create_task(bad_editor())
    with pytest.raises(ToolRejectedError, match="don't match signature"):
        await write_file("/tmp/f.txt", "content")

Architecture Notes

Why this design fits CyClaw's invariants:

Concern How it's handled
Module isolation (Invariant 13) Entire package lives in agentic/; zero imports touch gate.py/graph.py [1]
Subprocess boundary example_tools.py's main() is the CLI entrypoint — spawned by ops_runner.py via subprocess.run [1]
No secrets in logs audit.py SHA-256 hashes all arg dicts, never logs raw values [2]
Deadlock prevention asyncio.wait_for + asyncio.shield — timeout always resolves, never hangs [3]
deepagents interrupt_config wiring interrupt_config={"delete_file": {"allow_edit": True, "allow_respond": True, "allow_accept": True}} on create_deep_agent calls the same HITL path [4]
LangGraph interrupt() compatibility The queue pattern is compatible with Command(resume={"type": "accept"}) — just swap _prompt_decision for a coroutine that calls graph.invoke(Command(resume=...)) [5][6]
Double-execution problem Approval node only calls interrupt() — no side effects before it, so re-execution on resume is a clean no-op [6]

Integration with deepagents.interrupt_config (when Phase 6 lands):

from deepagents import create_deep_agent
from langgraph.checkpoint.sqlite import SqliteSaver  # local, no Postgres needed

agent = create_deep_agent(
    tools=[delete_file, write_file, read_file, api_post],
    instructions="CyClaw GitHub harness...",
    interrupt_config={
        "delete_file": {"allow_ignore": False, "allow_respond": True,
                        "allow_edit": True, "allow_accept": True},
        "api_post":    True,  # shorthand: all options enabled
    },
    checkpointer=SqliteSaver.from_conn_string("data/agentic/hitl_checkpoints.db"),
)

The @require_approval decorator and interrupt_config are complementary, not redundant: the decorator enforces the risk gate at the Python call-site (works without deepagents), while interrupt_config wires the deepagents harness interrupt mechanism (requires checkpointer).[4][7]

Sources
[1] Analyze the import graph of the CyClaw repository to verify the architectural invariant where gate.py, graph.py, and mcp_hybrid_server.py do not import the agentic layer (GitHub/FS/SQL/Dropbox/NeMo). Generate a Mermaid dependency diagram, identify any circular or forbidden imports, and propose a refactoring plan to move agentic dependencies into a registry or dependency-injection pattern to ensure clean decoupling for subagent middleware management https://www.perplexity.ai/search/71b682d2-ebeb-4d98-a11b-0069c55a3e26
[2] Random infographic daily

load research and @html skills and output reply as both text reply and downloadable robust html mobile web app

Generate an interesting infographic based on or inspired by memory: my following interests:
, history, ...

...e topics) - also include a detailed summary to begin with that gives a holistic overview of the topic and includes a brief table of contents if applicable - (verify 100% accuracy/ precision and spelling/grammer)

Topic: CyClaw via GitHub connector https://www.perplexity.ai/search/ac497761-b7a5-4e69-a843-3a513ca3554f
[3] [Python] Setting Timeouts for asyncio.Queue Operations | 森の株 https://morinokabu.com/2026/01/14/python-asyncio-queue-timeout-management/
[4] Deep Agents - deepagents · PyPI https://pypi.org/project/deepagents/0.0.5/
[5] LangGraph State Management: Checkpoints, Thread State, and ... https://eastondev.com/blog/en/posts/ai/20260424-langgraph-agent-architecture/
[6] LangGraph Human in the Loop: A Complete Tutorial (With ... https://thehandover.xyz/blog/langgraph-human-in-the-loop-tutorial
[7] deep-agents-orchestration — AI agent skill | explainx.ai | explainx.ai https://explainx.ai/skills/langchain-ai/langchain-skills/deep-agents-orchestration
[8] LangchainIntegrationPlan.md https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/2912707/a4610b72-ab51-465c-a6f3-e3eca1b2b215/LangchainIntegrationPlan.md?AWSAccessKeyId=ASIA2F3EMEYEVDDB4RPE&Signature=FKm%2BXOILqZF5beqv27rTtD2CC4c%3D&x-amz-security-token=IQoJb3JpZ2luX2VjEOr%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMSJHMEUCIQCr1kXGyv0o5%2BUEJJsj17%2FxCgPAbiX%2BchAM0U8U8YOPfQIgAUCOvcn0qwfspo5BCee3tKwMfsUuOupdqwWjeY1fZ6Aq%2FAQIs%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARABGgw2OTk3NTMzMDk3MDUiDJAO1wpoxqhikdhe3yrQBOemryh5vVIlx0Ky79tStk5JVcm79%2F%2BfWUiJUNTa%2B%2BNiYxZcZzYU7TcvaebFxqNXSsh9l5UtN4u0dDXrw7%2BgJecQovBh%2FjfGkM2fsOK48u3kbOUN0sPhlkvhttu%2FrjlE3VXAmJIqVRZ2mI0kLNBibm8AT1jHIrf%2BE%2FVBTmGZBLzdVwX4stNw4XdTtMh4dhGcsGBSUaICMfgtCakydEhRr5JPzxPzRSQ30Iv6Ay1RRNV1t9YMkyVG13lwQYUTbTEQLs9%2FbqA9RK9cNLPerBbbfOeiU1SEKtgFGdo%2F1175Za6QA2dpsoKcrziuZXQo%2Bk9JkvFZl%2FKZ3x2RWMbJ3iU3Abb2uZogYqf%2BI7EQFlYPfmjKRlkjTqLXTiU48AA7WdXpmcq%2B454zED7lZYt9zmi0ujVJE%2BQA5KbQg946AvejNu3gsxCqoD8wfE1sO51iHk%2FXL0oNRb8zG%2BlAYsfyKxVFxesOQh1XImsska5fxfML6c0ixvZ4N25EEfdhMqXDYJmBqAPH46LffUwJRPjMvoo%2FoA3O%2Fg9YiJ8aIER0meWkG2M1StHC6mxiSDbcCQ6HIQwrZXmXz02Dahlyr%2FX0ZjwrWJULriTEGSxWgYGFfqGfVvs4mFtXH79Zq2eCRs4B0cbLBnL0sFeunFhzEbGXgljEQHc0rdi0KgzGBOQEx9IF9bxXtI1sbUAe1kCJ2TftP54N2P1OqySTV6G%2BcYDKBwLCQy%2FiVqy%2FSZw8gx0iM%2F4UkU%2F7XB7VYMUYuEnc81EsVGcmFprpG5%2F%2FtnY9jqdKzQYTrKIwg%2FjC0gY6mAF2lPu9852Df2vQmIFFn5V6SD9KxIGfiSyX3lPuy%2BI4cigF3ujWBNl0Ti4bkJj1UWD13n%2BoV%2Fbmx5ibwTivn3PBFQaQmz7wE2YqtbeB1Gc23PgcVxEzpn4OrkoC3n%2BIPlnqgs2xbDKaWMr9KD5yEogqKm6XHfpHPuzW%2Ft7EherCDsKTJfvccmbYbIVNyZmj6QfLZ4AWUDrlCg%3D%3D&Expires=1783679446
[9] CyClaw_Swarm_Verification_Report_2026-07-09.pdf https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/collection_bb753fae-2826-4344-be5a-7a4bfcfb2760/e0dcc5c1-b3f5-4171-8f15-6df2022f7e25/CyClaw_Swarm_Verification_Report_2026-07-09.pdf?AWSAccessKeyId=ASIA2F3EMEYEVDDB4RPE&Signature=IRLAKDw4N21%2Bql5JJ1sHhHGf%2B5Y%3D&x-amz-security-token=IQoJb3JpZ2luX2VjEOr%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMSJHMEUCIQCr1kXGyv0o5%2BUEJJsj17%2FxCgPAbiX%2BchAM0U8U8YOPfQIgAUCOvcn0qwfspo5BCee3tKwMfsUuOupdqwWjeY1fZ6Aq%2FAQIs%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARABGgw2OTk3NTMzMDk3MDUiDJAO1wpoxqhikdhe3yrQBOemryh5vVIlx0Ky79tStk5JVcm79%2F%2BfWUiJUNTa%2B%2BNiYxZcZzYU7TcvaebFxqNXSsh9l5UtN4u0dDXrw7%2BgJecQovBh%2FjfGkM2fsOK48u3kbOUN0sPhlkvhttu%2FrjlE3VXAmJIqVRZ2mI0kLNBibm8AT1jHIrf%2BE%2FVBTmGZBLzdVwX4stNw4XdTtMh4dhGcsGBSUaICMfgtCakydEhRr5JPzxPzRSQ30Iv6Ay1RRNV1t9YMkyVG13lwQYUTbTEQLs9%2FbqA9RK9cNLPerBbbfOeiU1SEKtgFGdo%2F1175Za6QA2dpsoKcrziuZXQo%2Bk9JkvFZl%2FKZ3x2RWMbJ3iU3Abb2uZogYqf%2BI7EQFlYPfmjKRlkjTqLXTiU48AA7WdXpmcq%2B454zED7lZYt9zmi0ujVJE%2BQA5KbQg946AvejNu3gsxCqoD8wfE1sO51iHk%2FXL0oNRb8zG%2BlAYsfyKxVFxesOQh1XImsska5fxfML6c0ixvZ4N25EEfdhMqXDYJmBqAPH46LffUwJRPjMvoo%2FoA3O%2Fg9YiJ8aIER0meWkG2M1StHC6mxiSDbcCQ6HIQwrZXmXz02Dahlyr%2FX0ZjwrWJULriTEGSxWgYGFfqGfVvs4mFtXH79Zq2eCRs4B0cbLBnL0sFeunFhzEbGXgljEQHc0rdi0KgzGBOQEx9IF9bxXtI1sbUAe1kCJ2TftP54N2P1OqySTV6G%2BcYDKBwLCQy%2FiVqy%2FSZw8gx0iM%2F4UkU%2F7XB7VYMUYuEnc81EsVGcmFprpG5%2F%2FtnY9jqdKzQYTrKIwg%2FjC0gY6mAF2lPu9852Df2vQmIFFn5V6SD9KxIGfiSyX3lPuy%2BI4cigF3ujWBNl0Ti4bkJj1UWD13n%2BoV%2Fbmx5ibwTivn3PBFQaQmz7wE2YqtbeB1Gc23PgcVxEzpn4OrkoC3n%2BIPlnqgs2xbDKaWMr9KD5yEogqKm6XHfpHPuzW%2Ft7EherCDsKTJfvccmbYbIVNyZmj6QfLZ4AWUDrlCg%3D%3D&Expires=1783679446
[10] CyClaw_Architecture_Guide_v1.9.0_crisp.pdf https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/collection_bb753fae-2826-4344-be5a-7a4bfcfb2760/4e2a11ff-ab32-4e8f-b3dc-1efbd4149ef1/CyClaw_Architecture_Guide_v1.9.0_crisp.pdf?AWSAccessKeyId=ASIA2F3EMEYEVDDB4RPE&Signature=YQvZAGALqRH3nY6SaXDOajMtvh4%3D&x-amz-security-token=IQoJb3JpZ2luX2VjEOr%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMSJHMEUCIQCr1kXGyv0o5%2BUEJJsj17%2FxCgPAbiX%2BchAM0U8U8YOPfQIgAUCOvcn0qwfspo5BCee3tKwMfsUuOupdqwWjeY1fZ6Aq%2FAQIs%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARABGgw2OTk3NTMzMDk3MDUiDJAO1wpoxqhikdhe3yrQBOemryh5vVIlx0Ky79tStk5JVcm79%2F%2BfWUiJUNTa%2B%2BNiYxZcZzYU7TcvaebFxqNXSsh9l5UtN4u0dDXrw7%2BgJecQovBh%2FjfGkM2fsOK48u3kbOUN0sPhlkvhttu%2FrjlE3VXAmJIqVRZ2mI0kLNBibm8AT1jHIrf%2BE%2FVBTmGZBLzdVwX4stNw4XdTtMh4dhGcsGBSUaICMfgtCakydEhRr5JPzxPzRSQ30Iv6Ay1RRNV1t9YMkyVG13lwQYUTbTEQLs9%2FbqA9RK9cNLPerBbbfOeiU1SEKtgFGdo%2F1175Za6QA2dpsoKcrziuZXQo%2Bk9JkvFZl%2FKZ3x2RWMbJ3iU3Abb2uZogYqf%2BI7EQFlYPfmjKRlkjTqLXTiU48AA7WdXpmcq%2B454zED7lZYt9zmi0ujVJE%2BQA5KbQg946AvejNu3gsxCqoD8wfE1sO51iHk%2FXL0oNRb8zG%2BlAYsfyKxVFxesOQh1XImsska5fxfML6c0ixvZ4N25EEfdhMqXDYJmBqAPH46LffUwJRPjMvoo%2FoA3O%2Fg9YiJ8aIER0meWkG2M1StHC6mxiSDbcCQ6HIQwrZXmXz02Dahlyr%2FX0ZjwrWJULriTEGSxWgYGFfqGfVvs4mFtXH79Zq2eCRs4B0cbLBnL0sFeunFhzEbGXgljEQHc0rdi0KgzGBOQEx9IF9bxXtI1sbUAe1kCJ2TftP54N2P1OqySTV6G%2BcYDKBwLCQy%2FiVqy%2FSZw8gx0iM%2F4UkU%2F7XB7VYMUYuEnc81EsVGcmFprpG5%2F%2FtnY9jqdKzQYTrKIwg%2FjC0gY6mAF2lPu9852Df2vQmIFFn5V6SD9KxIGfiSyX3lPuy%2BI4cigF3ujWBNl0Ti4bkJj1UWD13n%2BoV%2Fbmx5ibwTivn3PBFQaQmz7wE2YqtbeB1Gc23PgcVxEzpn4OrkoC3n%2BIPlnqgs2xbDKaWMr9KD5yEogqKm6XHfpHPuzW%2Ft7EherCDsKTJfvccmbYbIVNyZmj6QfLZ4AWUDrlCg%3D%3D&Expires=1783679446
[11] Human-in-the-loop - Docs by LangChain中文 https://www.langchain-cn.com/oss/python/deepagents/human-in-the-loop
[12] Human-in-the-loop - Docs by LangChain https://docs.langchain.com/oss/python/deepagents/human-in-the-loop
[13] Interrupts - Docs by LangChain https://docs.langchain.com/oss/python/langgraph/interrupts
[14] create_deep_agent | deepagents https://reference.langchain.com/python/deepagents/graph/create_deep_agent
[15] Responding to interrupts https://docs.langchain.com/oss/python/langchain/human-in-the-loop
[16] deep-agents-orchestration - Remote OpenClaw https://www.remoteopenclaw.com/skills/langchain-ai/langchain-skills/deep-agents-orchestration
[17] Human-in-the-Loop Workflows with LangGraph - Abstract Algorithms https://www.abstractalgorithms.dev/langgraph-human-in-the-loop
[18] How Agent Frameworks Solve Human-in-the-Loop https://dzone.com/articles/agent-frameworks-human-loop
[19] Customize Deep Agents - Docs by LangChain https://docs.langchain.com/oss/python/deepagents/customization
[20] HumanInTheLoopMiddleware - LangChain Reference https://reference.langchain.com/python/langchain/agents/middleware/human_in_the_loop/HumanInTheLoopMiddleware
[21] interrupt | langgraph - LangChain Reference https://reference.langchain.com/python/langgraph/types/interrupt
[22] Interrupts and Commands in LangGraph: Building Human-in-the ... https://dev.to/jamesbmour/interrupts-and-commands-in-langgraph-building-human-in-the-loop-workflows-4ngl
[23] langchain-ai/langgraph-human-in-the-loop | Decision Hub https://hub.decision.ai/skills/langchain-ai/langgraph-human-in-the-loop
[24] LangGraph Interrupt and Resume Workflow - Part 4/22 https://www.youtube.com/watch?v=PYH7Mn4pXUc
[25] Human-in-the-Loop Authorization in Python with LangGraph ... - Auth0 https://auth0.com/blog/async-ciba-python-langgraph-auth0/
[26] Making it easier to build human-in-the-loop agents with interrupt https://www.langchain.com/blog/making-it-easier-to-build-human-in-the-loop-agents-with-interrupt
[27] Permissions - Docs by LangChain中文 https://www.langchain-cn.com/oss/python/deepagents/permissions
[28] Context management https://docs.langchain.com/oss/javascript/deepagents/overview
[29] HITL Patterns in LangGraph: Approve, Reject, and Edit ... - Medium https://medium.com/the-advanced-school-of-ai/human-in-the-loop-in-langgraph-approve-or-reject-pattern-fcf6ba0c5990
[30] How to Build Human-in-the-Loop Plan-and-Execute AI Agents with ... https://www.marktechpost.com/2026/02/16/how-to-build-human-in-the-loop-plan-and-execute-ai-agents-with-explicit-user-approval-using-langgraph-and-streamlit/
[31] Harness capabilities - Docs by LangChain中文 https://www.langchain-cn.com/oss/python/deepagents/harness
[32] LangGraph (Part 4): Human-in-the-Loop for Reliable AI Workflows https://medium.com/@sitabjapal03/langgraph-part-4-human-in-the-loop-for-reliable-ai-workflows-aa4cc175bce4
[33] Interrupt on a specific tool when using Langgraph ReAct ... https://github.com/langchain-ai/langgraph/discussions/929
[34] Coroutines and tasks — Python 3.14.6 documentation https://docs.python.org/3/library/asyncio-task.html
[35] cpython/Lib/asyncio/timeouts.py at main · python/cpython https://github.com/python/cpython/blob/main/Lib/asyncio/timeouts.py
[36] LangGraph Advanced – Directly Integrate Human-in-the- ... https://lilys.ai/en/notes/ai-agent-20251017/langgraph-advanced-human-in-loop-tools
[37] GitHub - MatteoH2O1999/timeout_decorator: Timeout decorator for Python synchronous and asynchronous functions https://github.com/MatteoH2O1999/timeout_decorator
[38] Human-in-the-Loop Systems | esurovtsev/langgraph-advanced ... https://deepwiki.com/esurovtsev/langgraph-advanced/4-human-in-the-loop-systems
[39] Coroutine Timeout Injection in Python: A Decorator ... https://medium.com/@RampantLions/coroutine-timeout-injection-in-python-a-decorator-approach-with-asyncio-wait-for-171dc0a3f5be
[40] Decorator to facilitate sync and async calls to one function https://discuss.python.org/t/decorator-to-facilitate-sync-and-async-calls-to-one-function/78986
[41] Can we make asyncio event.wait with timeout atomic? - Ideas https://discuss.python.org/t/can-we-make-asyncio-event-wait-with-timeout-atomic/106049
[42] tessl/pypi-langgraph-prebuilt@0.6.x - Registry - Tessl https://tessl.io/registry/tessl/pypi-langgraph-prebuilt/0.6.0/files/docs/agent-creation.md
[43] aiodecorator - PyPI https://pypi.org/project/aiodecorator/
[44] Queues — Python 3.14.6 documentation https://docs.python.org/3/library/asyncio-queue.html
[45] йҳҹеҲ— https://docs.python.org/zh-cn/dev/library/asyncio-queue.html
[46] async-timeout https://pypi.org/project/async-timeout/
[47] LangGraph's interrupt() Function: The Simpler Way to Build ... https://medium.com/@areebahmed575/langgraphs-interrupt-function-the-simpler-way-to-build-human-in-the-loop-agents-faef98891a92
[48] Add human intervention - GitHub Pages https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/add-human-in-the-loop/
[49] Human-in-the-Loop with LangGraph: Approval Gates and ... https://callsphere.ai/blog/langgraph-human-in-the-loop-approval-gates-manual-intervention
[50] human in the loop · Issue #2732 · langchain-ai/langgraph langchain-ai/langgraph#2732
[51] Human-in-the-Loop - Docs by LangChain https://docs.langchain.com/oss/python/langchain/frontend/human-in-the-loop
[52] How to Build Autonomous Multi-Step AI Systems(Part-2) https://medium.com/@dharamai2024/mastering-langchain-deep-agent-how-to-build-autonomous-multi-step-ai-systems-part-2-b223f0a30d96
[53] deepagents · PyPI https://pypi.org/project/deepagents/0.3.4/
[54] Crash course in asyncio - Ben Dixon https://www.bendixon.net/blog/2025/crash-course-in-asyncio/
[55] 自定义 Deep Agents - Docs by LangChain中文 https://www.langchain-cn.com/oss/python/deepagents/customization
[56] Releases · langchain-ai/deepagents - GitHub https://github.com/langchain-ai/deepagents/releases
[57] Prompt Engineering for AI Agents: 2026 Guide https://www.inflectra.com/Ideas/Topic/AI-Agent-Prompt-Engineering.aspx
[58] Tool calling - Docs by LangChain https://docs.langchain.com/oss/javascript/langchain/frontend/tool-calling
[59] How to Secure LangChain Agents in Production https://forum.langchain.com/t/how-we-add-runtime-security-to-langchain-agents-in-production/3461
[60] How to Build Production-Ready AI Agents with Deep Agents - Milvus Blog https://milvus.io/blog/how-to-build-productionready-ai-agents-with-deep-agents-and-milvus.md
[61] How to Build Asyncio Queues in Python - OneUptime https://oneuptime.com/blog/post/2026-01-30-python-asyncio-queues/view
[62] asyncio.Queue - any benefit in async put with maxsize? https://stackoverflow.com/questions/79892703/asyncio-queue-any-benefit-in-async-put-with-maxsize
[63] Asyncio Queue in Python - SuperFastPython https://superfastpython.com/asyncio-queue/
[64] Waiting in asyncio - Hynek Schlawack https://hynek.me/articles/waiting-in-asyncio/
[65] Document whether asyncio.wait_for(q.get(), timeout) is safe ... python/cpython#92824
[66] 【Python】asyncio.Queueの待機処理にタイムアウトを設定する https://note.com/morinokabu/n/n88e3eb51a8dc
[67] When using 'interrupt' followed by 'new Command({ resume: ...})', get an undefined message error from LangChain + LangGraph https://stackoverflow.com/questions/79471648/when-using-interrupt-followed-by-new-command-resume-get-an-undefin
[68] wait_for timeout to over come asyncio.queue not having a timeout not working https://stackoverflow.com/questions/60695753/wait-for-timeout-to-over-come-asyncio-queue-not-having-a-timeout-not-working
[69] langgraph-agent-interrupt-1-reject-node.js https://gist.github.com/bguiz/d0d5d3b732f9395c1a6817700fd5a311
[70] Using asyncio.Queue for producer-consumer flow https://stackoverflow.com/questions/52582685/using-asyncio-queue-for-producer-consumer-flow
[71] How to review tool calls¶ https://langchain-ai.github.io/langgraphjs/cloud/how-tos/human_in_the_loop_review_tool_calls/
[72] Python asyncio.wait_for(): Cancel a Task with a Timeout https://www.pythontutorial.net/python-concurrency/python-asyncio-wait_for/
[73] Human-in-the-Loop Tool Calling with LangGraph https://sangeethasaravanan.medium.com/human-in-the-loop-tool-calling-with-langgraph-building-interruptible-ai-agents-fd0275ce4523
[74] How to Build Human-in-the-Loop Agentic Workflows with LangGraph https://subagentic.ai/howtos/langgraph-human-in-the-loop-agentic-workflows/
[75] deepagents/libs/code/THREAT_MODEL.md at main · langchain-ai ... https://github.com/langchain-ai/deepagents/blob/main/libs/code/THREAT_MODEL.md
[76] Implementing LangGraph with Human-In-The-Loop https://medium.com/@sarthakpattanaik_4094/implementing-langgraph-with-human-in-the-loop-0f02bff6c453
[77] LangGraph in Production: Building Stateful AI Agents - Kalvium Labs https://www.kalviumlabs.ai/blog/langgraph-in-production-stateful-multi-step-agents/
[78] langchain-langgraph-human-in-loop - Agent Skill https://tonsofskills.com/skills/langchain-langgraph-human-in-loop/
[79] LangGraph's interrupt/resume primitives are genuinely ... - GitHub https://github.com/raed667/langgraph-hitl
[80] LangGraph HITL(Human In The Loop) | AI Korner - A Quick AI Skim https://www.linkedin.com/posts/ai-korner-a-quick-ai-skim_langgraph-hitlhuman-in-the-loop-activity-7442397353371873280-7OsD
[81] How I implemented human-in-the-loop with LangGraph's interrupt ... https://www.reddit.com/r/LangChain/comments/1s6qidj/how_i_implemented_humanintheloop_with_langgraphs/
[82] Human-in-the-Loop Workflows with LangGraph - Abstract Algorithms https://abstractalgorithms.dev/langgraph-human-in-the-loop
[83] asyncio : How to handle empty queue when you use non-blocking get()? https://stackoverflow.com/questions/66033949/asyncio-how-to-handle-empty-queue-when-you-use-non-blocking-get
[84] langgraph/docs/docs/cloud/how-tos/human_in_the_loop_breakpoint.md at main · langchain-ai/langgraph https://github.com/langchain-ai/langgraph/blob/main/docs/docs/cloud/how-tos/human_in_the_loop_breakpoint.md
[85] 同步原语— Python 3.12.13 文档 https://docs.python.org/zh-cn/3.12/library/asyncio-sync.html
[86] Synchronization Primitives https://docs.python.org/3.12/library/asyncio-sync.html
[87] Функция wait_for() модуля asyncio в Python https://docs-python.ru/standart-library/modul-asyncio-python/funktsija-wait-for-modulja-asyncio/
[88] GH-96764: rewrite asyncio.wait_for to use asyncio.timeout by kumaraditya303 · Pull Request #98518 · python/cpython python/cpython#98518
[89] Event Loop https://docs.python.org/3/library/asyncio-eventloop.html?highlight=asyncio+timeout
[90] asyncio.timeout() To Wait and Cancel Tasks - SuperFastPython https://superfastpython.com/asyncio-timeout/
[91] deepagents · PyPI https://pypi.org/project/deepagents/0.0.6/
[92] Deep Agents overview - Docs by LangChain https://docs.langchain.com/oss/python/deepagents/overview
[93] Agents - GitHub Pages https://langchain-ai.github.io/langgraph/reference/agents/
[94] Human-in-the-loop - Docs by LangChain中文 http://www.langchain-cn.com/oss/javascript/deepagents/human-in-the-loop
[95] Human-in-the-Loop with LangGraph Interrupts: Approve, Edit, Resume https://callsphere.ai/blog/td30-fw-langgraph-human-in-the-loop-interrupt-resume
[96] Human-in-the-loop Interrupt Flows Not Working with ... assistant-ui/assistant-ui#1899
[97] Human-in-the-loop approval dashboard for LangGraph agents https://forum.langchain.com/t/human-in-the-loop-approval-dashboard-for-langgraph-agents-open-source-free-to-deploy/3616
[98] LangGraph Durable Execution Patterns: Resilient AI Agents ... - 芝士貓 https://cheesecat.net/blog/langgraph-durable-execution-resilient-agents-implementation-guide-2026-zh-tw/
[99] EgoAlpha https://egoalpha.com/zh/DeepAgents/deepagents-7-10.html
[100] langgraph-human-in-the-loop - Remote OpenClaw https://www.remoteopenclaw.com/skills/langchain-ai/langchain-skills/langgraph-human-in-the-loop
[101] Add human intervention https://langchain-ai.lang.chat/langgraph/how-tos/human_in_the_loop/add-human-in-the-loop/
[102] HumanInterruptConfig | @langchain/langgraph https://reference.langchain.com/javascript/langchain-langgraph/prebuilt/HumanInterruptConfig
[103] Python asyncio: async/await Explained https://www.w3docs.com/learn-python/python-asyncio
[104] agent-inbox-langgraph-example/src/agent/graph.py at main · langchain-ai/agent-inbox-langgraph-example https://github.com/langchain-ai/agent-inbox-langgraph-example/blob/main/src/agent/graph.py
[105] langchain/langgraph https://app.unpkg.com/@langchain/langgraph@1.0.0/files/dist/prebuilt/interrupt.d.ts.map

--

@claude review this comment thoroughly and reply in Claude code session - I'm tempted to finish the initial plans phases through 7 or 8 then verifying if it works outside of testing sandbox - if it's clearly wrong lemme know

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@cgfixit

cgfixit commented Jul 10, 2026

Copy link
Copy Markdown
Owner Author

@claude review

@cgfixit

cgfixit commented Jul 10, 2026

Copy link
Copy Markdown
Owner Author

@codex

@github-actions

Copy link
Copy Markdown

Claude Code is working…

I'll analyze this and get back to you.

View job run

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e89a2e9501

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +6 to +7
The canonical, current version of this plan lives at
`docs/agentic/GITHUB_DEEP_AGENT_HARNESS_OPTIMIZER_PLAN.md`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Point only to a current canonical plan

This replacement makes the root note depend entirely on the target as the “canonical, current” plan, but that target still says at docs/agentic/GITHUB_DEEP_AGENT_HARNESS_OPTIMIZER_PLAN.md:88-91 that no agentic/deepagent_github/ package exists, while the package and files such as agentic/deepagent_github/config.py are present. Since the deleted copy contained the accurate phase 0-5 scaffold inventory, readers following this pointer can now plan from contradictory/stale guidance and risk recreating or overwriting shipped scaffold; update the canonical doc in the same change or keep the current-state inventory here.

Useful? React with 👍 / 👎.

Comment thread LangChainFix.md Outdated
Comment on lines +11 to +13
contradicts the repo rule that `config.yaml` is the single source of truth
for every tunable; env vars in this repo are reserved for secrets and
database URLs. The shipped `agentic/deepagent_github/config.py` (merged

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Qualify the env-var guidance

This is broader than repo truth: retrieval/embeddings.py defines CYCLAW_EMBED_CACHE_SIZE as an operator tunable because the LRU cache size is fixed at decoration time, and gate.py also manages telemetry-kill environment controls. As written, future maintainers following this reconciliation could reject or remove valid non-secret env controls rather than just avoiding the proposed GitHub-agent env flags; please narrow the claim to agentic feature configuration staying in config.yaml.

Useful? React with 👍 / 👎.

…ording

Codex review on PR #501 flagged two real issues, both verified before
fixing:

- docs/agentic/GITHUB_DEEP_AGENT_HARNESS_OPTIMIZER_PLAN.md section 3 still
  said "No agentic/deepagent_github/ package exists yet" (pre-phase-0-5
  wording), contradicting the package's actual presence and the doc's own
  later "Unwired scaffold inventory" section — pointing readers at this file
  as "the canonical, current plan" (LangChainFix.md's new preface) risked
  sending them to stale guidance. Struck through with a pointer to the
  accurate section.
- LangChainFix.md's preface claimed "env vars in this repo are reserved for
  secrets and database URLs," which is wrong: CYCLAW_EMBED_CACHE_SIZE,
  gate.py's telemetry-kill vars, and CYCLAW_RATELIMIT_DB_URL are all
  legitimate non-secret env-var tunables. Narrowed to the actual rule this
  document violates: agentic feature flags belong in config.yaml, not env
  vars — not a blanket statement about all env var usage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3WhoiUiUak84jUmiBhhvA
Repository owner locked and limited conversation to collaborators Jul 10, 2026
@cgfixit cgfixit changed the title docs: reconcile LangChain root notes with shipped code and canonical plan [docs: LangChain root notes with shipped code and canonical plan] Jul 10, 2026

cgfixit commented Jul 11, 2026

Copy link
Copy Markdown
Owner Author

Closing as superseded: all three of this PR's file changes landed on main verbatim in commit d073b28 ("docs: consolidate root LangChain planning docs into canonical plan", 2026-07-11), per the owner's direct-to-main consolidation decision — the LangchainIntegrationPlan.md pointer replacement, the LangChainFix.md reconciliation header (extended with two additional corrections), and the canonical plan doc's section-3 strikethrough fix. Nothing from this branch was dropped. Follow-on status/roadmap: docs/LG_Deep_Agentic_Harness_status_n_roadmap.md.


Generated by Claude Code

@cgfixit cgfixit closed this Jul 11, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants