Engagements — the four-phase delivery spine
VibeCody ships the tools that produce almost every artifact a client engagement promises: architecture specs, threat models, CI gates, SLO dashboards, cost models, runbooks. What it did not have was the object that says which of them this engagement has actually produced, and whether the phase they belong to may be closed.
That is what an engagement is. One record, four phases, a list of promised deliverables, and a set of gates that decide whether a phase can be exited.
- Panel: Delivery → Engagement (VibeCoder)
- Routes:
/engagements/*on the VibeCLI daemon (bearer auth required) - Store:
~/.vibecli/engagements.db - Source:
vibecli/vibecli-cli/src/engagement.rs,vibecli/vibecli-cli/src/engagement_routes.rs
The four phases
| # | Phase | Cadence | Purpose |
|---|---|---|---|
| 01 | Discover & Assess | not published | Map the current state, interview the people who operate it, and separate the problems worth solving from the noise. No engagement starts with a solution already chosen. |
| 02 | Prove | 4–8 weeks | A narrow pilot on your data, on your infrastructure, judged against success criteria agreed up front. If the approach is wrong, this is where we find out cheaply. |
| 03 | Build & Harden | Scope-dependent | Production delivery alongside your engineers — infrastructure as code, CI/CD, test coverage, observability, and a security review that happens during the build rather than after it. |
| 04 | Operate & Transfer | Ongoing or fixed | Managed operation for as long as it is useful, and a deliberate handover after that. The goal is for your team to own the system, not to keep us on the invoice. |
Discover has no cadence, and the code says so. Phase::cadence() returns
None for it, the API returns null, and the panel renders a dash. The
engagement model publishes a duration for the other three phases and not for
this one; inventing “2–4 weeks” would put a commitment in front of a client that
nobody made. See Modelling Honesty.
Deliverables
Creating an engagement seeds it with every deliverable the engagement model promises — 30 of them across the four phases — each carrying a tool hint: the VibeCody panel that produces it.
The tool hint is the point. VibeCoder has more than three hundred panels, and a list of promised artifacts is not useful if the operator has to guess which panel produces “threat model”. In the panel each deliverable’s tool hint is a button that opens the owning tab.
A representative slice:
| Phase | Deliverable | Produced with |
|---|---|---|
| Discover | Current-state architecture map | ArchitectureSpecPanel |
| Discover | Risk register | SecurityPosturePanel |
| Discover | Prioritized roadmap with effort and sequencing | PlanDocumentPanel |
| Prove | Working pilot deployed in your environment | DeployPanel |
| Prove | Cost model at target production volume | CostPanel |
| Prove | Go / no-go recommendation with alternatives | CounselPanel |
| Build | Infrastructure as code | K8sPanel |
| Build | Alerting and SLO definitions | HealthMonitorPanel |
| Build | Threat model | SecurityReviewPanel |
| Operate | Runbooks | PlanDocumentPanel |
| Operate | Escalation paths | TeamGovernancePanel |
| Operate | Documented exit plan | CompanyPortabilityPanel |
The full list is TEMPLATE in engagement.rs, and
GET /engagements/template returns it without needing an engagement to exist.
Deliverable status
not_started → in_progress → ready → accepted, plus waived.
Only accepted closes a deliverable — ready means we produced it and
reviewed it ourselves, which is not the same as the client signing it off.
waived means agreed out of scope: it is excluded from the completion
denominator and reported in its own section of the handover pack, never folded
into “done”.
Evidence
A deliverable marked ready or accepted with zero attached evidence is
flagged, counted in claimed_without_evidence, and blocks the phase. A
deliverable claimed done with nothing behind it is the most common way an
engagement report lies.
Evidence kinds: file, url, run (an eval run id, job id, or workflow run),
metric, note.
Gates
A gate is a criterion, a measurement procedure, an observation, and a
verdict. The measurement procedure is mandatory — POST /engagements/{id}/gates
rejects a gate without one, because a criterion with no stated way of judging it
is settled at review time by whoever argues hardest.
The five verdicts
| Verdict | Meaning | Satisfies the gate? |
|---|---|---|
not_measured |
Nobody has arranged to judge this. | No |
pending |
A judgement is scheduled and has not happened. | No |
pass |
Judged, and it passed. | Yes |
fail |
Judged, and it failed. | No |
waived |
Agreed not to apply. | Yes |
not_measured and fail are kept strictly apart, exactly as in
vibecli --eval. Collapsing them into “fail” produces a
report that says the work is broken when the truth is that nobody looked;
collapsing them into “pass” ships an unmeasured claim to a client.
A gate is seeded not_measured on creation. A fresh engagement reporting its
gates as passing would assert a fact about the world nobody has checked.
A passing gate must record what was observed. POST .../judge returns 400
for verdict: "pass" with an empty observed. Returning a gate to
not_measured clears its decider, decision time, and observation — leaving them
would assert that somebody judged it.
Default gates are seeded per phase (GATE_TEMPLATE), including:
- Criteria agreed before the pilot ran — compare each gate’s creation timestamp against the pilot’s start. A criterion created after the start is evidence of the outcome, not a test of it.
- Pipeline blocks a bad change — open a PR that deliberately violates each gate and record that the pipeline stopped it.
- Every alert has a runbook — enumerate configured alerts and join against runbooks; any unmatched alert fails.
- Client engineers have operated the system unaided — a real change and a real incident handled with no vendor participation.
Phase exit
A phase can be closed when every in-scope deliverable is accepted with
evidence and every gate is pass or waived. Anything else appears in
blockers, each with its own kind so the reason is not flattened:
deliverable_outstanding · deliverable_without_evidence · gate_failed ·
gate_pending · gate_not_measured
POST /engagements/{id}/advance refuses and returns the blockers. Passing
{"force": true} advances anyway — sometimes the client decides to proceed with
a gate failing — but the response carries forced: true and the blockers
survive in the record. An override is visible as an override.
Completion is n/a, never 0%, when there is nothing in scope
completion is accepted / (total − waived). When the denominator is zero it
is null, and every renderer shows n/a. 0% reads as “measured, and bad”;
n/a is the truth.
HTTP API
All routes are authenticated. Use daemonFetch() from a VibeCoder panel, the
SDK’s authedFetch() elsewhere, or Authorization: Bearer $(cat ~/.vibecli/daemon_token)
by hand. See Calling a daemon route.
| Method | Path | Purpose |
|---|---|---|
GET |
/engagements/template |
The engagement model: phases, deliverable templates, gate templates. Static. |
GET |
/engagements |
List engagements. |
POST |
/engagements |
Create one, seeded with the full template. |
GET |
/engagements/{id} |
The engagement plus its readiness report for all four phases. |
PATCH |
/engagements/{id} |
Set status and/or phase. |
DELETE |
/engagements/{id} |
Remove it (cascades to deliverables, evidence, gates). |
POST |
/engagements/{id}/seed |
Top up with template rows added since creation. Never disturbs an accepted deliverable. |
GET |
/engagements/{id}/deliverables?phase= |
List, optionally filtered by phase. |
POST |
/engagements/{id}/deliverables |
Add a deliverable outside the template. |
PATCH |
/engagements/{id}/deliverables/{did} |
Patch status, owner, notes. Omitted fields are untouched. |
GET/POST |
/engagements/{id}/deliverables/{did}/evidence |
List / attach evidence. |
DELETE |
/engagements/{id}/evidence/{eid} |
Remove one evidence item. |
GET/POST |
/engagements/{id}/gates?phase= |
List / add gates. |
POST |
/engagements/{id}/gates/{gid}/judge |
Record a verdict. |
DELETE |
/engagements/{id}/gates/{gid} |
Remove a gate. |
POST |
/engagements/{id}/scan |
Propose workspace files as evidence ({"path": …, "attach": false}). Never changes a status. |
POST |
/engagements/{id}/advance |
Close the current phase; {"force": true} overrides. |
GET |
/engagements/{id}/report.md |
Status report, text/markdown. |
GET |
/engagements/{id}/handover.md |
Handover pack, text/markdown. |
An unrecognised ?phase= value is a 400, not a silent “all phases” — a typo
that quietly widens a query is how a client ends up reading another phase’s
numbers.
Example
TOKEN=$(cat ~/.vibecli/daemon_token)
API=http://localhost:7878
# Create — seeded with 4 phases, every promised deliverable, every gate.
ID=$(curl -s -X POST $API/engagements \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"Acme platform","client":"Acme Corp"}' | jq -r .engagement.id)
# Where does the engagement actually stand?
curl -s $API/engagements/$ID -H "Authorization: Bearer $TOKEN" \
| jq '.report.phases[] | {title, completion, blockers: (.blockers|length)}'
# The client sees this.
curl -s $API/engagements/$ID/report.md -H "Authorization: Bearer $TOKEN"
Workspace scan — finding evidence that already exists
POST /engagements/{id}/scan, vibecli --engagement scan, or Scan
workspace in the panel walks the workspace and proposes which existing files
could back which deliverable — architecture documents, ADRs, Terraform, CI
workflows, coverage reports, SLO rules, threat models, runbooks.
What a scan is allowed to conclude is deliberately small. A file existing is
not a deliverable: docs/threat-model.md may be a finished threat model, an
empty heading, or a template somebody copied in three years ago. So the scan:
- proposes candidates, never findings;
- never changes a deliverable’s status — with
attach: trueit records evidence and nothing else; - labels what it attaches
detected: <rule>, so a reviewer can tell a machine’s guess from a human’s assertion; - has no rule at all for deliverables it cannot honestly detect. No filename
is evidence that a production system works, that you paired with the client’s
engineers, or that anyone signed an SLA. Those keys are listed in the
response’s
undetectablearray, so an empty result is never mistaken for “there is nothing there”.
Caps are reported rather than silent — a scan that stopped at its depth, file,
or candidate limit sets truncated and says so in notes. A truncated list
that looked complete would read as “there is nothing else here”.
With no path and no workspace bound to the engagement, the route is a 400.
Falling back to the daemon’s working directory would scan an unrelated tree and
label the result as this client’s evidence.
vibecli --engagement scan <id> --path /srv/acme # propose only
vibecli --engagement scan <id> --path /srv/acme --attach # record as evidence
CLI — vibecli --engagement
The panel is the comfortable way to run an engagement; the CLI is the way to check one from a pipeline.
vibecli --engagement list
vibecli --engagement new "Acme platform" --client "Acme Corp"
vibecli --engagement show <id>
vibecli --engagement deliverables <id> --phase build
vibecli --engagement set <id> threat-model accepted
vibecli --engagement evidence <id> threat-model docs/threat-model.md --kind file
vibecli --engagement gates <id> --phase build
vibecli --engagement judge <id> <gate-id> pass --observed "clean provision from scratch"
vibecli --engagement advance <id>
vibecli --engagement report <id> > status.md
vibecli --engagement handover <id> > handover.md
The pipeline check
vibecli --engagement gate <id> --phase build
Exit codes are part of the contract:
| Code | Meaning |
|---|---|
0 |
The phase is clean: every in-scope deliverable accepted with evidence, every gate pass or waived. |
1 |
The phase has blockers, or the operation failed. |
2 |
Usage error — unknown subcommand, bad phase, bad verdict. |
1 and 2 are deliberately different. A pipeline that reads “you typed the
phase name wrong” as “the engagement is not ready” fails for a reason nobody can
act on.
When gates are unmeasured the check says so separately, rather than reporting them as failures:
Build & Harden: 7 blocker(s)
- 'Infrastructure as code' is not_started, not accepted.
...
5 gate(s) are unmeasured. That is not a failure — it is an absence of
measurement, and it blocks for that reason.
--engagement judge … pass refuses without --observed, the same rule the HTTP
route enforces. --engagement set warns when a deliverable becomes ready or
accepted with nothing attached.
VIBECLI_ENGAGEMENT_DB overrides the store path — useful for pointing a team at
one shared engagement database, and for CI runs that should not touch a
developer’s ~/.vibecli.
Exports
report.md— the status report. Phase summary table, deliverables with evidence counts and their producing panel, every gate with its criterion, measurement procedure and observation, and the blockers per phase. A gate with no observation renders not recorded, not a blank or a zero.handover.md— the handover pack. How to operate the system, everything accepted, and — the section that makes it worth reading — what is not delivered, plus what was waived and why.
Design notes
The rules this subsystem enforces are the evaluation harness’s rules applied to delivery, for the same reason: an engagement report is a measurement, and it fails the same ways.
- Verdicts stay disjoint. Five gate verdicts, five deliverable statuses, and no code path that maps one onto another for convenience.
- Absent stays absent. No
unwrap_or_else(Utc::now)on a timestamp, no default observation, no invented cadence.INSERT OR REPLACEis not used anywhere — patches areDO UPDATE SETover named columns, so a partial update cannot NULL a column the caller never mentioned. - Unmeasured blocks. A gate nobody judged does not let a phase close, and appears as its own blocker kind.
- A percentage over an empty denominator is
n/a. - Re-seeding is additive.
seed_templateinserts only what is missing, so a template that grows later reaches existing engagements without discarding a client’s acceptance record.
Tests live alongside the module (cargo test -p vibecli --lib engagement) and
assert each of these directly — including that every phase has deliverables and
gates, that every gate template names a measurement procedure, and that every
deliverable names a producing surface.