Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ execution remains an explicit handoff or provider-specific transport. See
## Optional Organizational Context

The base installation works without an organizational-memory provider. The
optional Coworker integration gives approved Claude and Codex roles the same
optional Coworker integration gives approved Claude, Codex, and Devin roles the same
bounded, cited evidence through an explicitly selected private account. Account
identity and credentials stay outside the repository.

Expand Down
80 changes: 73 additions & 7 deletions docs/context-delivery.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Share optional evidence with Claude and Codex
# Share optional evidence with Claude, Codex, and Devin

An approved context packet can now accompany a work order and its independent
review. Both hosts use the same evidence renderer and authorization checks.
review. Every host uses the same evidence renderer and authorization checks.
The calling host remains the orchestrator; a context provider gains no builder,
reviewer, tracker-write, or merge authority. Ordinary sessions without context
keep their existing behavior and do not load the optional Coworker SDK.
Expand All @@ -10,9 +10,10 @@ Start with a verified [private connection](context-connections.md). The guided
path below fetches once for the work item and retains the opaque packet handle;
the lower-level workflow can still fetch it explicitly. Every delivery verifies
authorization online; it does not repeat the search.
Only Claude and Codex orchestrator, builder, and reviewer roles are supported
for private delivery in this release. Each role must be explicitly approved in
the private connection configuration.
Only Claude, Codex, and Devin orchestrator, builder, and reviewer roles are
supported for private delivery in this release. Each role must be explicitly
approved in the private connection configuration as `<host>:<role>`; a
connection without `devin:*` recipients never delivers to Devin.

## Guided session path

Expand Down Expand Up @@ -53,8 +54,8 @@ and diagnostics.
`session context deliver` derives the repository, work item, connection, policy,
packet, and selected builder from protected state. It writes the private
evidence to stdout for the builder's prompt; keep that output out of tracked
files and public logs. The selected builder must be Claude or Codex in this
release.
files and public logs. The selected builder must be Claude, Codex, or Devin in
this release.

## Work order and builder

Expand All @@ -74,6 +75,71 @@ Use the same scope and policy as the original fetch. Evidence is written to
stdout for the approved participant's prompt. Keep it out of public terminal
logs, tracked files, PR descriptions, and shared artifacts.

### Hosted Devin builder

A trusted hosted work order can carry the packet to `devin:builder` without a
local prompt file. The trusted context decision is part of the work order:
`WorkOrder.context_policy` is `none`, `optional`, or `required`, comes from
dispatcher policy, and is included in the durable binding. A context-bearing
order also carries the trusted `WorkOrder.context_work_item`: the tracker-neutral
identity from the session or manifest context binding (a Jira key, for example),
which may differ from the integer GitHub delivery issue `WorkOrder.issue`. Only
the integer issue closes and verifies the pull request; only the work item binds
the packet. When a key is supplied the prepared source may omit `issue_number`,
though a present one must still match. Context-free orders carry no key, and
their serialized binding is unchanged.

The embedding binds one packet with `devin_work_orders.packet_context(store,
name, handle, policy, order=order)`, which validates the handle and the trusted
policy (its `required` flag must agree with the order) and returns a
`PacketContext` holding only the protected store, connection name, packet
handle, normalized policy, and backend. It carries no packet, evidence, callable,
or identity of its own: the request is derived from the order it is used with.
That value is passed as `context=` to `dispatch`, `clarify`, or `fix`.

Immediately before each paid create or message write, and never in preview, the
work-order boundary itself calls `load_authorized()` with the order's repository,
work item, and `devin:builder`: a new online authorization under the store lock
for the selected account, current authorization, revocation and expiry, then the
packet's own binding, freshness, and recipients. The evidence is rendered inside
that boundary from the exact packet the authorized load returned, under the
exact authorized packet handle as its `Packet identity`, the same identity the
Claude and Codex peer paths render; no separately supplied packet or evidence is
ever accepted, and a synthetic local packet with matching binding fields never
reaches the store lookup. A handle for another ticket, a packet without the
recipient, or a store without an authorizable connection is `unavailable`; a
bare handle, packet, string, look-alike object, store subclass, or any context
on a `none` order fails as `context_binding_mismatch`. Rendering and the 64 KiB
combined-size check (`context_budget_exceeded`) happen before the work-order
record, branch reservation, or a new round is written, so a rejected input
leaves no undispatched reservation and consumes no round. Evidence text is
appended only to the provider input; records keep only digests plus the safe
state enum, and status, collect, and cancel reject a context argument.

A `required` order fails closed when no packet is supplied or it cannot be
reauthorized (wrong account, revoked or expired authorization, refreshed or
invalidated packet). `run` then returns, rather than raises, the issue's closed
outcome: `{"outcome": "UNKNOWN", "state": "paused", "reason":
"context_unavailable", "context": {"policy": "required", "dispatch" | "message":
"unavailable"}, "merge_authority": false}`; nothing was reserved, persisted, or
sent, and an already dispatched order keeps its prior round and state. Only an
`optional` order degrades to a code-only input (`degraded`) or runs without a
packet (`omitted`).

The complete intended input, including the evidence and its safe state, is
digested before the local dispatch or message intent becomes durable, and the
remote session's input fingerprint covers the same text. A retry after a stop
between the two writes must regenerate the identical input; changed, refreshed,
or dropped evidence fails as `request_conflict` and the saved state keeps
reporting what was actually intended. The state chosen for the create input and
for each message intent is persisted and reported by every command as
`context: {policy, dispatch, message}`; replay, acknowledgement, status, and
collect return the saved state rather than recomputing it. Durable records
written before the context field existed keep their original binding and
dispatch input: a `none` order serializes without `context_policy` or
`context_work_item`, and records without context or input digests are read as
context-free.

## Attach evidence to independent review

After creating the PR, attach the packet to its current code head without a
Expand Down
11 changes: 7 additions & 4 deletions docs/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ different participant; review lanes then exclude that builder. `--output`
accepts a repository-relative path only.

The command rechecks the session lease and trusted context policy, authorizes
the calling Claude or Codex host, fetches through the existing bounded provider
the calling Claude, Codex, or Devin host, fetches through the existing bounded provider
contract, drafts through the existing work-order contract, and saves only a
request hash plus opaque packet and work-order references in private session
state. Its output does not contain the work-item identity, query, connection,
Expand Down Expand Up @@ -182,7 +182,7 @@ Running `prepare` while publication is pending or uncertain preserves the saved
intent and directs the session back to `attach`; it never drops the revision or
repeats provider authorization while publication recovery is unresolved.

After a selected Claude or Codex reviewer finishes, give its private findings
After a selected Claude, Codex, or Devin reviewer finishes, give its private findings
to the selected builder without copying the revision:

```bash
Expand Down Expand Up @@ -280,7 +280,7 @@ readiness, and explicit gaps. The modes describe maintained Code Mower paths:
| Review | Local runner | Evidence only |
| Message | Unavailable | Unavailable |
| Cancel | Unavailable | Unavailable |
| Authorized context delivery | Unavailable | Unavailable |
| Authorized context delivery | Unavailable | Agent handoff (authorized packet in hosted builder input) |
| Structured results | Local runner | Release campaign only |

These declarations describe the current integration; they do not launch a
Expand All @@ -298,7 +298,10 @@ product/transport declaration fails with instructions to set
calibrated repository promotion requires explicit product and transport fields;
selection never performs that promotion. Contradictory driver/transport pairs or
capability overrides fail validation; remove `capabilities` to use maintained
defaults. Keep `provider: devin_cli` for local execution and `provider: devin`
defaults. The one exception is the exact earlier maintained hosted declaration
(`devin_api_v3` with `context: unavailable`), which earlier templates wrote:
it is read as the current declaration in memory, again without file writes, and
any other deviation still fails. Keep `provider: devin_cli` for local execution and `provider: devin`
for hosted compatibility, with `product: devin` in both cases.

Devin Cloud needs its own execution setup. Cursor's agent
Expand Down
10 changes: 5 additions & 5 deletions src/code_mower/context_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from . import context_review
from .claude_audit_pr import _decision_authorities_for_repo
from .context_contract import ContextError, ContextRequest, _object, normalize_policy
from .context_delivery import attach, deliver, read_binding, render_evidence
from .context_delivery import SUPPORTED_HOSTS, SUPPORTED_RECIPIENTS, attach, deliver, read_binding, render_evidence
from .context_packets import load_authorized
from .context_store import ContextStore, strict_json
from .provider_runners import fetch_issue_comments, fetch_pull_request, post_pr_comment
Expand All @@ -39,7 +39,7 @@ def main(argv=None):
attach_parser.add_argument("--connection", required=True)
attach_parser.add_argument("--unavailable", action="store_true", help="Explicitly declare unavailable context; optional work may continue with a fresh code-only review")
attach_parser.add_argument("--request-stdin", action="store_true", required=True)
attach_parser.add_argument("--host", choices=("claude", "codex"), default=os.environ.get("CODE_MOWER_HOST"))
attach_parser.add_argument("--host", choices=SUPPORTED_HOSTS, default=os.environ.get("CODE_MOWER_HOST"))
for verb in ("deliver", "feedback"):
command = sub.add_parser(verb, help="Output private evidence or findings only after authorization")
command.add_argument("--revision", help="Previously attached PR input revision")
Expand All @@ -48,7 +48,7 @@ def main(argv=None):
command.add_argument("--request-stdin", action="store_true")
command.add_argument("--recipient", required=True, help="Approved host:role, such as codex:builder")
if verb == "feedback":
command.add_argument("--reviewer", choices=("claude", "codex"), required=True)
command.add_argument("--reviewer", choices=SUPPORTED_HOSTS, required=True)
for command in (attach_parser, *[sub.choices[name] for name in ("deliver", "feedback")]):
command.add_argument("--state-dir", type=Path)
command.add_argument("--repo-path", type=Path, default=Path.cwd(), help="Target repository checkout for trusted base configuration")
Expand All @@ -60,7 +60,7 @@ def main(argv=None):
if args.revision or not args.connection or not args.request_stdin:
raise ContextError("packet delivery requires a connection and private request on stdin")
spec = _object(_private_spec(), {"repository", "work_item", "policy"})
if args.recipient not in ("claude:orchestrator", "codex:orchestrator", "claude:builder", "codex:builder"):
if args.recipient not in SUPPORTED_RECIPIENTS or args.recipient.endswith(":reviewer"):
raise ContextError("independent reviewers consume an attached review revision")
packet = load_authorized(store, args.connection, args.packet, spec["policy"],
ContextRequest(spec["repository"], spec["work_item"], args.recipient))
Expand All @@ -76,7 +76,7 @@ def main(argv=None):
policy = normalize_policy(spec['policy'])
if policy is None or policy['connection'] != args.connection:
raise ContextError('select the connection named by the work-item policy')
if args.host not in ("claude", "codex"):
if args.host not in SUPPORTED_HOSTS:
raise ContextError("supply the calling host when attaching context")
if type(spec["pr"]) is not int or spec["pr"] < 1:
raise ContextError("context attachment requires a PR number")
Expand Down
14 changes: 9 additions & 5 deletions src/code_mower/context_delivery.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@

SCHEMA = "code_mower.contextDelivery.v1"
MAX_DELIVERY_BYTES = 80_000
SUPPORTED_RECIPIENTS = frozenset(f"{host}:{role}" for host in ("claude", "codex")
for role in ("orchestrator", "builder", "reviewer"))
SUPPORTED_HOSTS = ("claude", "codex", "devin")
SUPPORTED_ROLES = ("orchestrator", "builder", "reviewer")
SUPPORTED_RECIPIENTS = frozenset(f"{host}:{role}" for host in SUPPORTED_HOSTS for role in SUPPORTED_ROLES)
SUPPORTED_PROVIDERS = ("Claude", "Codex", "Devin")


def render_evidence(packet: ValidatedPacket, revision: str) -> str:
Expand Down Expand Up @@ -59,7 +61,7 @@ def _binding(value):
if metadata["revision"] != value["revision"] or metadata["required"] != policy["required"]:
raise ContextError("context delivery metadata does not match its binding")
feedback = value["feedback"]
if (not isinstance(feedback, dict) or feedback.keys() - {"claude", "codex"}
if (not isinstance(feedback, dict) or feedback.keys() - set(SUPPORTED_HOSTS)
or any(not isinstance(v, str) or len(v.encode("utf-8")) > 100_000 for v in feedback.values())):
raise ContextError("context feedback exceeds its private storage budget")
return dict(value)
Expand Down Expand Up @@ -237,7 +239,7 @@ def deliver(store, revision, *, repository, pr, head, recipient, current, backen

def save_feedback(store, delivery: Delivery, host, prose):
"""Only credential-free verdict prose is retained, under connection cleanup."""
if host not in ("claude", "codex") or not isinstance(prose, str) or len(prose.encode()) > 100_000:
if host not in SUPPORTED_HOSTS or not isinstance(prose, str) or len(prose.encode()) > 100_000:
raise ContextError("private review feedback exceeds its supported budget")
with store.locked(delivery.binding["connection"]) as locked:
artifact = locked.artifact("d-" + delivery.metadata["revision"])
Expand All @@ -251,10 +253,12 @@ def save_feedback(store, delivery: Delivery, host, prose):
def public_verdict(delivery, *, provider, head, verdict, counts, trailer,
actions_run_id=None, merge_authority=True):
"""Never publish model-authored prose when private context was supplied."""
if provider not in ("Claude", "Codex") or verdict not in ("PASS", "BLOCKED", "UNKNOWN", "STALE"):
if provider not in SUPPORTED_PROVIDERS or verdict not in ("PASS", "BLOCKED", "UNKNOWN", "STALE"):
raise ContextError("unsupported context review metadata")
if len(counts) != 4 or any(type(value) is not int or not 0 <= value <= 1000 for value in counts):
raise ContextError("invalid context review counts")
if provider == "Devin":
merge_authority = False # Devin review is informational in every lane.
metadata = delivery.metadata if isinstance(delivery, Delivery) else delivery
from .provider_runners.comments import format_audit_comment_header
header = format_audit_comment_header(provider_name=provider, head_sha=head,
Expand Down
11 changes: 6 additions & 5 deletions src/code_mower/context_guided.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .claude_audit_pr import _decision_authorities_for_repo
from .context_contract import ContextError, ContextRequest
from .context_delivery import (
SUPPORTED_HOSTS,
SUPPORTED_RECIPIENTS,
abandon_attachment,
deliver,
Expand Down Expand Up @@ -197,8 +198,8 @@ def attach_session(
raise ContextError("prepare the selected context before attaching it")
if record["stage"] not in {"prepared", "attached", "reviewed"}:
raise ContextError("prepare the selected context before attaching it")
if record["host"] not in {"claude", "codex"}:
raise ContextError("guided private context currently supports Claude and Codex hosts")
if record["host"] not in SUPPORTED_HOSTS:
raise ContextError("guided private context currently supports Claude, Codex, and Devin hosts")

token, authorities = _github_access(repo_path, base_ref)
with association_store.locked(_workflow_key(record)):
Expand Down Expand Up @@ -344,7 +345,7 @@ def _builder_recipient(record: Mapping[str, Any]) -> str:
recipient = builder + ":builder"
if recipient not in SUPPORTED_RECIPIENTS:
raise ContextError(
"the selected builder cannot consume private context in this release; choose Claude or Codex"
"the selected builder cannot consume private context in this release; choose Claude, Codex, or Devin"
)
return recipient

Expand Down Expand Up @@ -416,12 +417,12 @@ def feedback_session(
record = context_session.validate(record)
reviewer = participant_id(reviewer)
if (
reviewer not in {"claude", "codex"}
reviewer not in SUPPORTED_HOSTS
or reviewer not in record["participants"]
or reviewer == record["builder"]
or PARTICIPANTS[reviewer].review_lane is None
):
raise ContextError("--reviewer must name a selected independent Claude or Codex reviewer")
raise ContextError("--reviewer must name a selected independent Claude, Codex, or Devin reviewer")
if record["attachment_state"] != "published":
raise ContextError("attach context and complete the independent review before reading feedback")
recipient = _builder_recipient(record)
Expand Down
5 changes: 3 additions & 2 deletions src/code_mower/context_prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from . import context_packets, context_session, work_orders
from .context_contract import ContextError, ContextRequest, _text
from .context_delivery import SUPPORTED_HOSTS
from .context_store import ContextStore
from .participants import PARTICIPANTS, participant_id

Expand Down Expand Up @@ -202,8 +203,8 @@ def prepare(
dependent_work="usable",
next_action="Continue the ordinary workflow or configure an optional context connection.",
), 0
if record["host"] not in {"claude", "codex"}:
raise ContextError("guided private context currently supports Claude and Codex hosts")
if record["host"] not in SUPPORTED_HOSTS:
raise ContextError("guided private context currently supports Claude, Codex, and Devin hosts")
selected_builder = participant_id(builder or record["builder"] or record["host"])
if (
selected_builder not in record["participants"]
Expand Down
Loading
Loading