5.8 KiB
@lilith/mcp-objectives
MCP server providing team-lead ownership, objective dashboards, and
cross-lead coordination over a file-based .project/ tree.
Ship work as markdown + YAML frontmatter. The server is the only thing that writes to the tree, so frontmatter stays canonical, the dashboard stays honest, and multiple agents can coordinate without stepping on each other.
Layout
<projectRoot>/.project/
├── objectives/
│ ├── README.md ← regenerated dashboard
│ ├── objectives.json ← machine-readable export
│ └── <id>.md ← one file per objective (frontmatter + body)
├── team-leads/
│ ├── README.md ← roster (optional)
│ └── <id>.md ← one file per team-lead
└── handoffs/
└── YYYYMMDD_<slug>.md ← dated cross-lead notes
Objective frontmatter
---
id: p0-05
title: Culture generation
priority: p0 # p0 | p1 | p2 | p3
status: missing # done | partial | stub | missing | oos | superseded
owner: alice # optional — must reference an existing team-lead
scope: backend # optional — free-form release scope
updated_at: 2026-04-18
evidence: # required when status=done
- src/culture.ts:42
blocked_by: [p0-04] # optional — cycle-checked on write
assigned_by: bob # optional — trail of cross-lead assignment
---
Team-lead frontmatter
---
id: alice
name: Alice
specialization: Backend infrastructure owner
objectives: [p0-05, p0-06]
---
Configuration
PROJECT_ROOT env var (or --project=<path> flag) selects the repository
the server operates on. All reads and writes are confined to
<projectRoot>/.project/. If unset, the current working directory is used.
Completion hooks
When an objective transitions to done (status: missing/stub/partial → done),
a completion hook is invoked with environment variables containing objective details.
Default behavior: If OBJECTIVES_COMPLETE_HOOK is unset, the server looks for
.project/hooks/onObjectiveComplete.sh in the project root. If it exists and is
executable, that script is invoked. This default hook updates docs (FEATURE_GAP.md,
AUDIT.md, etc.), adds Completion Notes, and runs verification.
Custom hooks: Set OBJECTIVES_COMPLETE_HOOK to override the default with your
own shell command.
Environment variables:
OBJECTIVE_ID: objective ID (e.g.p0-05)OBJECTIVE_PATH: absolute path to objective.mdfileOBJECTIVE_DATA: JSON object with selected fields (id, title, priority, owner, evidence, scope)
Example (validate completion, update docs):
export OBJECTIVES_COMPLETE_HOOK='
FEATURE=$(echo "$OBJECTIVE_ID" | cut -d- -f2-) && \
echo "✅ Completed: $OBJECTIVE_ID ($FEATURE)" && \
if [ -x "docs/verify.sh" ]; then
bash docs/verify.sh "$OBJECTIVE_ID" "$OBJECTIVE_PATH"
fi
'
Example (run playwright verification against dev server):
export OBJECTIVES_COMPLETE_HOOK='
FEATURE=$(echo "$OBJECTIVE_DATA" | jq -r ".scope // \"landing\"") && \
echo "Verifying $OBJECTIVE_ID in $FEATURE..." && \
npx playwright test --grep "$OBJECTIVE_ID" --headed 2>&1 | head -20
'
Hook errors are logged but do not block the status transition. Stdout/stderr are logged to the server console.
.mcp.json
{
"mcpServers": {
"objectives": {
"command": "npx",
"args": ["-y", "@lilith/mcp-objectives"],
"env": { "PROJECT_ROOT": "${workspaceRoot}" }
}
}
}
Tools
| Tool | Purpose |
|---|---|
objectives_list |
Filter by owner/status/priority/blocked |
objective_get |
Full frontmatter + body for one id |
objective_create |
Scaffold a new objective with Summary + Acceptance |
objective_update_status |
Change status; rejects done without evidence |
objective_assign |
Reassign to another lead (appends to lead's roster) |
objective_set_blockers |
Replace blocker list; validates ids, rejects cycles |
team_leads_list |
Roster with per-lead status rollup |
team_lead_get |
Single lead + counts of owned objectives |
team_lead_message |
Dated handoff note between two leads |
dashboard_regen |
Rewrite README.md + objectives.json |
dashboard_json |
Return live dashboard as JSON (cheap loop query) |
loop_next_action |
Next unblocked objective to dispatch, ranked |
Orchestration loop
Designed to back prompts like:
do /experts-team on a /loop with the help of /experts-thinking until everything is finished
Each tick:
dashboard_regen— keep README + JSON current.loop_next_action— pick the highest-priority unblocked gap.- Dispatch a specialist owned by that objective's team-lead.
- Specialist calls
objective_update_status(id, 'done', evidence=[...]). - If blocked,
objective_set_blockers+team_lead_messageflag the owner of the blocker. - Terminate when
dashboard_json().totalsshows no p0 remaining.
Integrity rules
- Status lives only in per-file frontmatter. Roadmap / changelog / prose must not restate it.
doneis gated on at least oneevidence:citation (file path, line, test count, screenshot — caller's choice).supersededfiles are index stubs; they are rendered separately and excluded from totals so they don't double-count their replacements.- Blocker edges are cycle-checked on every write; the server refuses a write that would form a cycle.
- Completion hooks (
OBJECTIVES_COMPLETE_HOOK) are invoked when transitioning todonefor post-completion validation, doc updates, or automated verification. Errors do not block the transition.
Dev
bun install # from the @packages/@ts workspace root
bun run typecheck
bun run src/selftest.ts # headless fixture tests
bun run build # emit dist/index.js