Skip to content
Open
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
74 changes: 72 additions & 2 deletions design/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,76 @@ Hyphenated keys use underscores in package names. Its class declares:
Import and `_register()` the class in `src/specify_cli/integrations/__init__.py`
(both lists alphabetically). This registry is the source of built-in Python
integration behavior. If the agent supports non-interactive workflows, implement
`build_exec_args()` with the full base signature; `options()` declares
install-time `--integration-options`, not per-workflow runtime options.
`build_exec_args()` with the full base signature. The `reasoning_effort` and
`context_size` keywords are required only for the settings it translates; accept
an untranslated one only to reject it through `check_dispatch_settings()` (see
[Per-step dispatch settings](#per-step-dispatch-settings)). `options()`
declares install-time `--integration-options`, not per-workflow runtime options.
Agent-specific native events can be declared on the integration. Set
`multi_install_safe = True` only when the integration emits no files or uses a
static, non-overlapping agent root and command directory; shared dynamic paths
are not safe by default.

### Per-step dispatch settings

Workflow `command` and `prompt` steps can request the settings in
`DISPATCH_SETTINGS` (`reasoning_effort`, then `context_size`) for one CLI
invocation. Values are opaque strings whose meaning belongs to the agent's CLI.
Prompt steps pass requested settings to `build_exec_args()`. Command steps pass
them to `dispatch_command()`, whose base implementation forwards only the
requested keywords to `build_exec_args()`. The *receiving method* is therefore
`build_exec_args()`, or `dispatch_command()` for a command step when the
integration overrides it.

Built-in integrations and external adapters opt in per setting with these
`specify_cli.integrations.base` APIs:

- `supported_dispatch_settings`: a `frozenset` of the settings the integration
translates. Declare it on the class that implements the receiving method.
`effective_dispatch_settings()` reads the declaration from that class only:
a subclass that inherits the method inherits support, but a subclass that
overrides it must declare support again. A declaration on any other class,
or one that is not a set or frozenset, declares nothing.
- `reasoning_effort` and `context_size` keywords: the receiving method accepts
one, defaulting to `None`, for each declared setting. Hosts pass a keyword
only when a step sets that setting, so calls without settings are unchanged.
Treat any value other than `None` as requested; hosts never pass `""`.
Translate the value into CLI arguments placed after those from
`SPECKIT_INTEGRATION_<KEY>_EXTRA_ARGS`, and do not pass it on:
`IntegrationBase.build_exec_args()` and the format-base builders translate
neither setting and raise `DispatchSettingsError` rather than drop a value.
- `validate_dispatch_setting(name, value)`: an optional per-setting hook that
the gate calls before anything runs. The default accepts every value; raise
`ValueError` to reject a value the CLI would ignore or misread, and the host
attributes the failure to that setting. If the receiving method itself must
reject a setting, raise `DispatchSettingsError` with `setting`; without it,
the step error lists every requested setting with its source. Keep the value
out of either error's message: it can come from captured output, which Spec
Kit does not print to the terminal or write to `log.jsonl`.
- `dispatch_setting_hint(setting)`: optional remediation text, such as another
step field to use instead, which the host appends to its error for an
unsupported setting. The default returns `None`.

The host gate, `check_dispatch_settings(integration, settings, *, via)`, cannot
be overridden by integrations. Steps run it before executable detection, with
*via* set to `"dispatch_command"` for command steps and `"build_exec_args"` for
prompt steps; the base `dispatch_command()` runs it with `"build_exec_args"`.
For each requested setting, in `DISPATCH_SETTINGS` order, the gate requires a
string of 1 to 64 letters, digits, `.`, `_`, or `-` that starts with a letter
or digit, support under the ownership rule above, and a receiving method that
accepts the keyword or `**kwargs`. It then calls `validate_dispatch_setting()`.
Each failure raises `DispatchSettingsError`, a `ValueError` whose `setting`
attribute names the setting, so the step can report where its value came from.
An unsupported-setting error lists the integrations that support the setting,
and an error about a malformed value reports only its type and length, never
the value. A translating method can also run the gate itself, with *via*
naming that method, so that direct calls are checked too.

The host cannot observe what the receiving method does with a declared
setting. An integration that declares support and then ignores the value, for
example by absorbing it in `**kwargs`, drops it silently: a declaration is a
promise to translate the setting.

## Output flavors

Choose the smallest base class that matches the agent's native format. The
Expand Down Expand Up @@ -182,6 +245,13 @@ output_json=True, integration_args=None, integration_options=None,
project_root=None)` as appropriate. Every explicitly declared host keyword
parameter must have a default; accepting omitted keywords through `**kwargs`
is also supported. Validation checks both prompt-only and complete host calls.
This required signature does not change for
[per-step dispatch settings](#per-step-dispatch-settings), and installation
does not check dispatch-setting declarations. An adapter that does not declare
support never receives `reasoning_effort` or `context_size`: a step that
requests either fails before the adapter's CLI runs. Adapters opt in like
built-in integrations, and the host gate checks the declaration and the
receiving method's signature when a step requests a setting.
An adapter using only the host API and
standard library needs no pip installation or source-registry edit.
Import-side-effect registration is rejected.
Expand Down
46 changes: 46 additions & 0 deletions docs/integrations/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,52 @@ CLI adapter declares none, tool checking probes its runtime executable.
Version detection is agent-specific; declaring a version constraint does not
supply a universal `--version` invocation.

### Per-step dispatch settings

Workflow `command` and `prompt` steps can request
[`reasoning_effort` and `context_size`](../workflow-steps/types.md#reasoning-effort-and-context-size)
for one CLI invocation. `DISPATCH_SETTINGS` in `specify_cli.integrations.base`
lists them in the order they are checked. Values are opaque strings whose
meaning belongs to the agent's CLI.

The `build_exec_args()` signature above does not change, and an adapter that
does not opt in never receives either keyword: a step that requests one fails
before the CLI runs. Prompt steps pass requested settings to
`build_exec_args()`. Command steps pass them to `dispatch_command()`, whose
base implementation forwards them to `build_exec_args()`. The *receiving
method* is therefore `build_exec_args()`, or `dispatch_command()` for a command
step when the integration overrides it. To opt in:

- Declare `supported_dispatch_settings`, a `frozenset` of the settings the
integration translates, on the class that defines the receiving method. A
subclass that overrides that method must declare support again. A
declaration on any other class, or one that is not a set or frozenset,
declares nothing.
- Accept a `reasoning_effort=None` or `context_size=None` keyword on the
receiving method for each declared setting. The host passes a keyword only
when a step sets that setting, and never passes `""`. Translate the value
into CLI arguments placed after those from
`SPECKIT_INTEGRATION_<KEY>_EXTRA_ARGS`, and do not pass it on:
`IntegrationBase.build_exec_args()` and the format-base builders translate
neither setting and raise `DispatchSettingsError` rather than drop a value.
- Optionally, override `validate_dispatch_setting(name, value)` to raise
`ValueError` for a value the CLI would ignore or misread, and
`dispatch_setting_hint(setting)` to return remediation text that the host
appends to its error for an unsupported setting.

Before the CLI runs, the host gate `check_dispatch_settings()` checks each
requested setting in `DISPATCH_SETTINGS` order. The value must be 1 to 64
letters, digits, `.`, `_`, or `-`, starting with a letter or digit; the
integration must declare the setting as above; and the receiving method must
accept the keyword or `**kwargs`. The gate then calls
`validate_dispatch_setting()`. Each failure raises `DispatchSettingsError`, a
`ValueError` whose `setting` attribute names the setting; if the receiving
method itself must reject a value, raise it with `setting`. Keep the value out
of every error message: it can come from captured output, which Spec Kit does
not print to the terminal or write to `log.jsonl`. The host cannot detect a
declared setting that the receiving method accepts and then ignores, for
example through `**kwargs`, so a declaration is a promise to translate it.

## Setup and file ownership

Format bases implement setup and teardown. Custom layouts may override:
Expand Down
7 changes: 7 additions & 0 deletions docs/integrations/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,13 @@ Docker Agent currently exposes structured agent/safety configuration.
Headless approval flags, model forwarding, and structured-output support are
also adapter-specific, not universal guarantees.

The [`reasoning_effort` and `context_size`](../workflow-steps/types.md#reasoning-effort-and-context-size)
settings of `command` and `prompt` steps instead follow one shared contract.
Spec Kit checks each value and passes it only to an integration that declares
and translates that setting; with any other integration, the step fails before
the CLI runs. Adapter authors opt in as described in the
[integration API reference](api.md#per-step-dispatch-settings).

## FAQ

### Can I install multiple integrations in the same project?
Expand Down
2 changes: 2 additions & 0 deletions docs/workflow-steps/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ invalid configuration in `execute()` too.
| `run_id` | `str \| None` | Current run ID |
| `is_resume` | `bool` | Whether this execution is a resume attempt |
| `workflow_dir` | `str \| None` | Source directory of the current workflow definition |
| `default_reasoning_effort` | `str \| None` | Workflow `reasoning_effort` default as written; can be an unevaluated expression |
| `default_context_size` | `str \| None` | Workflow `context_size` default as written; can be an unevaluated expression |

Mappings default to empty dictionaries, boolean flags to false, and optional
paths/identifiers to `None`. Paths are strings, not `Path` objects. Use
Expand Down
94 changes: 90 additions & 4 deletions docs/workflow-steps/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ Invoke an installed Spec Kit command by name via the integration CLI:
```

For positional arguments and named runtime options, see
[Per-step integration configuration](#per-step-integration-configuration).
[Per-step integration configuration](#per-step-integration-configuration). For
`reasoning_effort` and `context_size`, see
[Reasoning effort and context size](#reasoning-effort-and-context-size).

## Prompt Steps

Expand Down Expand Up @@ -195,6 +197,89 @@ or paused command is resumed, the complete dispatch configuration is re-resolved
from the current inputs, so values supplied with `workflow resume --input` take
effect consistently. A resume without updated inputs reproduces the same values.

A command step's `options` mapping, merged over `workflow.options`, is recorded
as `steps.<id>.output.options` but is not passed to the integration CLI. To
change the invocation, use `integration_args`, `integration_options`, `model`,
or [`reasoning_effort` and `context_size`](#reasoning-effort-and-context-size).

## Reasoning Effort and Context Size

`command` and `prompt` steps can set `reasoning_effort` and `context_size` for
the integration CLI they invoke. `workflow.reasoning_effort` and
`workflow.context_size` set defaults for both step types:

```yaml
workflow:
id: plan-and-review
name: Plan and review
reasoning_effort: "medium" # Default for command and prompt steps

inputs:
review_effort:
type: string
default: "" # Empty: the review step sends no effort

steps:
- id: plan
command: speckit.plan
reasoning_effort: "high" # Overrides the workflow default
context_size: "200000" # Quoted: values are strings

- id: review
type: prompt
prompt: "Review the plan for gaps"
reasoning_effort: "{{ inputs.review_effort }}"
```

**Resolution.** Each setting resolves like `model`: the step's value, then the
workflow default, then unset. The step value and the default can both be
expressions. A step value that is missing, `null`, or `""` inherits the
default. An expression that resolves to `""` or null unsets the setting for
that step, without falling back to the default; this is how a step opts out of
a default. In the example, `review` sends no reasoning effort unless the run
supplies `--input review_effort=<value>`.

**Values.** Values are strings. Quote numbers and words that YAML reads as
booleans, such as `on`, `off`, `yes`, and `no`: write `context_size: "200000"`,
not `context_size: 200000`. An expression must resolve to a string too, so
read a number from an input declared with `type: string`, not `type: number`.
A value must be 1 to 64 letters, digits, `.`, `_`, or `-`, starting with a
letter or digit. Literal values are checked when the workflow is validated, and
expression results when the step runs. An error about a malformed value reports
only its type and length, not its text. Spec Kit passes the value to the
integration unchanged. Its meaning and accepted values are defined by the
integration's CLI, and an integration can reject a value before the CLI runs.

**Support.** An integration translates a setting only if it declares support
for it. Otherwise the step fails before the CLI runs, with an error that names
the setting, its source (the step or the workflow default), and any
integrations that support it. Workflow defaults are not exempt: a default fails
each inheriting step whose integration does not support it. No built-in
integration supports these settings yet; see
[Workflow CLI dispatch](../integrations/cli.md#workflow-cli-dispatch) for how
integrations opt in.

**CLI rejections.** When the CLI rejects a value and exits with a non-zero code,
the step fails like any other CLI failure. The step error reports the exit
code, and the CLI's message appears on the terminal, or in `output.stderr` with
[`capture: true`](#capturing-command-and-prompt-output).

**Recording and resume.** The resolved values are recorded in
`steps.<id>.output.reasoning_effort` and `steps.<id>.output.context_size`, which
are `null` when unset. When `specify workflow resume` re-executes a step, it
resolves both again from the current inputs, so values supplied with `--input`
take effect.

**Extra arguments.** An integration that supports these settings adds their
flags after any arguments from `SPECKIT_INTEGRATION_<KEY>_EXTRA_ARGS`. If both
set the same CLI flag, the CLI decides which value wins, so set each flag in
only one place.

**Compatibility.** Spec Kit versions without this feature ignore these fields,
including the workflow defaults, and run steps without them.
`requires.speckit_version` does not prevent this: it is advisory and is not
enforced.

## Shell step

`type: shell` executes `run` through the platform shell, retaining support for
Expand Down Expand Up @@ -628,9 +713,10 @@ preserved across workflow boundaries.

## Defaults and Errors at the Call Boundary

Parent `integration`, `model`, and `options` defaults apply only to steps in
the parent workflow. A child uses its own defaults or automatic resolution. To
make a value common to both, declare it as a child input and map it explicitly.
Parent `integration`, `model`, `options`, `reasoning_effort`, and
`context_size` defaults apply only to steps in the parent workflow. A child
uses its own defaults or automatic resolution. To make a value common to both,
declare it as a child input and map it explicitly.
The call result intentionally has an empty `input` field and does not expose
the child's private inputs, step records, or logs.

Expand Down
3 changes: 2 additions & 1 deletion docs/workflows/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ This reference describes workflow YAML and execution/state semantics. The CLI's
| `workflow.version` | String in `X.Y.Z` form; omitted defaults to `0.0.0` |
| `workflow.author`, `workflow.description` | Descriptive metadata |
| `workflow.integration`, `workflow.model` | Optional string/null dispatch defaults |
| `workflow.options` | Optional mapping/null of dispatch options |
| `workflow.reasoning_effort`, `workflow.context_size` | Optional string/null dispatch defaults for `command` and `prompt` steps; literals must follow the [value rules](../workflow-steps/types.md#reasoning-effort-and-context-size) |
| `workflow.options` | Optional mapping/null of default command-step `options`; recorded in step output, not passed to the integration CLI |
| `requires` | Optional mapping accepting `speckit_version` and `integrations` only |
| `inputs` | Mapping of input definitions |
| `outputs` | Mapping of declared workflow-call outputs |
Expand Down
Loading
Loading