diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b4b1cb08..c1ab1394 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -68,7 +68,7 @@ { "name": "darrow-adaptive-delivery", "source": "./plugins/orchestration/darrow-adaptive-delivery", - "description": "Compile bounded engineering work into task-informed host-native goals" + "description": "Diagnose host capacity or compile bounded engineering work into one host-native owner" }, { "name": "darrow-ticket-pipeline", diff --git a/docs/choosing-plugins.md b/docs/choosing-plugins.md index a0e42251..cb56bb1f 100644 --- a/docs/choosing-plugins.md +++ b/docs/choosing-plugins.md @@ -7,23 +7,24 @@ the other layers. Its README owns its prerequisites, behavior, and safety rules. ## Choose by intent -| Your next task | Plugin and local instructions | -| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -| Assess whether a request is ready to implement | [Readiness gate](../plugins/capability/darrow-readiness-gate/README.md) | -| Clarify behavior or make an implementation plan | [Discovery](../plugins/capability/darrow-discovery/README.md) | -| Implement an observable behavior test-first | [TDD](../plugins/capability/darrow-tdd/README.md) | -| Review a change independently | [Review](../plugins/capability/darrow-review/README.md) | -| Verify implementation acceptance through independent assessment | [Verification](../plugins/capability/darrow-verification/README.md) | -| Create a branch, commit, or pull request | [Git](../plugins/capability/darrow-git/README.md) | -| Read, list, create, or update GitHub issues | [GitHub tickets](../plugins/capability/darrow-tickets-github/README.md) | -| Understand technical structure in a compact visual | [Explanation](../plugins/capability/darrow-explanation/README.md) | -| Export Codex turn telemetry to Langfuse | [Langfuse observability](../plugins/capability/darrow-observability-langfuse/README.md) | -| Organize repository agent guidance | [Information architecture](../plugins/foundation/darrow-information-architecture/README.md) | -| Capture or find accepted decisions | [Decisions](../plugins/foundation/darrow-decisions/README.md) | -| Create or validate a reusable skill | [Skill authoring](../plugins/foundation/darrow-skill-authoring/README.md) | -| Explicitly hand a bounded engineering task to one execution owner | [Adaptive Delivery](../plugins/orchestration/darrow-adaptive-delivery/README.md) | -| Explicitly request one ticket through a new branch to a PR | [Ticket to PR](../plugins/task-recipe/darrow-ticket-to-pr/README.md) | -| Study the former static workflow or run a comparison | [Ticket pipeline reference](../plugins/orchestration/darrow-ticket-pipeline/README.md) | +| Your next task | Plugin and local instructions | +| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Assess whether a request is ready to implement | [Readiness gate](../plugins/capability/darrow-readiness-gate/README.md) | +| Clarify behavior or make an implementation plan | [Discovery](../plugins/capability/darrow-discovery/README.md) | +| Implement an observable behavior test-first | [TDD](../plugins/capability/darrow-tdd/README.md) | +| Review a change independently | [Review](../plugins/capability/darrow-review/README.md) | +| Verify implementation acceptance through independent assessment | [Verification](../plugins/capability/darrow-verification/README.md) | +| Create a branch, commit, or pull request | [Git](../plugins/capability/darrow-git/README.md) | +| Read, list, create, or update GitHub issues | [GitHub tickets](../plugins/capability/darrow-tickets-github/README.md) | +| Understand technical structure in a compact visual | [Explanation](../plugins/capability/darrow-explanation/README.md) | +| Export Codex turn telemetry to Langfuse | [Langfuse observability](../plugins/capability/darrow-observability-langfuse/README.md) | +| Organize repository agent guidance | [Information architecture](../plugins/foundation/darrow-information-architecture/README.md) | +| Capture or find accepted decisions | [Decisions](../plugins/foundation/darrow-decisions/README.md) | +| Create or validate a reusable skill | [Skill authoring](../plugins/foundation/darrow-skill-authoring/README.md) | +| Explicitly hand a bounded engineering task to one execution owner | [Adaptive Delivery](../plugins/orchestration/darrow-adaptive-delivery/README.md) | +| Check whether Codex or Claude Code can support Adaptive Delivery delegation | [Adaptive Delivery doctor](../plugins/orchestration/darrow-adaptive-delivery/README.md#doctor-adaptive-delivery) | +| Explicitly request one ticket through a new branch to a PR | [Ticket to PR](../plugins/task-recipe/darrow-ticket-to-pr/README.md) | +| Study the former static workflow or run a comparison | [Ticket pipeline reference](../plugins/orchestration/darrow-ticket-pipeline/README.md) | This is the complete shipped plugin catalog. Planned names are not installation targets; consult the [marketplace manifest](../.claude-plugin/marketplace.json) diff --git a/docs/specs/adaptive-delivery.md b/docs/specs/adaptive-delivery.md index 5c948c40..804ea6ae 100644 --- a/docs/specs/adaptive-delivery.md +++ b/docs/specs/adaptive-delivery.md @@ -7,6 +7,7 @@ verification, feedback, and completion. Plugin: `darrow-adaptive-delivery` Skill: `adaptive-delivery` +Companion capability: `doctor-adaptive-delivery` ## Why @@ -89,17 +90,19 @@ reason: explicit-orchestration-entrypoint-required The contained Python package is `/backend`, invoked through `uv run --quiet --frozen --no-dev --project ` followed by -`adaptive-delivery-preflight` or `claude-agent-route`. On Codex, the activated +`adaptive-delivery-preflight`, `claude-agent-route`, or `host-config-doctor`. On Codex, the activated file `/skills/adaptive-delivery/SKILL.md` binds that same plugin root; `../../backend` starts at the directory containing `SKILL.md`. Claude binds `${CLAUDE_PLUGIN_ROOT}/backend`. Check the package and lock at that exact location; unavailability does not authorize searching for a different installation. The package supports Python 3.10–3.13, UV, and Git on Linux, macOS, and native -Windows, with no runtime dependencies or Bash entrypoint adapters. Native paths -and argument-vector subprocesses preserve spaces, Unicode, and linked-worktree -identity. It retains the prepared-v2, route-v2, and Claude-agent-route-v1 records, -record ordering, policy precedence, refusal exit 2, and help exit 0. Commands emit +Windows, with no Bash entrypoint adapters. Python 3.10 uses the locked `tomli` +backport for host-configuration diagnosis; newer interpreters use `tomllib` from +the standard library. Native paths and argument-vector subprocesses preserve +spaces, Unicode, and linked-worktree identity. It retains the prepared-v2, +route-v2, and Claude-agent-route-v1 records, record ordering, policy precedence, +refusal exit 2, and help exit 0. Commands emit UTF-8 stdout/stderr with LF records regardless of the host's redirected console encoding, including native Windows code pages. JSON uses the standard-library parser; syntax diagnostics may name its line/column while @@ -156,6 +159,54 @@ If a material product choice, permission, destructive scope, security policy, or publication authority is missing before launch, ask the smallest concrete question and do not start the owner. +## Host-configuration doctor + +`doctor-adaptive-delivery` is a separate, read-only capability for diagnosing +whether the effective Codex or Claude Code host configuration can support the +delegation topology required by Adaptive Delivery. Ordinary doctor intent may +select this capability, but must never activate `adaptive-delivery` or launch an +owner. + +The doctor identifies the host, installed version when observable, and the +exact effective source it checked. For Codex it checks the active +`CODEX_HOME/config.toml` (or the default user configuration under `HOME` on +Unix and `USERPROFILE` on native Windows when `CODEX_HOME` is unset), not a +similarly named repository file. An isolated evaluation home is +reported as such so the source checkout's `.codex/config.toml` is not mistaken +for the evaluated process's configuration. It reports absent, unreadable, +malformed, explicitly disabled, unset/default, inadequate, and adequate states +without printing unrelated configuration or credentials. + +Codex diagnosis evaluates `agents.enabled` and +`agents.max_concurrent_threads_per_session` separately. The concurrency limit +counts spawned-agent threads and excludes the primary. `agents.max_depth` is a +different nesting control for the V1 backend and is explicitly reported as +ignored by V2; it can never compensate for inadequate V2 concurrency. + +Claude Code diagnosis checks the effective process environment and installed +version. On versions that support them it evaluates +`CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS` and +`CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH` as separate controls. It reports version +or backend applicability honestly rather than inventing support for an older +or unobserved host. + +Capacity conclusions derive from this topology, where the primary thread is +not itself a spawned-agent slot: + +```text +primary -> owner -> verification coordinator -> review coordinator + -> Standards reader + -> Spec reader +``` + +The baseline owner-only path needs one spawned-agent slot and one layer below +the primary. The full required-assessment path needs five concurrently open +spawned-agent slots and four layers of nesting. A doctor result distinguishes +those two conclusions and gives a source-specific setup action for every known +inadequate or disabled value. Unknown host defaults, versions, or backends stay +unknown rather than being called adequate. Diagnostics never mutate host or +repository configuration. + ## Goal contract The contract is self-contained and concise. It contains, in plain language: @@ -705,6 +756,13 @@ Nested host processes are not an adaptive-delivery fallback. capability evidence and applies the one/many/zero-match policy above. The recipe owns no branch choice; the Git capability owns deterministic discovery and preparation. +20. **ADL-D1 — Separate read-only host diagnosis.** Host-configuration doctor + intent selects `doctor-adaptive-delivery`, never activates orchestration, + and makes no configuration or repository mutation. Its result identifies + the effective source and host/version applicability, keeps concurrency and + nesting distinct, derives baseline and full-path capacity from the topology + above, refuses unreadable or malformed required input, and never exposes + unrelated values or credentials. ## Packaging and portability diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 0ea37802..6f64f593 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -47,6 +47,22 @@ and refuse conflicts; ticket retrieval refuses a foreign-project URL. A refusal does not authorize force, fallback mutations, or bypassing hooks. Resolve the exact missing input or authority through the responsible workflow. +## Adaptive Delivery cannot launch a nested coordinator or reader + +Invoke `doctor-adaptive-delivery` from the installed +`darrow-adaptive-delivery` plugin. It performs a read-only check of the current +host's effective configuration, reports the exact source, and distinguishes +the one-slot owner-only path from the five-slot, four-layer full verification +and review topology. + +For Codex, keep `agents.max_concurrent_threads_per_session` separate from +`agents.max_depth`; V2 ignores the latter. In an isolated eval, diagnose the +eval process's `CODEX_HOME/config.toml`, not this checkout's +`.codex/config.toml`. For Claude Code, the doctor checks concurrency and spawn +depth separately and reports whether the installed version supports each +control. Follow the source-specific guidance in the result and start a fresh +host session after changing configuration yourself. + ## Two sources disagree Cite both paths and the concrete claims. Specifications and accepted decisions diff --git a/plugins/orchestration/README.md b/plugins/orchestration/README.md index d4395a84..8713c844 100644 --- a/plugins/orchestration/README.md +++ b/plugins/orchestration/README.md @@ -6,7 +6,8 @@ installable from the capabilities they may compose. ## Choose a plugin -- [Adaptive Delivery](darrow-adaptive-delivery/README.md) is the current bounded engineering helper. +- [Adaptive Delivery](darrow-adaptive-delivery/README.md) provides the current + bounded engineering helper and a separate read-only host-capacity doctor. - [Ticket pipeline](darrow-ticket-pipeline/README.md) is an installable deprecated reference for deliberate comparisons. Read the chosen plugin's local prerequisites, expected effects, and safety diff --git a/plugins/orchestration/darrow-adaptive-delivery/.claude-plugin/plugin.json b/plugins/orchestration/darrow-adaptive-delivery/.claude-plugin/plugin.json index c765a08e..88d429ae 100644 --- a/plugins/orchestration/darrow-adaptive-delivery/.claude-plugin/plugin.json +++ b/plugins/orchestration/darrow-adaptive-delivery/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "darrow-adaptive-delivery", - "description": "Adaptive Delivery: ready and compile bounded engineering requests, then launch one routed subagent owner", - "version": "0.22.5", + "description": "Adaptive Delivery: diagnose host capacity or launch one routed owner for bounded engineering work", + "version": "0.23.0", "license": "BUSL-1.1", "author": { "name": "Björn Rochel", diff --git a/plugins/orchestration/darrow-adaptive-delivery/.codex-plugin/plugin.json b/plugins/orchestration/darrow-adaptive-delivery/.codex-plugin/plugin.json index f065bd0e..d9f65cbc 100644 --- a/plugins/orchestration/darrow-adaptive-delivery/.codex-plugin/plugin.json +++ b/plugins/orchestration/darrow-adaptive-delivery/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "darrow-adaptive-delivery", - "version": "0.22.5", - "description": "Adaptive Delivery: ready and compile bounded engineering requests, then launch one routed subagent owner", + "version": "0.23.0", + "description": "Adaptive Delivery: diagnose host capacity or launch one routed owner for bounded engineering work", "author": { "name": "Björn Rochel", "email": "bjoern@bjro.de" @@ -10,12 +10,12 @@ "interface": { "displayName": "Darrow -> Adaptive Delivery", "shortDescription": "One routed owner for bounded engineering goals", - "longDescription": "Adaptive Delivery prepares a bounded engineering request, reuses or obtains readiness, binds intent-matched skills, selects a workflow, risk, model, and effort, and launches exactly one separate subagent owner without a lifecycle ledger.", + "longDescription": "Adaptive Delivery can diagnose host delegation capacity read-only, or prepare a bounded engineering request and launch exactly one separate routed owner without a lifecycle ledger.", "developerName": "Björn Rochel", "category": "Productivity", "capabilities": ["Interactive", "Read", "Write"], "defaultPrompt": [ - "Preflight this bounded engineering request and launch one route-selected subagent owner." + "Diagnose this host's delivery capacity or preflight a bounded request and launch one route-selected owner." ] } } diff --git a/plugins/orchestration/darrow-adaptive-delivery/README.md b/plugins/orchestration/darrow-adaptive-delivery/README.md index 18de5ee0..f198911a 100644 --- a/plugins/orchestration/darrow-adaptive-delivery/README.md +++ b/plugins/orchestration/darrow-adaptive-delivery/README.md @@ -1,7 +1,9 @@ # Darrow Adaptive Delivery -This plugin compiles one bounded engineering request and launches exactly one -route-selected, host-visible subagent as its work owner. +This plugin provides two separate entrypoints: a read-only host-configuration +doctor, and explicit Adaptive Delivery orchestration that compiles one bounded +engineering request and launches exactly one route-selected, host-visible +subagent as its work owner. ```text request + repository -> read-only preflight -> readiness -> contract -> one owner @@ -15,6 +17,9 @@ feedback, independent review, authorized publication, and completion. an explicitly invoked orchestration entrypoint. Ordinary engineering intent never starts it. +`doctor-adaptive-delivery` is an ordinary intent-matched capability. Asking it +to check host configuration never starts orchestration or launches an owner. + [![Adaptive Delivery: preflight launches one execution owner, which invokes verification and handles bounded repairs.](https://raw.githubusercontent.com/BjRo/darrow/main/docs/assets/adaptive-delivery-capabilities.svg)](https://github.com/BjRo/darrow/blob/main/docs/assets/adaptive-delivery-capabilities.svg) Preflight prepares the contract and selects compatible capabilities. @@ -69,6 +74,27 @@ The complete goal contract follows inline. Host acceptance of the explicitly routed subagent launch proves the model and effort. The owner does not create a second nested goal or replacement adaptive owner. +## `doctor-adaptive-delivery` + +The doctor checks the effective Codex or Claude Code delegation controls +without modifying them. It reports the exact configuration source, host or +backend applicability, and separate conclusions for: + +- the baseline owner-only path: one spawned-agent slot and one nested layer; +- the full required-assessment path: five spawned-agent slots and four nested + layers across owner, verification, review, and two parallel readers. + +For Codex it checks `agents.enabled` and +`agents.max_concurrent_threads_per_session`. It reports `agents.max_depth` as a +V1-only nesting control that V2 ignores. For Claude Code it checks the effective +process values of `CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS` and +`CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH` only where the installed version supports +them. Absent, unreadable, malformed, disabled, inadequate, and unknown states +remain distinct, and the output never includes unrelated settings or secrets. + +Example: _“Check whether this Codex configuration supports Adaptive Delivery's +full verification path.”_ + Example: _“Use adaptive-delivery to diagnose and fix the intermittent cache test.”_ ## Workflows and risk @@ -126,20 +152,25 @@ already completed. ## Python helpers -The contained `backend` package exposes two read-only preflight operations. +The contained `backend` package exposes read-only preflight, route, and host +diagnosis operations. Prefix each command with `uv run --quiet --frozen --no-dev --project "/backend"`: ```text adaptive-delivery-preflight prepare --repo --host adaptive-delivery-preflight route --repo --host --profile [--route ] +host-config-doctor codex --config --backend --context +host-config-doctor claude [--version ] ``` `prepare` reports the repository root, revision, working-tree state, instructions, workflow documents, and active route catalog. `route` resolves a policy or exact user-supplied route. Repository overrides live in `.darrow/config.json`; malformed, unsafe, unknown, duplicate, or host-inconsistent -route configuration fails closed. +route configuration fails closed. `host-config-doctor` reads only the effective +host controls named by the doctor skill and reports baseline and full-path +capacity without changing configuration. The helper does not launch models, persist objectives, record lifecycle state, render completion reports, supervise work, or provide nested host sessions. @@ -181,9 +212,10 @@ Use this for an explicitly invoked bounded engineering outcome. Ordinary complex Codex with native subagent support or Claude Code with the bundled foreground route agents; [UV and Python](https://github.com/BjRo/darrow/blob/main/docs/installing-plugins.md#uv-and-python-for-plugin-helpers), Git, available routes, and the capabilities matching authorized operations. -The helpers support Linux, macOS, and native Windows without Bash or runtime -Python dependencies. Regression tests run through the repository's Python -quality gate on all three platforms. +The helpers support Python 3.10–3.13 on Linux, macOS, and native Windows without +Bash. Python 3.10 installs the locked `tomli` backport for host-configuration +diagnosis; newer interpreters use the standard-library TOML parser. Regression +tests run through the repository's Python quality gate on all three platforms. ## Installation @@ -201,6 +233,10 @@ start it. For example: Select `adaptive-delivery` from Codex's `$` menu, or invoke `/darrow-adaptive-delivery:adaptive-delivery` in Claude Code and provide the bounded request. +For read-only diagnosis, select `doctor-adaptive-delivery` from Codex's `$` +menu or invoke `/darrow-adaptive-delivery:doctor-adaptive-delivery` in Claude +Code. + ## Expected result Read-only preflight and readiness, then one routed execution owner. Effects depend on the explicit contract; completion alone grants no publication or tracker authority. @@ -208,6 +244,10 @@ Read-only preflight and readiness, then one routed execution owner. Effects depe ## Troubleshooting If readiness is not ready, resolve its findings before launch. Malformed route configuration or an unavailable exact capability binding must be reported. Keep feedback with the same accepted owner. +If an owner, verification coordinator, review coordinator, or parallel reader +cannot launch, run `doctor-adaptive-delivery` in the affected host session. Use +the exact source it reports; in isolated Codex evals, that is the eval +`CODEX_HOME/config.toml`, not the checkout's `.codex/config.toml`. For host discovery problems, use the [installation checks](https://github.com/BjRo/darrow/blob/main/docs/troubleshooting.md). Report the exact host/plugin versions and refusal without credentials. diff --git a/plugins/orchestration/darrow-adaptive-delivery/backend/pyproject.toml b/plugins/orchestration/darrow-adaptive-delivery/backend/pyproject.toml index dd8e736b..e6f87dee 100644 --- a/plugins/orchestration/darrow-adaptive-delivery/backend/pyproject.toml +++ b/plugins/orchestration/darrow-adaptive-delivery/backend/pyproject.toml @@ -1,17 +1,18 @@ [project] name = "darrow-adaptive-delivery" -version = "0.22.3" -description = "Contained preflight, route resolution, and eval mechanics for Darrow" +version = "0.23.0" +description = "Contained preflight, host diagnosis, route resolution, and eval mechanics for Darrow" requires-python = ">=3.10,<3.14" -dependencies = [] +dependencies = ["tomli>=2,<3; python_version < '3.11'"] [project.scripts] adaptive-delivery-preflight = "darrow_adaptive_delivery.cli:preflight" claude-agent-route = "darrow_adaptive_delivery.cli:claude_route" adaptive-delivery-fixture = "darrow_adaptive_delivery.fixtures.cli:main" +host-config-doctor = "darrow_adaptive_delivery.cli:host_doctor" [dependency-groups] -dev = ["coverage[toml]>=7.10,<8", "editables>=0.3,<1", "hatchling==1.32.3", "hypothesis>=6.138,<7", "mypy>=1.18,<2", "pytest>=8.4,<9", "ruff>=0.12,<1"] +dev = ["coverage[toml]>=7.10,<8", "editables>=0.3,<1", "hatchling==1.32.3", "hypothesis>=6.138,<7", "mypy>=1.18,<2", "pytest>=8.4,<9", "ruff>=0.12,<1", "tomli>=2,<3"] [build-system] requires = ["hatchling==1.32.3"] diff --git a/plugins/orchestration/darrow-adaptive-delivery/backend/src/darrow_adaptive_delivery/cli.py b/plugins/orchestration/darrow-adaptive-delivery/backend/src/darrow_adaptive_delivery/cli.py index 26b1ff6e..6102032d 100644 --- a/plugins/orchestration/darrow-adaptive-delivery/backend/src/darrow_adaptive_delivery/cli.py +++ b/plugins/orchestration/darrow-adaptive-delivery/backend/src/darrow_adaptive_delivery/cli.py @@ -30,3 +30,22 @@ def preflight() -> int: def claude_route() -> int: return execute("claude-agent-route", claude.run) + + +def host_doctor() -> int: + from . import doctor + + for stream in (sys.stdout, sys.stderr): + if isinstance(stream, TextIOWrapper): + stream.reconfigure(encoding="utf-8", newline="\n") + try: + output = doctor.run(sys.argv[1:]) + except doctor.UsageError: + sys.stderr.write(doctor.USAGE) + return 2 + except doctor.DiagnosisError as error: + sys.stdout.write(error.output) + sys.stderr.write(error.diagnostic) + return 1 + sys.stdout.write(output) + return 0 diff --git a/plugins/orchestration/darrow-adaptive-delivery/backend/src/darrow_adaptive_delivery/doctor.py b/plugins/orchestration/darrow-adaptive-delivery/backend/src/darrow_adaptive_delivery/doctor.py new file mode 100644 index 00000000..8291ba8b --- /dev/null +++ b/plugins/orchestration/darrow-adaptive-delivery/backend/src/darrow_adaptive_delivery/doctor.py @@ -0,0 +1,476 @@ +"""Read-only host capacity diagnosis for Adaptive Delivery.""" + +from __future__ import annotations + +import os +import re +import subprocess +import sys +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from pathlib import Path + +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + +from .arguments import options +from .common import RefusalError + +VersionProbe = Callable[[], str] + +USAGE = """usage: host-config-doctor codex [--config PATH] [--backend v1|v2|unknown] [--context effective|isolated-eval] + host-config-doctor claude [--version VERSION] +""" + +TOPOLOGY = ( + "topology: primary -> owner -> verification coordinator -> review coordinator " + "-> parallel Standards/Spec readers\n" + "required_spawned_slots: baseline=1 full=5 (primary excluded)\n" + "required_nesting_layers: baseline=1 full=4\n" +) + + +class UsageError(Exception): + """Invalid command arguments; the process boundary prints usage.""" + + +class DiagnosisError(Exception): + """A safe refusal with any records emitted before the failure.""" + + def __init__(self, output: str, status: str, reason: str) -> None: + self.output = output + self.diagnostic = f"status: {status}\nreason: {reason}\n" + super().__init__(self.diagnostic.rstrip()) + + +@dataclass(frozen=True) +class AgentSettings: + enabled: bool | None + concurrency: int | None + depth: int | None + + +def _lines(*values: str) -> str: + return "\n".join(values) + "\n" + + +def _parse_options(args: list[str], names: set[str]) -> dict[str, str]: + try: + return options(args, names, "host doctor ") + except RefusalError as error: + raise UsageError from error + + +def _default_config(environment: Mapping[str, str]) -> str: + codex_home = environment.get("CODEX_HOME") + if codex_home: + return str(Path(codex_home) / "config.toml") + home = environment.get("HOME") or environment.get("USERPROFILE") + if home: + return str(Path(home) / ".codex/config.toml") + raise DiagnosisError( + "", "unavailable", "HOME, USERPROFILE, and CODEX_HOME are unset" + ) + + +def _resolve_config( + configured: str | None, environment: Mapping[str, str], cwd: Path +) -> Path: + value = _default_config(environment) if configured is None else configured + candidate = Path(value) + if not candidate.is_absolute(): + candidate = cwd / candidate + try: + return candidate.parent.resolve(strict=True) / candidate.name + except OSError as error: + raise DiagnosisError( + "", "unavailable", "configuration parent directory cannot be resolved" + ) from error + + +def _codex_header(source: Path, backend: str, context: str) -> str: + return ( + _lines( + "format: darrow-adaptive-delivery-host-doctor-v1", + "host: codex", + f"configuration_source: {source}", + f"configuration_context: {context}", + "checkout_config_used: no", + f"backend: {backend}", + "concurrency_control: agents.max_concurrent_threads_per_session", + "nesting_control: agents.max_depth (V1 only; ignored by V2)", + ) + + TOPOLOGY + ) + + +def _optional_bool(values: Mapping[str, object], name: str) -> bool | None: + if name not in values: + return None + value = values[name] + if type(value) is not bool: + raise ValueError + return value + + +def _optional_int( + values: Mapping[str, object], name: str, *, positive: bool +) -> int | None: + if name not in values: + return None + value = values[name] + if type(value) is not int or (positive and value <= 0): + raise ValueError + return value + + +def _agent_settings(document: Mapping[str, object]) -> AgentSettings: + agents = document.get("agents", {}) + if not isinstance(agents, dict): + raise ValueError + return AgentSettings( + enabled=_optional_bool(agents, "enabled"), + concurrency=_optional_int( + agents, "max_concurrent_threads_per_session", positive=True + ), + depth=_optional_int(agents, "max_depth", positive=False), + ) + + +def _read_settings(source: Path, header: str) -> AgentSettings: + if not source.is_file() or not os.access(source, os.R_OK): + raise DiagnosisError( + header, + "unreadable", + "effective configuration is not a readable regular file", + ) + try: + document = tomllib.loads(source.read_bytes().decode("utf-8")) + return _agent_settings(document) + except (tomllib.TOMLDecodeError, UnicodeError, ValueError) as error: + raise DiagnosisError( + header, + "malformed", + "invalid or duplicate Adaptive Delivery agent setting", + ) from error + except OSError as error: + raise DiagnosisError( + header, "unreadable", "failed while reading effective configuration" + ) from error + + +def _codex_absent(source: Path, backend: str) -> str: + nesting = ( + "not-applicable (agents.max_depth is V1-only and ignored by V2)" + if backend == "v2" + else "unknown" + ) + return _lines( + "status: absent", + "delegation: enabled-by-default", + "concurrency: unknown (host default is not a configured value)", + f"nesting: {nesting}", + "baseline_owner_only: unknown", + "full_required_assessment: unknown", + f"guidance: create {source} with [agents], enabled = true, and " + "max_concurrent_threads_per_session = 5 or greater", + ) + + +def _concurrency_capacity(concurrency: int | None) -> tuple[str, str]: + if concurrency is None: + return "unknown", "unknown" + if concurrency >= 5: + return "supported", "supported" + return "supported", "unsupported" + + +def _v1_capacity(baseline: str, full: str, depth: int | None) -> tuple[str, str]: + if depth is None: + return "unknown", "unknown" + if depth < 1: + return "unsupported", "unsupported" + if depth < 4: + return baseline, "unsupported" + return baseline, full + + +def _codex_capacity(settings: AgentSettings, backend: str) -> tuple[str, str]: + baseline, full = _concurrency_capacity(settings.concurrency) + if backend == "unknown": + return "unknown", "unknown" + return ( + _v1_capacity(baseline, full, settings.depth) + if backend == "v1" + else (baseline, full) + ) + + +def _codex_values(settings: AgentSettings, backend: str) -> tuple[str, str, str]: + delegation = { + True: "enabled", + False: "disabled", + None: "enabled-by-default", + }[settings.enabled] + concurrency = ( + "unknown (host default is not a configured value)" + if settings.concurrency is None + else ( + f"adequate ({settings.concurrency}; full path requires 5)" + if settings.concurrency >= 5 + else f"inadequate ({settings.concurrency}; baseline requires 1, full path requires 5)" + ) + ) + if backend == "v2": + depth = "unset" if settings.depth is None else str(settings.depth) + nesting = ( + f"not-applicable (agents.max_depth={depth} is V1-only and ignored by V2)" + ) + elif backend == "unknown": + nesting = "unknown (backend was not established; max_depth applies only to V1)" + elif settings.depth is None: + nesting = "unknown (agents.max_depth is unset)" + elif settings.depth >= 4: + nesting = f"adequate ({settings.depth}; full path requires 4)" + else: + nesting = ( + f"inadequate ({settings.depth}; baseline requires 1, full path requires 4)" + ) + return delegation, concurrency, nesting + + +def _codex_result(source: Path, settings: AgentSettings, backend: str) -> str: + delegation, concurrency, nesting = _codex_values(settings, backend) + if settings.enabled is False: + baseline, full = "unsupported", "unsupported" + actions = ["agents.enabled = true"] + if settings.concurrency is not None and settings.concurrency < 5: + actions.append("agents.max_concurrent_threads_per_session = 5 or greater") + if backend == "v1" and settings.depth is not None and settings.depth < 4: + actions.append("agents.max_depth = 4 or greater") + guidance = f"set {', '.join(actions)} in {source}" + else: + baseline, full = _codex_capacity(settings, backend) + guidance = ( + "no Adaptive Delivery capacity change is required" + if full == "supported" + else "set agents.max_concurrent_threads_per_session = 5 or greater in " + f"{source}; for V1 also set agents.max_depth = 4 or greater" + ) + return _lines( + "status: valid", + f"delegation: {delegation}", + f"concurrency: {concurrency}", + f"nesting: {nesting}", + f"baseline_owner_only: {baseline}", + f"full_required_assessment: {full}", + f"guidance: {guidance}", + ) + + +def _codex(args: list[str], environment: Mapping[str, str], cwd: Path) -> str: + values = _parse_options(args, {"--config", "--backend", "--context"}) + backend = values.get("backend", "unknown") + context = values.get("context", "effective") + if backend not in {"v1", "v2", "unknown"}: + raise UsageError + if context not in {"effective", "isolated-eval"}: + raise UsageError + source = _resolve_config(values.get("config"), environment, cwd) + header = _codex_header(source, backend, context) + if not source.exists(): + return header + _codex_absent(source, backend) + return header + _codex_result(source, _read_settings(source, header), backend) + + +def _default_version_probe() -> str: + try: + result = subprocess.run( + ["claude", "--version"], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + check=False, + ) + except OSError: + return "" + return result.stdout + + +def _version(value: str | None, probe: VersionProbe) -> str: + if value is None: + try: + match = re.search(r"[0-9]+\.[0-9]+\.[0-9]+", probe()) + except OSError: + return "unknown" + return match.group(0) if match else "unknown" + return value if re.fullmatch(r"[0-9]+(?:\.[0-9]+)*", value) else "unknown" + + +def _at_least(version: str, required: tuple[int, int, int]) -> bool: + parts = tuple(int(value) for value in version.split(".")[:3]) + return parts + (0,) * (3 - len(parts)) >= required + + +def _claude_header(version: str) -> str: + return ( + _lines( + "format: darrow-adaptive-delivery-host-doctor-v1", + "host: claude", + "configuration_source: process-environment", + f"host_version: {version}", + "concurrency_control: CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS", + "nesting_control: CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH", + ) + + TOPOLOGY + ) + + +def _positive_control(value: str | None, name: str, header: str) -> int | None: + if not value: + return None + if not re.fullmatch(r"[1-9][0-9]*", value): + raise DiagnosisError( + header, "malformed", f"{name} must be a positive whole number" + ) + return int(value) + + +def _claude_unknown() -> str: + return _lines( + "status: version-unknown", + "concurrency_control_applicability: unknown (installed version not established)", + "nesting_control_applicability: unknown (installed version not established)", + "concurrency: unknown (installed version applicability was not established)", + "nesting: unknown (installed version applicability was not established)", + "baseline_owner_only: unknown", + "full_required_assessment: unknown", + "guidance: provide the installed Claude Code version before changing either control", + ) + + +def _claude_controls( + version: str, environment: Mapping[str, str], header: str +) -> tuple[list[str], str, str]: + if not _at_least(version, (2, 1, 217)): + return ( + [ + "concurrency_control_applicability: unsupported (requires Claude Code 2.1.217 or later)", + "nesting_control_applicability: fixed host behavior (environment control requires Claude Code 2.1.217 or later)", + "concurrency: not-applicable (control requires Claude Code 2.1.217 or later)", + ], + "unknown", + "unknown", + ) + concurrency = _positive_control( + environment.get("CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS"), + "CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS", + header, + ) + depth = _positive_control( + environment.get("CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH"), + "CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH", + header, + ) + effective = 20 if concurrency is None else concurrency + concurrency_state = "yes" if effective >= 5 else "no" + adjective = "adequate" if concurrency_state == "yes" else "inadequate" + return ( + [ + "concurrency_control_applicability: supported (Claude Code 2.1.217 or later)", + "nesting_control_applicability: supported (Claude Code 2.1.217 or later)", + f"concurrency: {adjective} ({effective}; " + + ( + "full path requires 5)" + if effective >= 5 + else "baseline requires 1, full path requires 5)" + ), + ], + concurrency_state, + "" if depth is None else str(depth), + ) + + +def _claude_nesting(version: str, depth: str) -> tuple[str, str]: + if _at_least(version, (2, 1, 219)): + effective = 3 if not depth else int(depth) + elif _at_least(version, (2, 1, 217)): + effective = 1 if not depth else int(depth) + elif _at_least(version, (2, 1, 172)): + return "adequate (host default 5; control not supported by this version)", "yes" + else: + return "unknown (version predates documented nesting behavior)", "unknown" + state = "yes" if effective >= 4 else "no" + adjective = "adequate" if state == "yes" else "inadequate" + requirement = ( + "full path requires 4" + if state == "yes" + else "baseline requires 1, full path requires 4" + ) + return f"{adjective} ({effective}; {requirement})", state + + +def _claude_result(version: str, environment: Mapping[str, str], header: str) -> str: + controls, concurrency, depth = _claude_controls(version, environment, header) + nesting, nesting_state = _claude_nesting(version, depth) + baseline = "unknown" if concurrency == "unknown" else "supported" + if concurrency == "yes" and nesting_state == "yes": + full = "supported" + elif "no" in {concurrency, nesting_state}: + full = "unsupported" + else: + full = "unknown" + if full == "supported": + guidance = "no Adaptive Delivery capacity change is required" + elif _at_least(version, (2, 1, 217)): + guidance = ( + "set CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS=5 and " + "CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH=4 in the effective Claude Code " + "environment, then start a new session" + ) + else: + guidance = ( + "upgrade Claude Code to a version with documented concurrency control " + "before relying on the full path" + ) + return _lines( + *controls, + f"nesting: {nesting}", + "status: valid", + f"baseline_owner_only: {baseline}", + f"full_required_assessment: {full}", + f"guidance: {guidance}", + ) + + +def _claude( + args: list[str], environment: Mapping[str, str], probe: VersionProbe +) -> str: + values = _parse_options(args, {"--version"}) + version = _version(values.get("version"), probe) + header = _claude_header(version) + if version == "unknown": + return header + _claude_unknown() + return header + _claude_result(version, environment, header) + + +def run( + args: list[str], + *, + environment: Mapping[str, str] | None = None, + cwd: Path | None = None, + version_probe: VersionProbe | None = None, +) -> str: + """Return one diagnosis or raise a safe usage/diagnosis refusal.""" + if not args: + raise UsageError + values = os.environ if environment is None else environment + host, host_args = args[0], args[1:] + if host == "codex": + return _codex(host_args, values, Path.cwd() if cwd is None else cwd) + if host == "claude": + return _claude(host_args, values, version_probe or _default_version_probe) + raise UsageError diff --git a/plugins/orchestration/darrow-adaptive-delivery/backend/tests/fresh_install.py b/plugins/orchestration/darrow-adaptive-delivery/backend/tests/fresh_install.py index c0e730f1..0ac50ae3 100644 --- a/plugins/orchestration/darrow-adaptive-delivery/backend/tests/fresh_install.py +++ b/plugins/orchestration/darrow-adaptive-delivery/backend/tests/fresh_install.py @@ -107,6 +107,34 @@ def routes(backend: Path, repo: Path) -> None: assert command(repo, "git", "status", "--porcelain=v1") == before +def diagnoses(backend: Path, repo: Path) -> None: + config = repo / "codex home/config.toml" + config.parent.mkdir() + config.write_text( + "[agents]\nenabled = true\nmax_concurrent_threads_per_session = 5\n", + encoding="utf-8", + ) + codex = runtime( + backend, + repo, + "host-config-doctor", + "codex", + "--config", + str(config), + "--backend", + "v2", + "--context", + "effective", + ) + assert f"configuration_source: {config.resolve()}\n" in codex + assert "full_required_assessment: supported\n" in codex + claude = runtime( + backend, repo, "host-config-doctor", "claude", "--version", "2.1.219" + ) + assert "configuration_source: process-environment\n" in claude + assert "full_required_assessment: unsupported\n" in claude + + def fixtures(plugin: Path, repo: Path) -> None: backend = plugin / "backend" fixture_dir = plugin / "skills/adaptive-delivery/evals/fixtures" @@ -180,6 +208,7 @@ def fixtures(plugin: Path, repo: Path) -> None: def validate(plugin: Path, temporary: Path) -> None: backend = plugin / "backend" assert not (plugin / "bin").exists() + assert not (plugin / "skills/doctor-adaptive-delivery/scripts").exists() command(temporary, "uv", "sync", "--frozen", "--no-dev", "--project", str(backend)) tree = command( temporary, "uv", "tree", "--frozen", "--no-dev", "--project", str(backend) @@ -190,6 +219,7 @@ def validate(plugin: Path, temporary: Path) -> None: repo = temporary / "repository ' with spaces-é" repository(repo) routes(backend, repo) + diagnoses(backend, repo) linked = temporary / "linked worktree-é" command(repo, "git", "worktree", "add", "-qb", "linked", str(linked)) nested = linked / "nested" @@ -230,7 +260,7 @@ def main() -> None: shutil.copytree(plugin, copied, ignore=ignored) validate(copied, temporary) print( - "fresh copied adaptive-delivery: both host routes, linked worktree, readiness, review, verification, and proof refusal passed; native owner launch remains host-owned" + "fresh copied adaptive-delivery: host doctor, both routes, linked worktree, readiness, review, verification, and proof refusal passed; native owner launch remains host-owned" ) diff --git a/plugins/orchestration/darrow-adaptive-delivery/backend/tests/test_doctor.py b/plugins/orchestration/darrow-adaptive-delivery/backend/tests/test_doctor.py new file mode 100644 index 00000000..ede7cdc2 --- /dev/null +++ b/plugins/orchestration/darrow-adaptive-delivery/backend/tests/test_doctor.py @@ -0,0 +1,395 @@ +from __future__ import annotations + +import os +import sys +from pathlib import Path + +import pytest + +from darrow_adaptive_delivery import cli, doctor + + +def diagnose_codex( + config: Path, + *, + backend: str = "v2", + context: str = "effective", + environment: dict[str, str] | None = None, +) -> str: + return doctor.run( + [ + "codex", + "--config", + str(config), + "--backend", + backend, + "--context", + context, + ], + environment=environment or {}, + ) + + +def write_config(path: Path, content: str) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + return path + + +def test_codex_v2_reports_adequate_capacity_and_ignores_depth(tmp_path: Path) -> None: + config = write_config( + tmp_path / "codex home/config.toml", + """[agents] +enabled = true +max_concurrent_threads_per_session = 5 +max_depth = 1 +""", + ) + + output = diagnose_codex(config) + + assert f"configuration_source: {config.resolve()}" in output + assert "configuration_context: effective" in output + assert "concurrency: adequate (5; full path requires 5)" in output + assert ( + "nesting: not-applicable (agents.max_depth=1 is V1-only and ignored by V2)" + in output + ) + assert "baseline_owner_only: supported" in output + assert "full_required_assessment: supported" in output + + +@pytest.mark.parametrize( + ("content", "expected"), + [ + ( + "[agents]\nenabled = false\nmax_concurrent_threads_per_session = 99\n", + "baseline_owner_only: unsupported", + ), + ( + "[agents]\nenabled = true\nmax_concurrent_threads_per_session = 4\n", + "full_required_assessment: unsupported", + ), + ], +) +def test_codex_reports_disabled_or_inadequate_capacity( + tmp_path: Path, content: str, expected: str +) -> None: + output = diagnose_codex(write_config(tmp_path / "config.toml", content)) + assert expected in output + + +def test_codex_disabled_guidance_includes_other_known_inadequate_controls( + tmp_path: Path, +) -> None: + config = write_config( + tmp_path / "config.toml", + """[agents] +enabled = false +max_concurrent_threads_per_session = 4 +max_depth = 3 +""", + ) + output = diagnose_codex(config, backend="v1") + assert "guidance: set agents.enabled = true" in output + assert "agents.max_concurrent_threads_per_session = 5 or greater" in output + assert "agents.max_depth = 4 or greater" in output + + +@pytest.mark.parametrize( + "content", + [ + "[agents]\nmax_concurrent_threads_per_session = 'nope'\n", + '{"agents":{"max_concurrent_threads_per_session":5}}\n', + "agents:\n max_concurrent_threads_per_session: 5\n", + "[agents]\nenabled = true\nenabled = false\n", + ], +) +def test_codex_rejects_malformed_or_wrongly_typed_toml( + tmp_path: Path, content: str +) -> None: + config = write_config(tmp_path / "config.toml", content) + with pytest.raises(doctor.DiagnosisError, match="status: malformed") as failure: + diagnose_codex(config) + assert "invalid or duplicate Adaptive Delivery agent setting" in str(failure.value) + + +def test_codex_accepts_unrelated_nested_toml(tmp_path: Path) -> None: + config = write_config( + tmp_path / "config.toml", + '''model = "gpt-test" +experimental_values = [ + ["one ] bracket"], + ["two"]] +instructions = """ +Keep this valid multiline TOML. +""" +[agents] +enabled = false +max_concurrent_threads_per_session = 5 +''', + ) + output = diagnose_codex(config) + assert "status: valid" in output + assert "delegation: disabled" in output + + +def test_codex_absent_is_unknown_and_names_isolated_source(tmp_path: Path) -> None: + config = tmp_path / "codex home/config.toml" + config.parent.mkdir() + output = diagnose_codex(config, context="isolated-eval") + assert "status: absent" in output + assert "configuration_context: isolated-eval" in output + assert "checkout_config_used: no" in output + assert "full_required_assessment: unknown" in output + + +def test_codex_uses_codex_home_instead_of_checkout(tmp_path: Path) -> None: + checkout = tmp_path / "checkout" + checkout_config = write_config( + checkout / ".codex/config.toml", + "[agents]\nmax_concurrent_threads_per_session = 99\n", + ) + home_config = write_config( + tmp_path / "home/config.toml", + "[agents]\nmax_concurrent_threads_per_session = 4\n", + ) + assert checkout_config.exists() + + output = doctor.run( + ["codex", "--backend", "v2", "--context", "isolated-eval"], + environment={"CODEX_HOME": str(home_config.parent)}, + cwd=checkout, + ) + + assert f"configuration_source: {home_config.resolve()}" in output + assert ( + "concurrency: inadequate (4; baseline requires 1, full path requires 5)" + in output + ) + assert "concurrency: adequate (99" not in output + + +def test_codex_uses_userprofile_on_native_windows(tmp_path: Path) -> None: + config = write_config( + tmp_path / ".codex/config.toml", + "[agents]\nmax_concurrent_threads_per_session = 5\n", + ) + output = doctor.run( + ["codex", "--backend", "v2"], + environment={"USERPROFILE": str(tmp_path)}, + ) + assert f"configuration_source: {config.resolve()}" in output + assert "full_required_assessment: supported" in output + + +def test_codex_requires_a_resolvable_default_source(tmp_path: Path) -> None: + with pytest.raises( + doctor.DiagnosisError, + match="HOME, USERPROFILE, and CODEX_HOME are unset", + ): + doctor.run(["codex", "--backend", "v2"], environment={}, cwd=tmp_path) + with pytest.raises( + doctor.DiagnosisError, + match="configuration parent directory cannot be resolved", + ): + diagnose_codex(tmp_path / "missing/config.toml") + + +def test_codex_rejects_non_file_and_read_failure( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + directory = tmp_path / "config.toml" + directory.mkdir() + with pytest.raises(doctor.DiagnosisError, match="readable regular file"): + diagnose_codex(directory) + + config = write_config(tmp_path / "real.toml", "[agents]\nenabled = true\n") + original = Path.read_bytes + + def fail_read(path: Path) -> bytes: + if path == config: + raise PermissionError("secret detail") + return original(path) + + monkeypatch.setattr(Path, "read_bytes", fail_read) + with pytest.raises(doctor.DiagnosisError, match="failed while reading") as failure: + diagnose_codex(config) + assert "secret detail" not in str(failure.value) + + +@pytest.mark.parametrize( + ("backend", "depth", "expected"), + [ + ("v1", None, "baseline_owner_only: unknown"), + ("v1", 0, "baseline_owner_only: unsupported"), + ("v1", 3, "full_required_assessment: unsupported"), + ("v1", 4, "full_required_assessment: supported"), + ("unknown", 4, "full_required_assessment: unknown"), + ], +) +def test_codex_keeps_v1_and_unknown_depth_results_distinct( + tmp_path: Path, backend: str, depth: int | None, expected: str +) -> None: + depth_line = "" if depth is None else f"max_depth = {depth}\n" + config = write_config( + tmp_path / f"{backend}-{depth}.toml", + "[agents]\nenabled = true\nmax_concurrent_threads_per_session = 5\n" + + depth_line, + ) + assert expected in diagnose_codex(config, backend=backend) + + +def diagnose_claude( + version: str | None, + environment: dict[str, str] | None = None, + *, + probe: doctor.VersionProbe | None = None, +) -> str: + args = ["claude"] if version is None else ["claude", "--version", version] + return doctor.run(args, environment=environment or {}, version_probe=probe) + + +def test_claude_supported_controls_report_full_capacity() -> None: + output = diagnose_claude( + "2.1.219", + { + "CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS": "5", + "CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH": "4", + }, + ) + assert "concurrency_control_applicability: supported" in output + assert "nesting_control_applicability: supported" in output + assert "concurrency: adequate (5; full path requires 5)" in output + assert "nesting: adequate (4; full path requires 4)" in output + assert "full_required_assessment: supported" in output + + +def test_claude_inadequate_and_default_capacity() -> None: + inadequate = diagnose_claude( + "2.1.219", + { + "CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS": "4", + "CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH": "3", + }, + ) + defaults = diagnose_claude( + "2.1.219", + { + "CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS": "", + "CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH": "", + }, + ) + assert ( + "concurrency: inadequate (4; baseline requires 1, full path requires 5)" + in inadequate + ) + assert ( + "nesting: inadequate (3; baseline requires 1, full path requires 4)" + in inadequate + ) + assert "concurrency: adequate (20; full path requires 5)" in defaults + assert ( + "nesting: inadequate (3; baseline requires 1, full path requires 4)" in defaults + ) + + +@pytest.mark.parametrize( + ("version", "nesting", "full"), + [ + ( + "2.1.216", + "nesting: adequate (host default 5; control not supported by this version)", + "full_required_assessment: unknown", + ), + ( + "2.1.171", + "nesting: unknown (version predates documented nesting behavior)", + "full_required_assessment: unknown", + ), + ( + "2.1.218", + "nesting: inadequate (1; baseline requires 1, full path requires 4)", + "full_required_assessment: unsupported", + ), + ], +) +def test_claude_version_boundaries(version: str, nesting: str, full: str) -> None: + output = diagnose_claude(version) + assert nesting in output + assert full in output + + +def test_claude_discovers_or_reports_unknown_version() -> None: + discovered = diagnose_claude(None, probe=lambda: "Claude Code 2.1.219 (stable)") + unknown = diagnose_claude(None, probe=lambda: "unparseable") + assert "host_version: 2.1.219" in discovered + assert "status: version-unknown" in unknown + assert "baseline_owner_only: unknown" in unknown + + +@pytest.mark.parametrize( + ("key", "value", "message"), + [ + ( + "CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS", + "secret-token", + "CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS must be a positive whole number", + ), + ( + "CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH", + "0", + "CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH must be a positive whole number", + ), + ], +) +def test_claude_rejects_invalid_controls_without_leaking_values( + key: str, value: str, message: str +) -> None: + with pytest.raises(doctor.DiagnosisError, match=message) as failure: + diagnose_claude("2.1.219", {key: value}) + assert value not in str(failure.value) + + +@pytest.mark.parametrize( + "args", + [[], ["other"], ["codex", "--backend"], ["codex", "--backend", "v3"]], +) +def test_usage_rejects_invalid_arguments(args: list[str]) -> None: + with pytest.raises(doctor.UsageError): + doctor.run(args, environment={}) + + +def test_cli_preserves_diagnosis_exit_contract( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + config = write_config( + tmp_path / "config.toml", + "[agents]\nmax_concurrent_threads_per_session = 'secret-token'\n", + ) + monkeypatch.setattr( + sys, "argv", ["host-config-doctor", "codex", "--config", str(config)] + ) + + assert cli.host_doctor() == 1 + captured = capsys.readouterr() + assert "configuration_source:" in captured.out + assert "status: malformed" in captured.err + assert "secret-token" not in captured.err + + +def test_cli_prints_usage_and_returns_two( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + monkeypatch.setattr(sys, "argv", ["host-config-doctor"]) + assert cli.host_doctor() == 2 + assert "usage: host-config-doctor codex" in capsys.readouterr().err + + +def test_default_claude_probe_handles_missing_binary( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("PATH", os.devnull) + assert "status: version-unknown" in diagnose_claude(None) diff --git a/plugins/orchestration/darrow-adaptive-delivery/backend/uv.lock b/plugins/orchestration/darrow-adaptive-delivery/backend/uv.lock index 2aefa67e..8b239afb 100644 --- a/plugins/orchestration/darrow-adaptive-delivery/backend/uv.lock +++ b/plugins/orchestration/darrow-adaptive-delivery/backend/uv.lock @@ -86,8 +86,11 @@ toml = [ [[package]] name = "darrow-adaptive-delivery" -version = "0.22.3" +version = "0.23.0" source = { editable = "." } +dependencies = [ + { name = "tomli", marker = "python_full_version < '3.11'" }, +] [package.dev-dependencies] dev = [ @@ -98,9 +101,11 @@ dev = [ { name = "mypy" }, { name = "pytest" }, { name = "ruff" }, + { name = "tomli" }, ] [package.metadata] +requires-dist = [{ name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2,<3" }] [package.metadata.requires-dev] dev = [ @@ -111,6 +116,7 @@ dev = [ { name = "mypy", specifier = ">=1.18,<2" }, { name = "pytest", specifier = ">=8.4,<9" }, { name = "ruff", specifier = ">=0.12,<1" }, + { name = "tomli", specifier = ">=2,<3" }, ] [[package]] diff --git a/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/SKILL.md b/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/SKILL.md new file mode 100644 index 00000000..124acddd --- /dev/null +++ b/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/SKILL.md @@ -0,0 +1,114 @@ +--- +name: doctor-adaptive-delivery +description: Diagnose whether the effective Codex or Claude Code host configuration supports Adaptive Delivery ownership and its required verification/review delegation. Use for host-configuration checks, capacity troubleshooting, or concurrency-versus-nesting questions. Do not select this capability to implement work or start adaptive-delivery orchestration. +--- + +# Doctor Adaptive Delivery + +Inspect the active host configuration without changing it, identify the exact +source checked, and explain whether it supports the baseline owner path and the +full required-assessment topology. + +## 1. Bind the host and effective source + +Use the current host when the request does not name one. If the request names a +different host, diagnose that host only when its effective source and installed +version are observable; otherwise ask for the missing source or version. Never +guess a backend or version. + +Resolve the bundled backend without searching: + +- Claude: use `${CLAUDE_PLUGIN_ROOT}/backend`; Claude substitutes the active + plugin's absolute root in skill content. +- Codex: starting at the directory containing this `SKILL.md`, use + `../../backend`. + +Require readable `pyproject.toml` and `uv.lock` in that exact backend, plus UV +and Python 3.10–3.13. Do not search the repository, home directory, plugin +caches, or `PATH` for a replacement. The frozen entrypoint runs on Linux, +macOS, and native Windows without Bash. + +For Codex, the effective user configuration is +`$CODEX_HOME/config.toml` when `CODEX_HOME` is set, otherwise +`$HOME/.codex/config.toml` on Unix or `%USERPROFILE%\.codex\config.toml` on +native Windows. Pass that exact path with `--config`. A checkout's +`.codex/config.toml` is not a substitute for an isolated eval process's +`CODEX_HOME/config.toml`. Pass `--context isolated-eval` when the session or +runner establishes an isolated evaluation home; otherwise pass +`--context effective`. Establish V1, V2, or unknown from current host evidence +and pass `--backend`; do not infer V2 merely from the presence of `max_depth`. + +For Claude Code, the helper reads only the two named environment controls from +the current process. Supply `--version` when the installed version is already +known; otherwise let the helper inspect `claude --version`. Its source is the +effective process environment, including values injected from settings by the +host. Do not read or display unrelated environment variables or settings. + +If the user provides a configuration path but the path is missing, confirm +whether absence is what they want diagnosed. If no host or usable effective +source can be established, ask one compact question and stop without invoking +the helper. + +## 2. Run one read-only diagnosis + +Run exactly one applicable command: + +```sh +uv run --quiet --frozen --no-dev --project "" \ + host-config-doctor codex \ + --config --backend \ + --context + +uv run --quiet --frozen --no-dev --project "" \ + host-config-doctor claude \ + [--version ] +``` + +On PowerShell, put the selected command on one line and omit the trailing +backslashes while passing the same arguments. + +The helper reports only the named controls and derived capacity. Preserve an +unreadable or malformed refusal; do not fall back to a repository copy, parse +around it, or suggest that unknown capacity is adequate. + +## 3. Explain the conclusion + +Base the conclusion on this topology: + +```text +primary -> owner -> verification coordinator -> review coordinator + -> Standards reader + -> Spec reader +``` + +The baseline owner-only path needs one spawned-agent slot and one layer below +the primary. The full required-assessment path needs five concurrently open +spawned-agent slots and four nesting layers. Codex concurrency excludes the +primary. Keep concurrency and nesting separate: on Codex V2, +`agents.max_depth` is V1-only and ignored, so it cannot compensate for too few +concurrent threads. + +For Claude Code, report +`CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS` and +`CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH` separately and honor the helper's +installed-version applicability. Do not claim an older version supports a +control merely because the variable is present. + +Return the exact checked source, host/version/backend evidence, baseline and +full-path result, and the helper's actionable source-specific guidance. Copy +the helper's `concurrency_control` and `nesting_control` names verbatim into the +user-visible result so a numeric conclusion cannot hide which setting was +evaluated. The +diagnosis is complete only when those facts are present, no configuration was +changed, no credentials or unrelated values were emitted, and no owner was +launched. + +## Boundaries + +- This capability is read-only and never edits configuration. +- Doctor intent does not activate `adaptive-delivery`, launch an owner, or run + delivery preflight. +- An absent file, unset control, unknown default, backend, or version is not + evidence of adequate capacity. +- Never print complete configuration files, settings objects, environment + dumps, tokens, or credentials. diff --git a/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/agents/openai.yaml b/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/agents/openai.yaml new file mode 100644 index 00000000..a2c85e1b --- /dev/null +++ b/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Doctor Adaptive Delivery" + short_description: "Check host delegation capacity" + default_prompt: "Use $doctor-adaptive-delivery to diagnose this host's Adaptive Delivery configuration." diff --git a/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/evals/counterexample-depth.yaml b/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/evals/counterexample-depth.yaml new file mode 100644 index 00000000..d7bde4ef --- /dev/null +++ b/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/evals/counterexample-depth.yaml @@ -0,0 +1,23 @@ +id: doctor-adaptive-delivery-counterexample-depth +invariant: ADL-D1 +activation: positive +prompt: >- + {{skill_invocation}} Codex V2 has agents.max_depth = 20 but only + agents.max_concurrent_threads_per_session = 3. Is the full Adaptive Delivery + verification path supported? Diagnose only; do not edit anything. +fixture: + commits: + - message: "chore: init" + files: + .codex/config.toml: | + [agents] + enabled = true + max_concurrent_threads_per_session = 3 + max_depth = 20 +checks: [] +semantic_output_checks: + - name: depth does not compensate for concurrency + proposition: The response concludes that the full path is unsupported because three spawned-agent slots are below the required five, and explicitly says the large max_depth value is V1-only and ignored by Codex V2. +transcript_checks: + - name: orchestration does not activate + not_regex: '"skill":"adaptive-delivery"|adaptive-delivery-preflight|"tool":"spawn_agent"' diff --git a/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/evals/direct-codex.yaml b/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/evals/direct-codex.yaml new file mode 100644 index 00000000..63af2c20 --- /dev/null +++ b/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/evals/direct-codex.yaml @@ -0,0 +1,23 @@ +id: doctor-adaptive-delivery-direct-codex +invariant: ADL-D1 +activation: positive +prompt: >- + {{skill_invocation}} Check whether this isolated Codex environment is + configured for Adaptive Delivery's full verification and review path. +fixture: + commits: + - message: "chore: init" + files: + .codex/config.toml: "[agents]\nmax_concurrent_threads_per_session = 99\n" + README.md: "fixture\n" +checks: + - name: diagnosis is read-only + run: test -z "$(git status --porcelain)" +semantic_output_checks: + - name: effective isolated config is diagnosed + proposition: The response identifies the absolute isolated CODEX_HOME config.toml as the checked source, does not treat the checkout .codex/config.toml containing 99 as effective, and reports the effective configured capacity instead of the checkout value. + - name: concurrency and nesting stay distinct + proposition: The response explains that max_concurrent_threads_per_session is the relevant V2 concurrency control and that agents.max_depth is a separate V1-only control ignored by V2. +transcript_checks: + - name: orchestration does not activate + not_regex: '"skill":"adaptive-delivery"|adaptive-delivery-preflight|"tool":"spawn_agent"|"name":"Agent"' diff --git a/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/evals/incomplete-host.yaml b/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/evals/incomplete-host.yaml new file mode 100644 index 00000000..ba012114 --- /dev/null +++ b/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/evals/incomplete-host.yaml @@ -0,0 +1,19 @@ +id: doctor-adaptive-delivery-incomplete-host +invariant: ADL-D1 +activation: positive +prompt: >- + Diagnose whether another machine's Claude Code configuration supports + Adaptive Delivery. I meant to include its installed version and effective + environment, but did not attach them. +fixture: + commits: + - message: "chore: init" + files: + README.md: "fixture\n" +checks: [] +semantic_output_checks: + - name: host ambiguity is requested + proposition: The response asks for the missing Claude Code version and effective values or diagnostic source from the other machine, and does not claim a capacity result. +transcript_checks: + - name: no diagnostic or orchestration starts before host choice + not_regex: 'host-config-doctor|adaptive-delivery-preflight|"tool":"spawn_agent"|"name":"Agent"' diff --git a/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/evals/indirect-claude.yaml b/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/evals/indirect-claude.yaml new file mode 100644 index 00000000..fd695e72 --- /dev/null +++ b/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/evals/indirect-claude.yaml @@ -0,0 +1,21 @@ +id: doctor-adaptive-delivery-indirect-claude +invariant: ADL-D1 +activation: positive +prompt: >- + Before I use Darrow's delivery workflow, tell me whether this Claude Code + session has enough parallel and nested subagent capacity for its full review + topology. Do not change configuration. +fixture: + commits: + - message: "chore: init" + files: + README.md: "fixture\n" +checks: + - name: diagnosis is read-only + run: test -z "$(git status --porcelain)" +semantic_output_checks: + - name: controls are distinguished + proposition: The response identifies the effective Claude Code process environment and installed version, treats CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS and CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH as distinct controls only where the version supports them, and gives non-mutating actionable guidance. +transcript_checks: + - name: orchestration does not activate + not_regex: '"skill":"adaptive-delivery"|adaptive-delivery-preflight|"tool":"spawn_agent"|"name":"Agent"' diff --git a/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/evals/nonactivation-delivery.yaml b/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/evals/nonactivation-delivery.yaml new file mode 100644 index 00000000..7685894f --- /dev/null +++ b/plugins/orchestration/darrow-adaptive-delivery/skills/doctor-adaptive-delivery/evals/nonactivation-delivery.yaml @@ -0,0 +1,14 @@ +id: doctor-adaptive-delivery-nonactivation-delivery +invariant: ADL-P1,ADL-D1 +activation: negative +mount_plugin_skills: true +prompt: Use adaptive-delivery to add a health endpoint and tests. Keep it local. +fixture: + commits: + - message: "chore: init" + files: + README.md: "fixture\n" +checks: [] +transcript_checks: + - name: doctor is not selected for delivery intent + not_regex: '"skill":"doctor-adaptive-delivery"|host-config-doctor'