Add verification and project-local TypeScript steps to task delivery, and customize the pipeline per repository or across a fleet.
Customizing the Task Pipeline
Task delivery runs a configurable pipeline. Out of the box it is:
implement → commit → auto-review → finalize
implementruns the coding agent and writes an implementation summary.commitstages and commits the work (with agent repair of failing git hooks).auto-reviewcritiques the committed diff locally and applies fixes (only when--auto-reviewis set).finalizepushes the branch, posts the implementation summary to the task, and opens the pull request (only when--create-pris set).
Branch preparation, the feasibility check, and the In Progress transition still
happen before this pipeline. You customize delivery by adding a pipeline
section to .devintern-code/settings.json, or by pointing a workspace at a
standalone pipeline file.
Project-local only. The current release customizes the pipeline inside a repository (
.devintern-code/settings.json) or through a workspacepipeline_file. There are no project-wide workspacepipelinesettings beyond those files.
Where a task gets its pipeline
A task subprocess can see up to four layers, and exactly one complete definition runs — ordered step arrays are never merged across scopes:
- Workspace per-repository override —
pipeline_fileon a[[repos]]entry inworkspace.toml(highest). - Repository settings — the
pipelineblock in the repository’s committed.devintern-code/settings.json. - Workspace default —
pipeline_fileon[defaults]inworkspace.toml. - Built-in default —
implement → commit → auto-review → finalize.
A referenced workspace file that is missing or invalid fails the run, naming the
repository and the file; it never silently falls back. See
Workspaces (Multi-Repo Fleet) → Pipelines for the fleet and per repository
for the workspace.toml reference and live-reload behavior.
Required ordering
pipeline.steps is validated before the agent starts:
claritymay appear once, first, beforeimplement.implementmust appear once, before every delivery step.commitmust appear once.verifyandauto-reviewmust followcommit.finalizemust be last; nothing may follow it.- Custom plugin steps may run after
implementand beforefinalize— including beforecommit.
The clarity marker only reserves its position. It does not control whether
the feasibility preamble runs: --skip-clarity-check still decides that, exactly
as before. If you omit clarity from the list, the preamble still runs unless
the flag is passed.
Built-in steps and CLI flags
These flags keep their existing, operator-controlled meaning:
| Flag | Effect on the pipeline |
|---|---|
--skip-clarity-check | Skips the pre-implementation feasibility preamble (also affected by the clarity marker position). |
--auto-review | Lets the auto-review step run its review–fix loop. Without it, that step is a no-op. |
--create-pr | Lets finalize push and open the PR. Without it, finalize only posts a summary comment. |
--auto-review-iterations N | Caps the auto-review loop (default from AUTO_REVIEW_ITERATIONS, else 2). |
Pre-push hook validation still runs before publication whether or not
auto-review is present: the auto-review step validates it, and finalize
validates it if no earlier step did.
Verification is enabled by adding { "use": "verify" } to the pipeline; there is
no standalone --verify flag in the current CLI. verify can be declared more
than once with different prompts (a functional check and a security check, for
example).
Built-in verification (verify)
verify asks the configured agent whether the committed diff satisfies the
task’s functional requirements. It is agent-based semantic verification; for a
deterministic gate use a custom step that runs a command
instead.
{
"pipeline": {
"steps": [
{ "use": "implement" },
{ "use": "commit" },
{ "use": "verify", "prompt": "./prompts/verify-functional.md", "onFail": "loopback", "minSeverity": "high", "maxIterations": 3 },
{ "use": "verify", "prompt": "./prompts/verify-security.md", "onFail": "warn", "minSeverity": "critical" },
{ "use": "auto-review" },
{ "use": "finalize" }
]
}
}
| Option | Default | Meaning |
|---|---|---|
prompt | bundled default | Inline instructions, or a path to a prompt file. |
minSeverity | "high" | Lowest priority that blocks: critical, high, medium, low, info. |
maxIterations | 3 | Bound on repair cycles when onFail is loopback. |
onFail | "loopback" | loopback, halt, or warn. |
prompt resolves like the catalog overrides: a relative string is treated as a
file path when a file of that name exists next to the declaring configuration;
otherwise it is used verbatim as inline instructions. A misspelled path
therefore does not fail the run — it either silently picks up a same-named file
in the repository or becomes a literal prompt, so confirm the file resolves.
On a failed verdict:
loopback(default) — the agent is asked to repair the findings, the repair is committed, and verification runs again, up tomaxIterationscycles.halt— the task is marked incomplete, reported to the tracker, and returned to its To Do status. Nothing is pushed.warn— the findings are recorded as warnings and delivery continues.
Malformed verdicts. Findings are validated before priority filtering.
Unknown priorities, malformed findings, and an approved: true that contradicts
medium-or-higher findings are rejected. A transient diff, agent, or JSON failure
is retried once; two failures halt with Verification failed twice: …. Account
usage limits propagate immediately to worker failover. If no valid verdict can be
obtained, the task is marked incomplete before any push.
Git disabled. With --no-git, verify records verify skipped: git workflow disabled because there is no committed diff.
Runnable verification example
settings.json fragment with a separate prompt file and a separate security
check:
{
"pipeline": {
"steps": [
{ "use": "implement" },
{ "use": "commit" },
{ "use": "verify", "prompt": "./prompts/verify.md", "onFail": "loopback", "minSeverity": "high", "maxIterations": 2 },
{ "use": "verify", "prompt": "Only flag security regressions; ignore style.", "onFail": "warn", "minSeverity": "critical" },
{ "use": "finalize" }
]
}
}
prompts/verify.md:
Verify whether the committed diff satisfies the task's acceptance criteria.
Report each unmet requirement as a `high` or `critical` item and return the JSON
verdict shape only. Do not report style preferences.
Run it and inspect the artifacts:
devintern PROJ-123 --create-pr
# Artifacts land in the task output directory (default /tmp/devintern-tasks/<task-key>):
# verify-prompt.txt the exact prompt sent to the agent
# verify-feedback.json the parsed verdict
To see the three policies, change onFail: loopback repairs and re-verifies
(bounded by maxIterations), halt stops before pushing and returns the task to
To Do, and warn records the findings and continues to finalize.
Deterministic checks vs agent verification
verify is semantic and model-driven. When a check can be expressed as a
command or an invariant, prefer a deterministic plugin step — it is faster,
cheaper, and reproducible. The run-tests example
runs a real Bun command and halts on a non-zero exit; check-policy blocks
forbidden added files; verify-artifacts validates a generated artifact’s
schema. Mix them freely: deterministic gates first, agent verification last.
Custom plugin steps
A custom step is a project-relative module or installed package listed under
pipeline.plugins. The module default-exports a StepDefinition:
import { StepExecutionError } from "@getdevintern/code/pipeline";
import type { PipelineContext, StepDefinition, TaskStepResult } from "@getdevintern/code/pipeline";
const definition: StepDefinition = {
name: "check-policy",
create(config) {
// Called once at pipeline resolution with the step's options.
return {
name: "check-policy",
async run(context: PipelineContext): Promise<TaskStepResult> {
if (config.dryRun === true) return { kind: "continue", data: { dryRun: true } };
return { kind: "continue" };
},
};
},
};
export default definition;
Register it:
{
"pipeline": {
"plugins": ["./.devintern-code/steps/check-policy.ts"],
"steps": [
{ "use": "implement" },
{ "use": "commit" },
{ "use": "check-policy", "dryRun": false },
{ "use": "finalize" }
]
}
}
Rules:
- A step name must be unique and non-empty, may not replace a reserved name
(
clarity,implement,commit,verify,auto-review,finalize,publish,repair), and may not start with__. - A given plugin step may appear only once in a configured pipeline.
create(config)receives the step entry’s options (minususe). Unknown or invalid options are the plugin’s responsibility to validate.
PipelineContext
The context passed to run is the same object across all steps and carries:
| Field | Meaning |
|---|---|
taskKey, taskSummary, task, tracker | Task identity and tracker client (may be undefined for markdown inputs). |
projectSettings | Parsed .devintern-code/settings.json, or null. |
taskContent | The formatted task prompt the agent received. |
taskFile | Path to the formatted task file. |
workingDir | The repository worktree the task runs in. |
outputDir | The task debug output directory. |
artifacts | Register immutable outputs and read registered evidence. |
output | The implementation output (agent stdout). |
committed | Whether the implementation has been committed. |
prTargetBranch | Base branch for the PR/diff. |
enableGit, createPr, skipComments, autoReview | Effective run flags. |
autoReviewIterations, maxTurns | Caps. |
warnings | Accumulated warning strings (warn results append here). |
results | Every step record so far: { step, result }, including data. |
loopbackFeedback, loopbackReason | Set by a repeat for the next run. |
runAgentPrompt(prompt) | Run the configured agent and return its stdout. |
getDiff() | Unified diff against prTargetBranch. |
parseReviewFeedback(output) | Parse and validate a JSON review verdict. |
filterByPriority(items, min) | Filter findings at or above a priority. |
data on a step result is how steps communicate: a later step reads
context.results to find an earlier step’s data. Plugins execute inside the
task process, with that process’s environment, credentials, and permissions —
the same access your coding agent has.
Step results
run returns (or, returning nothing, implies continue):
| Result | Shape | Effect |
|---|---|---|
| continue | { kind: "continue", data? } | Proceed to the next step. |
| warn | { kind: "warn", reason, data? } | Record a warning (context.warnings) and continue. |
| halt | { kind: "halt", reason, haltKind?, data? } | Stop. Default marks the task incomplete; haltKind: "stop" stops delivery without changing the task’s status and without posting tracker feedback. |
| repeat | { kind: "repeat", from, maxRepeats, reason?, feedback? } | Re-execute from an earlier step, bounded by maxRepeats. |
repeat details:
frommust name an earlier step in the sequence. Re-executing runs that step and every step after it, up to and including the repeating step.repeatfromimplementtherefore re-runsimplement,commit, and every remaining step before the diff is checked again.maxRepeatsis a positive integer bound counted per repeating step. When it is exceeded the pipeline halts withTask step '<name>' exceeded its repeat limit (<n>).feedback(aReviewFeedback) is exposed to the re-run throughcontext.loopbackFeedback; the built-inimplementandverifysteps build a repair prompt from it.- The built-in
verifyloopback does not repeatimplement. It repeats its private__repair-<n>step, which runs the agent repair and recommits, then runsverifyagain. A custom step that wants a full re-implementation repeats fromimplement.
A plugin that mutates files after commit must ensure they are committed
before publication: return { kind: "repeat", from: "commit", maxRepeats: 1 }
(or repeat from implement) so the commit step runs again. Prefer placing
mutating steps before commit when possible.
Surfacing artifacts in the PR and dashboard
Register intended outputs with context.artifacts.addFile({ key, path, title, kind, mediaType }). The runtime snapshots each file and attributes it to this
run and step execution. The dashboard reads the same registry; later attempts
cannot overwrite earlier evidence.
Publication is explicit through pipeline.artifacts.publish: select artifact
keys and render them inline, collapsed, embedded, or linked in one managed
GitHub PR comment.
See Pipeline artifacts for the contract, configuration,
uploader adapters, reference resolution, and explainer plugin setup.
Transient retries with StepExecutionError
Throw StepExecutionError for a transient execution failure that deserves one
retry (a flaky remote, a momentarily missing dependency):
import { StepExecutionError } from "@getdevintern/code/pipeline";
async run(context) {
try {
await refreshRemoteArtifact(context.workingDir);
return { kind: "continue" };
} catch (error) {
throw new StepExecutionError(`Could not refresh artifact: ${(error as Error).message}`);
}
}
The runner retries the same step once. A second StepExecutionError halts
with the error message; any other thrown error propagates to the task’s existing
failure handler (including account-wide usage limits).
A failing check is not a transient error. A failed test, a policy violation,
or a negative review verdict is a permanent finding: return
{ kind: "halt" } (or repeat) instead of throwing. The
transient-retry example shows both shapes.
Loading plugins and resolving paths
Plugins are listed as module paths or package names. Resolution depends on where the pipeline is declared:
.devintern-code/settings.json— relative paths and package names resolve against the repository root (the task working directory).- Workspace
pipeline_file— relativepluginsandverify.promptpaths are made absolute against the directory containing the pipeline file. Package names resolve from that same base.
Consequently, a shared plugin’s dependencies must be installed next to the
declaring file. For a workspace file at ~/.devintern/pipelines/backend.json,
install with cd ~/.devintern/pipelines && bun add <package> and place relative
modules (for example ./steps/check-policy.ts) there too.
Installed-package plugins. A plugin published to npm is referenced by package
name and resolved with Bun.resolveSync from the declaring configuration’s
directory:
{ "pipeline": { "plugins": ["@acme/devintern-steps"], "steps": [ /* … */ ] } }
The package’s entry module must default-export a StepDefinition, exactly like a
project-local file. Community plugins live in their own repositories; this
project does not host or endorse a plugin marketplace.
Two repositories, two pipelines
Each managed repository can run a completely different pipeline. Commit the configuration to the repository so the worker picks it up.
backend/.devintern-code/settings.json:
{
"pipeline": {
"plugins": ["./.devintern-code/steps/run-tests.ts"],
"steps": [
{ "use": "implement" },
{ "use": "commit" },
{ "use": "run-tests", "args": ["test", "--filter", "unit"] },
{ "use": "verify", "onFail": "loopback" },
{ "use": "finalize" }
]
}
}
frontend/.devintern-code/settings.json:
{
"pipeline": {
"plugins": ["./.devintern-code/steps/check-policy.ts"],
"steps": [
{ "use": "implement" },
{ "use": "commit" },
{ "use": "check-policy", "forbiddenPaths": ["public/secrets/"] },
{ "use": "auto-review" },
{ "use": "finalize" }
]
}
}
workspace.toml:
[[repos]]
name = "backend"
remote = "git@github.com:acme/backend.git"
[[repos]]
name = "frontend"
remote = "git@github.com:acme/frontend.git"
What to expect:
- The worker manages bare clones under
~/.devintern/repos/and runs each task in a fresh, disposable worktree under~/.devintern/worktrees/. The pipeline it selects is the one committed in that worktree. - Each task runs in its own CLI subprocess, so plugin registries and selections never leak between tasks or repositories.
- Editing files only in a personal checkout (for example
~/code/backend) does not affect managed runs: the worker clones and checks out its own copies. Commit the pipeline (and its plugin/prompt files) to the repository, or put the definition in a workspacepipeline_file. - A
[[repos]].pipeline_fileon either repository overrides its committedsettings.json; a[defaults].pipeline_fileapplies only when the repository declares neither.
Examples
Complete, runnable examples live under
examples/pipeline-plugins/:
| Example | Demonstrates |
|---|---|
run-tests | Run a real Bun command in context.workingDir, await completion, halt on non-zero exit. |
check-policy | Inspect changed files against the PR target and block forbidden additions. |
verify-artifacts | Require a generated artifact and expose it through context.results. |
transient-retry | StepExecutionError single-retry vs a permanent halt. |
agent-review | getDiff/runAgentPrompt/parseReviewFeedback/filterByPriority with a bounded repeat. |
pr-explainer-narrative, pr-explainer-visual | Diff-grounded explanations written before finalize; registered for the dashboard and optional PR-comment publication. |
Run the whole suite with bun run examples/pipeline-plugins/run-tests.ts.
Source of truth
These files implement the behavior described above:
pipeline-config.ts— default pipeline, ordering validation, plugin loading.pipeline-registry.ts—StepDefinition,PipelineContext, reserved names,StepExecutionError.step-runner.ts— step results, repeat bounds, transient retry.run-harness-finalize.ts— delivery steps (commit,auto-review,finalize) andhaltKind.verify.ts— verification policies and repair prompts.pipeline-selection.ts— workspace/repository precedence.pipeline-api.ts— the public@getdevintern/code/pipelinesurface.
The explainer examples’ prompt guidance is adapted from public skill
repositories; pinned revisions and attribution are in the example CREDITS.md
files. Explainer outputs are registered artifacts; pipeline policy selects what
appears in the managed PR comment. See Pipeline artifacts.