Files
spec-kit/src/specify_cli/integrations/devin/__init__.py
T
kanfilandGitHub 99970560db feat(events): context injection for opencode and JSON-envelope agent hooks (#3934)
* feat(events): context injection for opencode and JSON-envelope agent hooks

Adds first-class context injection to agent runtime events:

1. opencode: maps session_start to experimental.chat.system.transform (injects into system prompt) and user_prompt_submit to chat.message (injects synthetic TextPart). TS plugin captures runEvent stdout (stdio pipe, encoding utf-8) and pushes into output objects. Part IDs derive from output.parts[last].id to preserve OpenCode's prt_ brand and prevent session schema crashes.

2. JSON-envelope hook wrapping: adds events_context_envelope to IntegrationBase so agents that require JSON on stdout receive their target envelope via the dispatcher's 5th argument:
   - gemini, tabnine, qwen, devin: hookSpecificOutput.additionalContext on session_start/user_prompt_submit; suppress on non-injectable events (prevents systemMessage user-facing noise)
   - copilot: top-level additionalContext on session_start
   - cursor: top-level additional_context on session_start; suppress elsewhere
   - claude, codex: plain stdout passthrough (already injected)

3. Dispatcher template and resolve_and_run_event_command parse the 5th envelope arg and wrap stdout accordingly.

Tests added for opencode TextPart schema, part ID derivation, envelope command generation, and dispatcher output wrapping. All 162 events/integration tests pass.

* fix(events): address code review on #3934

- Qwen/Gemini/Tabnine/Devin: include native hookEventName inside
  hookSpecificOutput envelope (required by Qwen's hooks spec). Thread
  the native event name from the integration's CANONICAL_TO_NATIVE
  through _dispatcher_command as a 6th dispatcher argument, through
  the dispatcher template's main()/_run_inline()/_emit(), and through
  resolve_and_run_event_command()/_emit_event_stdout().
- Copilot: map user_prompt_submit to additionalContext (previously
  unmapped, breaking per-prompt context injection despite Copilot CLI
  supporting it via userPromptSubmitted).
- OpenCode: guard experimental.chat.system.transform so canonical
  session_start handlers only run when input.sessionID is present —
  OpenCode fires this hook for non-session operations (e.g. agent
  generation) with no sessionID.

Assisted-by: opencode (model: glm-5.2, supervised)

* fix(events): address second Copilot review round on #3934

- Positional arg alignment: always emit default timeout (60s) as the
  4th dispatcher argument even when timeout_seconds is omitted, so the
  envelope (5th) and native_event (6th) land in the correct argv slots.
  Previously, omitting timeout_seconds caused the envelope to be parsed
  as an invalid timeout, silently falling back to plain stdout.
- OpenCode session_start caching: cache handler output per sessionID in
  the generated TS plugin so non-idempotent handlers (setup, telemetry,
  file-mutating scripts) run once per session instead of on every LLM
  request. Cache is evicted on session.deleted.
- Updated PR description to reflect Copilot user_prompt_submit now maps
  to additionalContext (was documented as plain/unprocessed).

Assisted-by: opencode (model: glm-5.2, supervised)
2026-08-04 07:32:30 -05:00

92 lines
3.1 KiB
Python

"""Devin for Terminal integration — skills-based agent.
Devin uses the ``.devin/skills/speckit-<name>/SKILL.md`` layout and
reads project context from ``AGENTS.md`` at the repo root. The CLI
binary is ``devin`` and skills are invoked via ``/<name>`` inside an
interactive ``devin`` session.
See: https://cli.devin.ai/docs/extensibility/skills/overview
"""
from __future__ import annotations
from ..base import IntegrationOption, SkillsIntegration
class DevinIntegration(SkillsIntegration):
"""Integration for Cognition AI's Devin for Terminal."""
key = "devin"
config = {
"name": "Devin for Terminal",
"folder": ".devin/",
"commands_subdir": "skills",
"install_url": "https://cli.devin.ai/docs",
"requires_cli": True,
}
registrar_config = {
"dir": ".devin/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": "/SKILL.md",
}
CANONICAL_TO_NATIVE = {
"session_start": "SessionStart",
"pre_tool_use": "PreToolUse",
"post_tool_use": "PostToolUse",
"session_end": "SessionEnd",
"user_prompt_submit": "UserPromptSubmit",
"stop": "Stop",
}
events_config_file = ".devin/hooks.v1.json"
# Devin's hooks.v1.json is a root event map ({"PreToolUse": [...]}) with no
# top-level "hooks" wrapper (U2), unlike the settings.json formats. The
# json-root-nested writer/remover operate directly on the root event keys.
events_format = "json-root-nested"
# Devin's hooks protocol is JSON-stdout; additionalContext is the
# documented injection field for SessionStart/UserPromptSubmit (C13).
events_context_envelope = {
"*": "suppress",
"session_start": "hookSpecificOutput",
"user_prompt_submit": "hookSpecificOutput",
}
def build_exec_args(
self,
prompt: str,
*,
model: str | None = None,
output_json: bool = True,
) -> list[str] | None:
"""Build non-interactive CLI args for Devin for Terminal.
Devin supports ``devin -p <prompt>`` for single-turn execution
and ``--model`` for model selection, but its CLI has no flag
for structured JSON output. When ``output_json`` is requested,
Devin is still dispatched normally and returns plain-text
stdout instead of structured JSON. ``requires_cli=True`` is
kept on the integration for tool detection.
"""
args = [self._resolve_executable(), "-p", prompt]
self._apply_extra_args_env_var(args)
if model:
args.extend(["--model", model])
return args
@classmethod
def options(cls) -> list[IntegrationOption]:
# Compose with super() so the base class declares --events for this
# event-capable integration; otherwise --integration-options
# "--events false" is rejected as unknown (#8).
opts = super().options()
opts.append(
IntegrationOption(
"--skills",
is_flag=True,
default=True,
help="Install as agent skills (default for Devin)",
),
)
return opts