ConPort — Project Management System
ConPort stores everything about the project: tasks, documents, decisions, infrastructure.
Without init, context is unavailable. Without search, you are answering blindly.
This skill carries the always-on discipline — what you must do every session
regardless of topic. Deep, situational reference (recipe semantics, the gap
system, the semantic pass, the full per-tool parameter tables, the block model,
the documentation-graph callout reference, the spec append-only rationale) lives
in the live docs — see the Live docs section below. Fetch the relevant
page before acting on one of those topics; don't act from memory.
MCP Prefix
| Environment | Prefix |
|---|
| Claude Code CLI | |
| Claude.ai Chat | |
FIRST ACTION OF THE SESSION
Step 1: Determine the project name
Claude Code CLI — the env var
is already available (from
.claude/settings.local.json
).
If it's not set, fall back in this priority order:
- Git remote — extract the repo name (last segment of the URL without )
- Directory name — basename of the current working directory
Claude.ai Chat — ask the user (the file system is not available).
Step 2: Call init
mcp__conport__init({
name: "<detected_name>",
skill_id: "conport",
skill_version: "<metadata.version from this SKILL.md frontmatter>", // substitute the real value; never send this placeholder
client_type: "claude-code" // or claude-ai / cursor / codex
})
/
/
are optional but strongly recommended — they let the server tell you when SKILL.md has been updated upstream so manual installs (Claude.ai project files, hand-copied skills) don't silently drift. Pick
from the list above; fall back to omitting it if running somewhere else.
If auto-detection of the project name did not work, ask the user.
After init — MANDATORY:
-
Print summary: [CONPORT] {summary from response}
-
Execute instructions from the response (read files, apply rules)
-
Report backlog: N tasks in TODO, M in progress. Top 5:
— use
from the response. Line format:
Pk · #id title (n subtasks)
, skipping
when zero.
is
. Skip the whole block if
and
backlog.total_in_progress == 0
.
-
Report the roadmap: when the response carries
and/or
, print them in the format given under
OUTPUT FORMAT → Roadmap sections. Both sections are omitted when the project has nothing to show — then print nothing.
-
If the project is empty (no decisions, no patterns, empty
) — offer the bootstrap flow from
.
-
If is present in the response — surface it to the user and offer to accept (
, e.g. with the
) or dismiss permanently (
set_routine_config(enabled=false)
).
-
If is present in the response — emit ONE short notice at the very start of your first reply (after the
line). Format:
[SKILL UPDATE] {skill_id} {current} → {latest} ({severity}). Changelog: {changelog_url} · Install: {install_guide}
- When — phrase as
cannot determine version, see {install_guide}
.
- When — emit a stronger line () and recommend updating before proceeding.
- Do NOT re-emit the notice in subsequent turns — once per session.
Never decide "is there an update" by hand-comparing version numbers.
The conport plugin (this skill + superpowers-conport + plugin.json +
marketplace) versions independently from
, which is a
separate privately-distributed unit with its own version line. Numbers from
different units are NOT comparable. The ONLY correct signal is
above: present → update; absent → you're current.
If you catch yourself reasoning "my number looks higher than that other
unit's, so I'm ahead" — stop, that's the exact mistake this signal prevents.
Save-first. Save decisions (
) and progress (
)
as they happen, not in a batch at the end of the session. The moment a choice is
made or a step is finished, persist it — saving at the end loses rationale and
an interrupted session leaves nothing behind. (Live docs →
.)
Without init you cannot: answer questions about the project or work with tasks.
Ignoring instructions is FORBIDDEN.
WORKFLOW — When to call which tool
The at-a-glance trigger → tool map. Each table is navigational; for the deep
semantics of a surface, fetch the live-docs page named in the table.
Planning
| Trigger | Tool |
|---|
| "We need to do X" | with priority and (priority 1-5 where 1=critical and 5=idle, default 3; epic rows leave effort NULL, sum-from-children at read time) |
| "X depends on Y" | |
| "Create an epic" / multi-step body of work | with |
| "Break it into subtasks" | with (parent must be ) |
| "Move task X under epic Y" / re-parent | with (target must be ; detaches to root) |
| "Roadmap" / "milestones" / "what ships in the release" | (roadmap view, for history) |
| "Plan a milestone / a release" | with (+ to insert at a position, for a release point) |
| "Rename / move / close a milestone" | ( moves it, + closes it) |
| "This epic belongs to milestone M" | with (epics only; detaches from its milestone) |
| "Promote this task to an epic" | with (task must be root — combine with to detach + promote atomically) |
| "Demote this epic to a task" | with (epic must have no children) |
| Need a task in another project I own (no context switch) | with name |
Execution
| Trigger | Tool |
|---|
| Starting work | → IN_PROGRESS (before the first write on a task) |
| Done / Finished | → DONE with (see below) |
| Cancelled | → CANCELLED with (why dropped) |
| Blocked | → BLOCKED |
| Milestone finished | → DONE with (all its epics must be closed first) |
| Milestone dropped | → CANCELLED with (why it left the roadmap) |
IN_PROGRESS gate. Before your first ConPort write against a task, move it to
IN_PROGRESS. This keeps
accurate and the backlog honest.
An epic closes only with its children closed — the server refuses
update_task(status='DONE')
on an epic that still has open children with
, listing them. A snoozed child counts as open.
Epic progress is counted ONLY from /
list_tasks(parent_task_id=…)
— never from a plan file's task list. A plan
document is a snapshot of the intent; the epic's children are the state. Every
write that touches a child (
under an epic,
status or
re-parent,
) echoes
— read that rollup, print it
per
OUTPUT FORMAT, and when it says
, close the epic with a
right then instead of leaving the tail to rot.
Closing tasks — always pass :
On
or
, pass a
argument with the verdict
(what was done / why cancelled). The server:
- Appends a section to the task's description (preserves the
original spec verbatim).
- Auto-creates a linked so the close shows up in
, , and search.
Do NOT call separately for task closes — that would
duplicate the entry.
is for progress events that don't belong
to a single closing task (e.g. mid-implementation notes, infra changes).
Roadmap discipline. A milestone is an ordered group of epics; the
current
milestone is the open one with the smallest
, and
/
return it in the
section.
- Work the current milestone. In an interactive session, picking up work
that belongs to a later milestone needs an explicit decision by the
owner — say which milestone the work sits in and ask before starting; never
silently pull work forward because it looked convenient. Work attached to no
milestone () is not off-limits, but say so when you take
it, so the owner can attach its epic to the roadmap or leave it aside
deliberately. (An autonomous routine run has no owner to ask: the
conport-routine skill overrides this rule with its own — unattached rows
are fair game, later-milestone rows are not.)
- Closing a milestone always carries — what was delivered
(DONE) or why it was dropped (CANCELLED). The server logs a progress entry
from it.
- A release milestone () closes only after the release
actually happened. "Every epic is DONE" is not a release; the resolution
names the shipped version/artifact. If the release hasn't shipped, leave the
milestone open and say so.
- is a prescription, not an error to route around.
update_milestone(status='DONE')
is refused while any epic of the milestone
is open; lists exactly what to close first (an empty
milestone is refused too — attach the epic that carries the work). Do the
prescribed closes, then retry. Never CANCEL a milestone just to get past the
refusal.
→ Deep detail: live docs
.
Patterns
| Trigger | Tool |
|---|
| "Show me the patterns" | |
| "Record a pattern" | with name, description, tags |
| "Update / rename / re-tag a pattern" | |
| "What approaches do we use?" | or by topic |
Search
| Trigger | Tool |
|---|
| Question about the project | BEFORE answering |
| "What was decided about Y?" | by topic |
| "Which projects do I own?" / bootstrapping without a known project_id | |
Sync
| Trigger | Tool |
|---|
| Technology choice | |
| Trade-off with rationale | |
| Read one decision's full body (id → tags + currency) before amending | |
| Amend / re-tag an existing decision | |
| Decision no longer holds, NO replacement | (reason required) |
| Un-retire a decision | |
| Decision candidates auto-extracted from | never committed directly — they wait in the proposals queue: → approve/reject/defer; filters provenance via ( | ) |
Progress
| Trigger | Tool |
|---|
| Standalone progress note (not a task close) | |
| Closing a task | with (auto-creates progress; do not also call ) |
| Context has changed | |
Documentation
| Trigger | Tool |
|---|
| Spec / API docs | |
| Wholesale body / metadata update | update_document(content=<full markdown>, ...)
|
| List a doc's blocks (pick ulids before a surgical edit) | |
| Read / edit / insert / delete one block | / / / |
| Read a doc (with rendered Wave 5 stubs) | ( for unmodified markdown) |
| "Who references this doc/block?" | (omit for whole doc) |
| "What's similar to this block but not yet linked?" | get_semantically_related_blocks
|
| Overlapping content / linking two docs | author one callout — / / / |
Block-level editing is the default for any narrow change — including specs.
Use
/
/
for surgical edits; only one
block re-embeds and the spec append-only invariant doesn't engage.
update_document(content=...)
is the
wholesale-rewrite channel.
Spec append-only invariant. on a
body
requires
:
(clarification/typo — allowed, logged with a
mandatory
) or
(a meaningful claim change —
rejected;
author a new spec and link the old one with
link_items(relationship='supersedes')
).
Don't chain block edits to rewrite a spec's claims either.
Don't create a doc when an edit will do. Never
whose purpose
is to
describe / amend / react to an existing doc — that accumulates synthesis
drift. Edit the original, or author an addendum with an explicit callout
(
/
> [!supersedes] [[doc-N]]
); default to
when unsure.
→ Deep detail: live docs
,
projects/spec-append-only
,
core/documentation-callouts
; full agent reference in
references/documentation_graph.md
.
Routines (periodic backlog cycles)
| Trigger | Tool |
|---|
| Starting a periodic cycle run | (single-call briefing), |
| "Set up / tune the cycle policy" | / (cadence, run limits, autonomy 0-2, selection |) |
| Mark a task as agent-executable | with (opt-in pool for cycles) |
| Finishing a run | with (auto-logs progress — no separate ) |
| "Show recent runs" | |
| Calibrating before estimating tasks | (median ratio, p50/p90 actuals) |
| Picking unblocked work deep in the backlog | with , , |
| Deferring a task to a date | with (empty string clears) |
→ Full runbook: the
conport-routine skill; live docs
.
Context assembly, gaps, semantic pass
| Trigger | Tool |
|---|
| "Open task #N and brief me" | with , |
| "Implementation status of spec doc-N?" | with recipe='spec_implementation_status'
, |
| "What recipes are available?" | |
| "Current architecture of subsystem X?" | render_current_architecture
with |
| "Is this architecture doc safe to archive?" | audit_doc_l1_coverage(doc_id)
|
| Init response shows gaps / "show all gaps" | review ; , , (reason), , , |
| "Clean up the graph" | (one-click; runs in background — poll ) |
| Manual semantic flow | semantic_pass_run(dry_run=true)
→ → approve/reject/defer → ; |
convention. Prefer the prefix form
(
,
); type vocabulary
/
/
/
/
. The wikilink form
is also accepted verbatim. Plain
integers work as a legacy fallback but the prefix form gives a clean 400 on type
mismatch.
→ Deep detail: live docs
,
,
.
TASK HIERARCHY (2 levels, schema-enforced)
The task tree is two levels and the database enforces it:
- — leaf node. May have pointing to an epic. Cannot have children.
- — root container. Always . Other tasks attach under it.
No third level. Trying to attach a task under another task (kind=task with kind=task parent) is
rejected at the DB level. The MCP/REST layer maps the rejection to a structured
payload with two recovery options.
Recovery: error
When
add_task(parent_task_id=X)
or
update_task(parent_task_id=X)
returns:
json
{
"error": "parent_not_epic",
"message": "Cannot attach a task under task-X — a task can only have an epic as parent.",
"context": {
"intended_parent": {"id": X, "kind": "task", "title": "..."},
"resolved_epic": {"id": Y, "kind": "epic", "title": "..."}
},
"suggestions": [
{"action": "promote_parent", "call": "update_task(task_id=X, kind='epic')"},
{"action": "attach_to_epic", "call": "add_task(..., parent_task_id=Y)"}
]
}
Decide by local context:
- promote_parent when X is itself a substantial body of work and the new task is a subtask of it → make X an epic, attach the new task under it.
- attach_to_epic when X is just another leaf inside an epic Y → attach the new task to Y as a sibling of X.
If
is
, only
is offered — there's no ancestor epic in the chain.
Promote / demote rules
- Promote → : task must be root (no parent). Combine with in one call to atomically detach + promote.
- Demote → : epic must have no children. Close or reparent subtasks first.
Cross-references stay
for both kinds — epic is a subtype, not a separate namespace.
CROSS-REFERENCE FORMAT (canonical grammar)
Every reference to another ConPort item — in
,
,
, document body, commit messages — uses the canonical form
.
Type vocabulary (lowercase): ,
,
,
,
. No aliases (no
, no
, no
).
Forms accepted by the parser:
- Plain prose: , , .
- Wikilink: , , — preferred inside
document bodies; the autolinker reifies them as item-graph edges.
- Block anchor (documents only): — link to a
specific block (Wave 6).
Anti-patterns (silently break tag/graph navigation):
| ❌ Don't write | ✅ Write |
|---|
| (typed legacy with ) | |
| (untyped continuation) | task-123, task-124, task-125
|
| (untyped, ambiguous) | (or the correct type) |
| (pre-migration global id) | drop — autolinker can't resolve; cite the new per-project id |
Microcheck (extends POST-WRITE VERIFICATION):
Before the write call, scan your
/
/
payload for
. If you find one:
- Replace with if you know the type.
- Untyped or pre-migration id → either drop, or flag explicitly ("legacy id
#N, not resolvable") so a reader knows it's intentional, not an oversight.
The parser currently accepts both legacy
and canonical
so
older corpus stays linked; emit canonical-only in new writes.
OUTPUT FORMAT
MCP tools return JSON with a
field. Use it to inform the user.
| After | Format |
|---|
| |
| [ConPort: N results found for "query"] ...
|
| |
| Task DONE/CANCELLED | (progress entry was auto-logged from ) + suggest updating active_context |
| / | (carries id, title, , , epics-closed rollup) |
| Any write response carrying | [EPIC] task-{epic_id}: {open_children} of {total_children} children open
— when is true, print [EPIC] task-{epic_id} ready to close — close it with a resolution now
instead, and do it. Print this line only when the summary you already printed doesn't carry the rollup itself: / summaries end with either — epic task-N: K of M children open
or, on the closable branch, — epic task-N ready to close: update_task(...)
— either way the count/prescription is already on screen; and the REST responses need the explicit line |
Roadmap sections. and
return
and
only when the project has them — a missing section means "nothing to show",
print nothing. When present:
[ROADMAP] milestone-{current.milestone_id} «{current.title}»{ [release]}{ · ready to close} · {open_milestones_total} milestones open
· task-{task_id} {title} — {open_children}/{total_children} subtasks open
Next: milestone-{next.milestone_id} «{next.title}»
One
line per entry of
(only the
open epics of the
current milestone are listed — a closed one is already delivered); add
only when
is true and
only when
is true; drop the
line entirely when
is
.
[TAILS] task-{epic_id} {title} — {suggested_action}
One line per entry of
, in the order returned (closest to closing
first;
means nothing is left but the close itself).
is prescribed by the server — print it and follow it, don't
substitute your own plan.
MCP create / update tools return a
slim payload (not the full entity body) to
save agent context —
,
(echoed even as
),
, and
context-specific fields (
,
,
, …). Need the full body? Use
the matching read tool (
,
,
, …). The
slim
/
echo is your POST-WRITE verification channel.
(Live docs →
core/post-write-verification
.)
Decision currency. Read surfaces (
,
,
) annotate each decision with
(
/
/
) and
. Before citing a decision as authoritative, check
: if
, follow
to the current decision; if
, treat it as retired (see
) and do not present
it as current. A high
on a
decision is a prompt to verify
it still holds, not proof that it's stale.
POST-WRITE VERIFICATION
Some MCP clients silently drop an optional parameter when the surrounding
tool-call XML is malformed — the dropped value folds into the previous string
field and the call returns 200 OK with a truncated payload. A real incident lost
a
's
array (they ended up inside
); the decision
landed untagged and broke graph navigation.
The server now
rejects any write whose string field contains literal
tool-call fragments (
,
,
,
,
) with a structured
error — the bad write does not land; re-issue with the field cleaned. That guard
catches the worst class; the echo check below still catches the subtler cases.
After every create/update call that took optional fields, verify the response
echo against intent:
| You passed | Check on the response |
|---|
| Response is non-empty and matches intent (count + values) |
| length ≈ what you sent (not visibly truncated) |
| equals |
| , links | Field is present and equal to intent |
| / | Response matches the promote/demote you asked for |
| / on a task | Response is (or after a detach) |
| / on a milestone | Response / match intent — milestone writes echo , , (not ), so those are the verification channel |
On mismatch. Re-issue the call with the field re-stated (often one retry
fixes the XML glitch). If a second attempt still loses it, flag the mismatch to
the user verbatim ("graph integrity: tags lost on decision N, please re-run")
rather than silently moving on — the damage is mute graph drift, easy to miss.
Applies to:
,
,
,
,
,
,
,
,
,
,
, block ops (
,
,
).
MCP ERROR HANDLING
On an
Invalid arguments for tool
error:
- READ — the name of the broken parameter
- READ — the required type
- FIX ONLY THAT PARAMETER
- DO NOT TOUCH OTHER PARAMETERS
CHECKLIST
Live docs
The deep, situational reference lives at
https://conport.app and is the
single source of truth.
Before acting on a deep topic, fetch the relevant
page. Public index:
https://conport.app/llms.txt. (No web fetch? Use the
CLI.)
| Topic | Page |
|---|
| Save-first discipline | |
| Knowledge-graph model (item links + GraphRAG) | |
| Cross-reference grammar | |
| Documentation-graph callouts (full reference) | core/documentation-callouts
|
| Post-write verification / slim responses | core/post-write-verification
|
recipes / render_current_architecture
| |
| Knowledge-base gaps | |
| Semantic pass | |
| Spec append-only invariant | projects/spec-append-only
|
| Block-level document model | |
| Task hierarchy | |
| Roadmap milestones | |
| Full per-tool parameter reference | |
Local references (shipped with the skill):
references/command_list.md
(full MCP tool API),
references/documentation_graph.md
(long-form callout
reference),
(fresh-project onboarding).