| name | ct-documentor |
| description | Documentation coordinator with CLEO style guide compliance. Routes every canonical-doc write (spec, adr, research, handoff, note, llm-readme) through the docs SSoT via `cleo docs add` / `cleo docs publish` / `cleo docs fetch` — never raw filesystem writes. Coordinates ct-docs-lookup, ct-docs-write, ct-docs-review, ct-spec-writer, and ct-adr-recorder. Use when creating or updating documentation files, consolidating scattered documentation, or validating documentation against style standards. Triggers on documentation tasks, doc update requests, or style guide compliance checks. |
| version | 3.15.0 |
| tier | 3 |
| core | false |
| category | specialist |
| protocol | null |
| metadata | {"version":"3.15.0","lastReviewed":"2026-05-27T00:00:00.000Z","stability":"stable"} |
| dependencies | ["ct-docs-lookup","ct-docs-write","ct-docs-review","ct-spec-writer","ct-adr-recorder"] |
| sharedResources | ["subagent-protocol-base","task-system-integration"] |
| compatibility | ["claude-code","cursor","windsurf","gemini-cli"] |
| license | MIT |
Documentation Specialist Context Injection
Protocol: @src/protocols/implementation.md
Type: Context Injection (cleo-subagent)
Version: 3.0.0
Canonical Docs Command Path (T10516 — T11048)
The agent-facing docs surface is simplified to six canonical verbs. Use these as the
primary workflow. Legacy verbs remain available but are advanced or migration
surfaces — do not present them as the default path to agents.
| Canonical Verb | Purpose | When to Use |
|---|
add | Create a new doc attached to a task/session | First write of any canonical doc |
update | Patch/replace an existing doc's content | Fixes, extensions, refreshes |
fetch | Retrieve a doc by slug | Reading, reviewing, verifying |
list | Browse docs by owner, type, or project | Discovery, checking what exists |
remove | Detach a doc from an owner | Cleanup, de-duplication |
publish | Mirror an SSoT blob to a git-tracked path | When a doc must live on disk |
Legacy verbs (advanced / migration surfaces only):
supersede, find --similar, graph, versions, sync, search,
merge, rank, generate, export, schema, list-types,
serve/open/stop/viewer-status, status/gap-check, import,
publish-pr.
For discovery (find, search, schema, list-types), prefer cleo docs list
first — it returns slug + owner + type without forcing a filesystem walk. Use
legacy discovery verbs only when list is insufficient.
Purpose
Context injection for documentation tasks spawned via cleo-subagent. Orchestrates documentation workflows by coordinating specialized skills for lookup, writing, and review.
Skill Coordination
| Skill | Purpose | Invoke When |
|---|
ct-docs-lookup | Query existing docs, find references via cleo docs fetch/list | Discovery phase, checking what exists |
ct-docs-write | Create/edit docs via cleo docs add with CLEO style | Writing or updating content |
ct-docs-review | Check compliance with style guide, read through cleo docs fetch | Quality validation before completion |
ct-spec-writer | Author specs (REQ-XXX requirements) via cleo docs add --type spec | Formal specification work |
ct-adr-recorder | Author ADRs via cleo docs add --type adr --slug adr-NNN-... | Architecture decisions promoted from consensus |
The coordinator never writes content itself — it routes the work to the
specialist skill that owns the doc type, and every specialist routes its
filesystem write through the docs SSoT (see "Coordinator Pattern" below).
Coordinator Pattern: SSoT-First Routing
ct-documentor is a router. It dispatches each doc-type to its owning skill,
and every owner writes through cleo docs add — not raw filesystem writes.
| Doc Type | Owner Skill | SSoT Command |
|---|
spec (REQ-XXX requirements) | ct-spec-writer | cleo docs add <ownerId> <path> --type spec --slug spec-<feature> |
adr (architecture decisions) | ct-adr-recorder | cleo docs add <ownerId> <path> --type adr --slug adr-<NNN>-<rest> (TODO T10360 · E3.2 pending: --title + auto-adr-NNN allocation) |
research (multi-source investigation) | ct-research-agent | cleo docs add <ownerId> <path> --type research --slug research-<topic> |
handoff (session/agent transition) | ct-documentor (this skill) | cleo docs add <ownerId> <path> --type handoff --slug handoff-<context> |
note (conversational prose) | ct-docs-write | cleo docs add <ownerId> <path> --type note --slug <kebab-topic> |
llm-readme (agent-facing) | ct-docs-write | cleo docs add <ownerId> <path> --type llm-readme --slug <kebab-topic> |
Hard rule: EVERY canonical-type write goes through the SSoT. The coordinator
rejects any subagent return that wrote raw markdown into .cleo/adrs/,
.cleo/research/, .cleo/agent-outputs/, or docs/ without first
materializing through cleo docs add + (optionally) cleo docs publish.
Decision: update vs supersede vs create (T10168 · Saga T9855 / E12)
Before adding a new doc, ALWAYS ask: is this a new doc, a change to an existing doc, or a replacement for an existing doc? Prefer the canonical six-verb path (add / update / fetch / list / remove / publish). Legacy verbs annotated below.
┌─────────────────────────────────────────────────────────────────────┐
│ Question │ Verb │
├─────────────────────────────────────────────────────────────────────┤
│ Same idea, fixing/extending content? │ cleo docs update <slug> │
│ → typo in ADR-076 │ (canonical — T10161) │
│ → adding a clarifying paragraph │ │
│ → refreshing a stale section │ │
│ │
│ Replacing the whole canonical model? │ cleo docs supersede │
│ → "saga model v2" replaces v1 │ <old> <new> │
│ → ADR-080 replaces ADR-073 │ (ADVANCED — T10162) │
│ → new architecture supplants old │ │
│ │
│ Genuinely new idea? │ cleo docs list FIRST │
│ → drafting a new spec │ (canonical discovery) │
│ → fresh ADR for a new concern │ → if no hit, docs add │
│ → recording a new investigation │ → if hit, docs update │
│ │
│ Tracing the lineage graph? │ cleo docs graph │
│ → "what does this doc replace?" │ --root <slug> │
│ → "what tasks reference this ADR?" │ (ADVANCED — T10164) │
└─────────────────────────────────────────────────────────────────────┘
Canonical discovery path: cleo docs list (browse by owner/type) → cleo docs fetch <slug> (read). Reserve cleo docs find --similar (ADVANCED — T10163) for near-duplicate collision checks and cleo docs search for semantic queries.
Examples
-
"Fix a typo in ADR-076 saga-first-class" - cleo docs update adr-076-saga-first-class --file /tmp/fixed.md --message "fix typo in §2" (canonical - T10161). NOT a new doc. NOT a supersession. Just patch in place; T10161 squashes patches within a 5-minute window so consecutive typo fixes don't bloat the audit log.
-
"Replace the entire saga-orchestration model with v2" (ADVANCED) - cleo docs add T9999 v2.md --type adr --slug adr-080-saga-orchestration-v2 followed by cleo docs supersede adr-073-above-epic-naming adr-080-saga-orchestration-v2 --reason "v2 canonicalizes the SG- prefix + 4-tier hierarchy". Both rows survive in the attachments table; readers see lifecycle_status=superseded on v1 and supersedes=adr-073 on v2.
-
"Drafting a new spec for the BRAIN recovery pipeline" - FIRST run cleo docs list --type spec to browse existing specs (canonical discovery). If a matching spec exists, use cleo docs update. If no match, proceed with cleo docs add T#### draft.md --type spec --slug spec-brain-recovery-pipeline. The cleo docs find --similar brain-recovery path is the ADVANCED collision-detection surface - use only when list is insufficient.
-
"Auditing the supersession chain for ADR-039" (ADVANCED) - cleo docs graph --root adr-039-lafs-envelope-unification --depth 3. Returns a DocProvenanceGraph envelope (T10166 contract) with nodes + edges, optionally --format dot for graphviz visualization.
Anti-patterns (INSTANT REJECTION)
- ❌ Calling
cleo docs add with a slug that's only a typo-distance away from an existing doc — the auto-warn (T10167) fires W_SLUG_SIMILAR; respect it and route to cleo docs update instead of bypassing with --allow-similar.
- ❌ Editing a
.cleo/adrs/*.md file directly with Write — bypasses the SSoT writer; T10366 WriterRegistry is the chokepoint and the docs SSoT will reconcile auto-emit drift.
- ❌ Creating a new doc when an existing one needs updating — bloats the attachments table and breaks lineage. The decision tree above resolves this — when in doubt, run
cleo docs list first, or cleo docs find --similar <proposed-slug> (ADVANCED) as a last resort.
Through SDK (preferred)
Documentation work flows through the docs SSoT using the canonical six-verb path:
add, update, fetch, list, remove, publish. Use the slug-based contract so downstream consumers
can retrieve docs without grepping the filesystem.
Add a doc attached to a task
cleo docs add T1234 docs/drafts/feature-x.md \
--type note \
--slug feature-x-overview \
--desc "Conversational overview — pre-review"
--type MUST be one of spec | adr | research | handoff | note | llm-readme.
Pick the type by the document's purpose, not its filename.
--slug is the human-friendly retrieval handle (kebab-case). If taken the
CLI returns E_SLUG_RESERVED (legacy alias E_SLUG_TAKEN) with 3
alternatives — pick one, do not overwrite.
- The owner ID (
T1234 above) auto-classifies the attachment by prefix:
T### → task, ses_* → session, O-* → observation.
Strict flag validation (T10359 · closes T10238)
cleo docs add rejects unknown flags with E_UNKNOWN_FLAG + Levenshtein
"did you mean" suggestions and exits with code 6 (VALIDATION_ERROR).
This closes the silent-absorption footgun where citty's underlying
parseArgs({ strict: false }) accepted typo'd flags (e.g. --titel,
--title) as positional values.
$ cleo docs add T123 file.md --titel "X"
{
"success": false,
"error": {
"code": 6,
"codeName": "E_UNKNOWN_FLAG",
"message": "E_UNKNOWN_FLAG: unknown flag '--titel' for 'docs add'. Did you mean: --type, --slug?",
"fix": "Try one of: --type, --slug. Run `cleo docs add --help` for the full flag list.",
"alternatives": [{ "action": "--type", "command": "--type" }, { "action": "--slug", "command": "--slug" }],
"details": { "flag": "--titel", "knownFlags": [...] }
},
"meta": { ... }
}
The accepted positional + named surface is enumerated in
cleo docs add --help — agents MUST consult --help rather than guessing
flag names. Use the --flag=value form (--type=spec) or
--flag value (--type spec) — both are recognised.
Slug allocation goes through ONE chokepoint (T10392 · Saga T10288)
Every code path that writes an attachment with a slug — cleo docs add,
cleo changeset add, and any future writer — MUST first call the central
allocator at packages/core/src/docs/slug-allocator.ts:reserveSlug BEFORE
invoking attachmentStore.put({ slug }). The allocator:
- Normalises the slug to canonical kebab-case (lowercase, trim, single
hyphens).
- Acquires a per-slug in-process Mutex so concurrent reservations
serialise.
- Returns
{ ok: false, code: 'E_SLUG_RESERVED', suggestions } when the
slug is taken — uniform shape across both writers.
The attachmentStore.put chokepoint enforces this via a runtime assert
(SlugNotReservedByAllocatorError) when CLEO_STRICT_SLUG_ALLOCATOR=1
is set. Strict mode becomes default in the next release after both
writers are wired. BOTH writers are LIVE on the allocator as of T10388:
cleo docs add (T10386 — packages/cleo/src/dispatch/domains/docs.ts:add)
and cleo changeset add (T10388 — packages/core/src/changesets/writer.ts:writeChangesetEntry)
both call reserveSlug(kind, slug) BEFORE any filesystem or DB mutation.
Collisions surface the uniform envelope:
{
"success": false,
"error": {
"code": "E_SLUG_RESERVED",
"message": "slug 'foo' is already in use in this project",
"details": {
"suggestions": ["foo-2", "foo-3", "foo-4"],
"aliases": ["E_SLUG_TAKEN"]
}
}
}
details.aliases retains the legacy E_SLUG_TAKEN (docs-add path) /
E_SSOT_WRITE_FAILED (changeset-add path) codes for ONE release of
back-compat — downstream consumers grepping for the old codes can still match
via the alias array. Removed after E2 (T10290 — DocKind writer dedup) lands.
Slugs share a GLOBAL namespace across all DocKinds — reserveSlug('changeset', 'foo') followed by reserveSlug('research', 'foo') collides. The decision
is recorded in ADR-076 §6 amendment AMD-001 (slug
adr-076-canonical-docs-ssot, T10390 / E1.5). Three-point evidence (human-memorable
global lookup, DocKind-distinct prefix conventions, backward-compat cost) and
the full counterfactual analysis live in that amendment. Matches the
uniq_attachments_slug partial UNIQUE INDEX in migration 20260519000001.
When choosing a slug, follow the DocKind prefix conventions in the routing
matrix above (adr-NNN-*, spec-*, research-*, t<id>-* for changesets,
etc.) — this is what makes cross-kind collisions structurally near-impossible.
One writer per DocKind — WriterRegistry SSoT (T10366 · Saga T10288 / Epic T10290)
packages/core/src/docs/writer-registry.ts:WriterRegistry is the SSoT for
"which CLI verb writes which DocKind". Every BuiltinDocKind maps to EXACTLY
ONE writer descriptor — multi-writer regressions trip the registry at build
time (the slug-collision class root-cause from T10294).
Decision tree — pick the verb without guessing:
if kind === 'changeset' → cleo changeset add
elif kind === 'llm-readme' → tooling-composed (system-managed)
elif kind === 'release-note' → cleo release reconcile (system-managed)
else → cleo docs add --type <kind>
In code:
import { WriterRegistry } from '@cleocode/core/internal';
const desc = WriterRegistry.for('changeset');
const verb = WriterRegistry.for(kind).verb;
Descriptor modes mirror .cleo/canon.yml's canonicalHome:
'ssot' — bytes live in the blob store; published copy is a mirror.
'ssot-first' — dual-write via a dedicated verb (changeset today).
'system-managed' — tooling-composed (e.g. llm-readme, release-note).
Test parity: every ssot-first descriptor MUST match a kind whose
canonicalHome: 'ssot-first' in .cleo/canon.yml, and vice versa.
Legitimate bypass — WriterRegistry.systemManaged (T10368): writers
for system-managed kinds (e.g. generateDocsLlmsTxt, composeReleaseNotes)
do not go through cleo docs add because the bytes are tooling-composed
rather than human-authored. The registry still names the producer via
coreFn, so drift tooling can follow the breadcrumb from kind →
core function without an exception list. Do NOT invent NEW system-managed
entries to dodge the docs-add path — every kind authored by a human MUST
flow through cleo docs add or cleo changeset add.
Cross-link to the allocator contract: the registry calls
{@link reserveSlug} BEFORE the writer delegates. A taken slug returns
{ ok: false, code: 'E_SLUG_RESERVED', details: { suggestions } }
with the SAME shape both cleo docs add and cleo changeset add surface
to callers (closing the T10294 envelope-drift bug). See the
"Slug allocation goes through ONE chokepoint" section above for the
allocator contract and the legacy E_SLUG_TAKEN alias.
T10366 establishes the registry contract; T10367 (docs add) and T10368
(changeset add) wire the actual writer delegation. Until those land,
WriterRegistry.write() returns E_NOT_IMPLEMENTED after consulting the
allocator — callers should continue invoking the existing writers
(cleo docs add dispatch handler, writeChangesetEntry) directly.
T10367 LIVE — cleo docs add --type changeset delegates to
writeChangesetEntry. The dispatch handler in
packages/cleo/src/dispatch/domains/docs.ts now branches on
payload.type === 'changeset' and routes the call through the
canonical dual-write transaction. This eliminates the second writer
for the changeset DocKind (the SG-DOCS-INTEGRITY invariant) — the
bytes that land on .changeset/<slug>.md AND the SSoT blob are
byte-identical regardless of which verb the operator invoked. The
cleo changeset add CLI remains the friendlier authoring surface
(it prompts for every required frontmatter field) while
cleo docs add --type changeset --file <path> works for agents that
already have a fully-formed changeset markdown blob in hand.
Contract for the docs-add path:
- The input file MUST carry valid changeset frontmatter
(
id, tasks, kind, summary). Missing → E_REQUIRES_CHANGESET_VERB
with a fix hint pointing at cleo changeset add for guided authoring.
- When
--slug is also passed it MUST match the frontmatter id
(the frontmatter is canonical) — divergence → E_SLUG_MISMATCH.
- The LAFS envelope on success carries
data.type === 'changeset',
data.slug, data.attachmentId, and data.sha256 — round-trip
identical to what cleo changeset add emits.
CI gate — DocKind Writer Uniqueness (T10369)
scripts/lint-dockind-writer-uniqueness.mjs (CI job:
DocKind Writer Uniqueness (T10369)) enforces the
WriterRegistry invariants at PR-time. It refuses to merge a PR that:
- Adds a new entry to
BUILTIN_DOC_KINDS (in
packages/contracts/src/docs-taxonomy.ts) without a matching
descriptor in writer-registry.ts (dockind-coverage-missing).
- Declares more than one descriptor for the same DocKind
(
dockind-coverage-collision — the registry itself throws at module
load too, this gate surfaces it earlier in CI).
- Has a
mode: 'ssot-first' descriptor that does NOT match
.cleo/canon.yml's canonicalHome for the same kind, or vice versa
(canon-yml-ssot-first-drift).
- Adds a NEW raw
writeFileSync(path.md, …) / writeFile(path.md, …)
call inside packages/core/src/** that is not in
.lint-dockind-writer-baseline.json (unregistered-md-write).
Schema-parity rules (#1-#3) are ALWAYS strict — there is no baseline.
The unregistered-md-write rule runs in baseline mode by default; count
decreases always pass, count increases fail. The two legitimate
non-DocKind .md writers (packages/core/src/sessions/handoff-markdown.ts
for session snapshots and packages/core/src/changesets/writer.ts for
the canonical changeset DocKind) are allowlisted in the script.
Per-line opt-out (use sparingly): append
// dockind-writer-allowed: <reason> on the writeFile line.
Audit complement — manual-write sweep (T10372)
scripts/sweep-manual-doc-writes.mjs (CI job:
Manual Write Sweep (T10372)) is the read-only audit counterpart to
the writer-uniqueness lint. Where T10369 prevents new raw .md
writers from landing in packages/core/src/**, this sweep walks every
*.md file already added under .cleo/canon.yml's rawMdPaths
directories since the T9791 docs-import cutoff (commit 251814e86)
and classifies each one against the docs SSoT:
| Remediation | Meaning | Fix |
|---|
in-sync | File SHA matches a blob already in the SSoT — bytes are tracked. | None. |
drift | Slug exists in SSoT but the on-disk content has changed. | Re-publish via cleo docs publish or re-add as a new version. |
orphan | Neither SHA nor slug resolves — the file is a raw fs write that bypassed cleo docs add. | Migrate via cleo docs add <ownerId> <file> --type <kind> --slug <slug>. |
deleted | File was added since the cutoff but no longer exists on disk. | Informational only — does not count toward unresolved. |
Each run writes a timestamped report to
audit/manual-write-sweep-<date>.json and prints the summary block to
stdout. The CI job uploads the report as a workflow artefact on every
run and is wired with continue-on-error: true initially so the
existing orphan corpus does not break PRs. Saga T10288 / Epic T10293
E5.3 closes the orphan migration; the gate flips strict after that
lands.
node scripts/sweep-manual-doc-writes.mjs
node scripts/sweep-manual-doc-writes.mjs \
--cleo-bin "node packages/cleo/dist/cli/index.js" \
--allow-unresolved
Exit codes: 0 (clean OR --allow-unresolved), 1 (at least one
orphan or drift entry), 2 (canon.yml parse failure, git not
available, SSoT query failed).
T10179 + T10203 manual-write migration (T10371)
Saga T10176's two known raw-write workarounds are normalised:
| Original file | SSoT slug | Type | Notes |
|---|
docs/research/t10179-executor-probe-result.md | t10179-executor-probe | research | in-sync via earlier T9791 import — verified by SHA. |
.changeset/t10179-executor-probe.md (consumed v5.108) | t10179-changeset-archive | note | bytes preserved verbatim from git cc48ca10e; archived because the pnpm/changesets "@cleocode/cleo": patch frontmatter does not satisfy the changeset DocKind schema. |
.changeset/t10203-napi-step-exports.md (consumed v5.108) | t10203-napi-step-exports | changeset | in-sync via the cleo changeset add dual-write at PR-time. |
Round-trip parity is regression-locked by
packages/core/src/docs/__tests__/manual-write-migration.test.ts. The
test embeds the canonical bytes inline and asserts that
createAttachmentStore().put(...) → findBySlug(...) returns the same
SHA-256 it started with. Any future migration that silently rewrites or
recompresses these blobs fails the test.
Sweep-driven remediation loop (T10373)
T10371 only covers the known manual-write set declared in the original
Saga T10176 disposition. The T10372 sweep surfaces every orphan
remaining under rawMdPaths at the moment it runs. T10373 closes the
loop by consuming the sweep report and migrating each orphan into the
SSoT using the same cleo docs add --slug pattern T10371 established.
The recurring pattern (use this any time the sweep flags fresh
orphans):
- Run the sweep:
node scripts/sweep-manual-doc-writes.mjs. The
report lands at audit/manual-write-sweep-<date>.json.
- For each
orphan entry, derive the migration tuple:
--type from the file's parent directory (.cleo/adrs/ → adr,
.cleo/research/ → research, .cleo/agent-outputs/ →
handoff or note based on content, .cleo/rcasd/ → rcasd).
--slug from the filename — lowercase, kebab-case, no
extension (e.g. ADR-085-cross-db-invariants.md →
adr-085-cross-db-invariants).
<owner-id> from the file's frontmatter task: field if
present, otherwise from cleo find "<filename-keyword>".
- Run
cleo docs add <ownerId> <file> --type <kind> --slug <slug> --desc "<sweep-remediation context>". The --desc should
reference the originating task ID so future operators can trace
the migration.
- Verify via
cleo docs fetch <slug> and re-run the sweep — the
orphan count MUST drop by the number of files migrated.
- Add the new (slug, sha256, type, ownerId) row to a
round-trip parity test alongside the T10371 set. The canonical
example lives at
packages/core/src/docs/__tests__/sweep-remediation.test.ts.
T10373 migrated five orphans this way: ADR-083, ADR-085,
T10268-saga-closeout, t10292-e4-cli-verb-matrix, and
t10292-e4-sdk-import-edges. The last two were direct fallout from
the pre-T10389 worktree-unreachable bug — T10353 and T10354 workers
fell back to raw filesystem writes because cleo docs add rejected
inside their spawned worktrees. Re-publishing the bytes via the SSoT
proves the round-trip and closes the loop the bug opened.
If a sweep run surfaces a file that should genuinely stay as raw
markdown (e.g. an audit log not meant for SSoT propagation), add an
entry to audit/sweep-exemptions.yml rather than migrating it. The
sweep script honours exemptions and does not flag them as orphans.
Stuck-saga closure via cleo saga reconcile (T10374 · Saga T10288 / Epic T10293)
When a Saga's docs-related closeout was completed under the saga's
member Epics — every Epic flipped to status='done' — but the parent
Saga row itself is still pending, the recovery verb is
cleo saga reconcile <sagaId>. This is the cron-safe T10121 path
that the ADR-076 / T10113 auto-close path delivers; sagas that pre-date
the auto-close path (T9625 is the canonical example) need an explicit
nudge.
The recipe — use this any time a docs-canon Saga is observably stuck
even though its members have all shipped via cleo docs add /
cleo docs publish:
- Verify member-Epic terminality:
for E in <memberIds>; do cleo show $E | jq '.data.task.status'; done.
Every member must be done, cancelled, or archived before
reconcile will close the parent. If any member is genuinely stuck,
close THAT one first (evidence-based per ADR-051) — do NOT cancel
a member just to satisfy the gate.
- Verify the SSoT fetch-gate the Saga's acceptance gates on (typically
a research plan or closure note):
cleo docs fetch <slug> — must return success: true with the
expected bytes.
- Reconcile:
cleo saga reconcile <sagaId>. The verb is idempotent
(re-runs return action: 'no-op') and never modifies member rows.
- Confirm:
cleo show <sagaId> — status must be done and
completedAt populated. The action is appended to
.cleo/audit/saga-reconcile.jsonl for audit.
- Write a closure-evidence handoff via
cleo docs add <taskId> <file> --type handoff --slug <saga>-closure-evidence
capturing: member statuses (table), reconcile envelope output,
sibling-saga sanity check (no cross-saga side effects), and
ADR-076 + T10113 path validation. The slug t9625-closure-evidence
is the canonical reference.
Regression coverage for this path lives at
packages/core/src/sagas/__tests__/t9625-closure.test.ts and locks
three invariants: stuck-saga closure (AC1), sibling-saga isolation
(AC2), and idempotency (AC3). Add a new case there whenever you close
another stuck docs-canon Saga so the recovery pattern stays under
test.
Docs->memory auto-emit (T9976 · regression-tested by T10375)
Every successful cleo docs add fires a fire-and-forget memory observation
into brain_observations. The CLI never blocks on this write — a BRAIN
failure cannot fail docs add — but the observation is the bridge that
makes cleo memory find '<slug>' surface attached docs.
Title shape: "Doc attached: <slug>" (or "Doc attached: <attachmentId>"
when no slug is provided). This is what the FTS index matches on, so the
slug is also a memory-discovery key — not just a docs-lookup key.
Narrative payload (the {@link DocAttachmentObservationPayload} contract):
{
"kind": "doc-attachment",
"attachmentId": "<id>",
"ownerId": "<T#### | SG-#### | …>",
"slug": "<kebab-slug>",
"type": "<docKind>",
"addedAt": "<ISO 8601 timestamp>"
}
The payload is consumed by cleo memory verify <observationId> for
round-trip checks against the docs store — see AC3 of the original T9976
suite at packages/cleo/src/dispatch/domains/__tests__/docs-memory-observation.test.ts.
Retroactive sweeps: when migrating manual Write-based docs back into
the SSoT (the T10371 + T10373 pattern), the auto-emit fires uniformly for
the kebab-case slugs the sweep uses (t<num>-<kebab>, adr-<num>-<kebab>).
Regression coverage lives at
packages/cleo/src/dispatch/domains/__tests__/docs-memory-observation-retroactive.test.ts
(T10375). Add a new case to that table whenever you discover a slug shape
not yet under test.
Anti-pattern: do NOT write a cleo memory observe manually after a
cleo docs add — the auto-emit already happened, and the duplicate
observation pollutes the FTS index. Use cleo memory backfill-docs (AC4
of T9976) only to repair attachments that pre-date the auto-emit feature
or were written outside the SSoT.
System-managed exemptions (T10368)
Not every .md write inside packages/core/src/** is a DocKind authoring
path. Release composers, RCASD migration tooling, nexus wiki generators, and
the publish-mirror copier all emit Markdown bytes from inside core/ but
SHOULD NOT route through WriterRegistry.write — they are deterministic
derived artifacts, not user-authored canonical documents.
These legitimate bypasses live in SYSTEM_MANAGED_ENTRIES inside
writer-registry.ts. Every entry cites an ADR pointer:
WriterRegistry.listSystemManaged();
const hit = WriterRegistry.isSystemManaged('.cleo/release/v2026.5.103.plan.json');
When adding a NEW .md writer inside packages/core/src/**, you MUST
either:
- Route through
WriterRegistry.write({kind, slug, payload}) (the
canonical path for DocKind authoring), OR
- Append a new entry to
SYSTEM_MANAGED_ENTRIES with an ADR pointer
ratifying the bypass.
The writer-audit.test.ts regression test fails when a new .md writer
appears without either path. T10369 (next in the epic) promotes this from a
unit test to a full CI lint gate.
Slug similarity warn (T10361 · closes T10167)
cleo docs add runs a fuzzy-match check against existing slugs for the
SAME DocKind at write-time. If the proposed --slug is close to an
existing one (default threshold: normalised Levenshtein score ≥ 0.85,
< 1.0), the CLI surfaces "did you mean: cleo docs update <slug>?"
so an agent does not fork a near-duplicate when an update is the
intent. Exact (1.0) matches fall through to the slug-collision path
(E_SLUG_TAKEN) — they are NOT covered by this check.
$ cleo docs add T123 file.md --slug cant-spec --type spec
INFO Similar to 'cantspec' (score 0.89) — did you mean: cleo docs update cantspec? Pass --allow-similar to bypass.
$ cleo docs add T123 file.md --slug cant-spec --type spec
{
"success": false,
"error": {
"code": 6,
"codeName": "E_SLUG_SIMILARITY",
"message": "Similar to 'cantspec' (score 0.89) — did you mean: cleo docs update cantspec? Pass --allow-similar to bypass.",
"fix": "Use `cleo docs update cantspec` if updating, or pass --allow-similar to add as a new doc.",
"alternatives": [
{ "action": "update 'cantspec' instead", "command": "cleo docs update cantspec" },
{ "action": "bypass the similarity check", "command": "cleo docs add T123 file.md --slug cant-spec --type spec --allow-similar" }
]
}
}
$ cleo docs add T123 file.md --slug cant-spec --type spec --allow-similar
Project-level overrides live in .cleo/canon.yml:
similarity:
warnThreshold: 0.85
mode: warn
mode: block is recommended for CI agents — it surfaces the intent
mismatch as an exit code rather than a silently-printed warning that
non-TTY callers ignore. The --allow-similar flag is ALWAYS the
escape hatch and ALWAYS logged.
Publish to a git-tracked path (when the doc must live on disk)
cleo docs publish --for T1234 --to docs/feature-x.md
Atomic tmp-then-rename. The published file ships in the next commit; the
SSoT blob remains canonical and continues to track future versions.
Fetch the doc back by slug
cleo docs fetch feature-x-overview
cleo docs versions --for T1234
Slug-based fetch is the contract used by reviewers, downstream skills, and
the docs graph — never grep the filesystem for the file you just wrote.
List + sync / versions
cleo docs list --type spec --project
cleo docs list --task T1234
cleo docs versions --for T1234
cleo docs sync --from docs/legacy.md --for T1234 --type note --slug legacy-doc
Deprecated: Direct filesystem
The legacy "write straight to .cleo/adrs/, .cleo/research/,
.cleo/agent-outputs/, or docs/ and commit" pattern is deprecated.
The drift between the working file and the docs SSoT is real: published
files go stale, types are inferred ad-hoc from path, and slug-based
retrieval becomes impossible. Migrate every doc-type write to
cleo docs add --type X --slug Y.
Worktree-Aware Routing (T10389 / ADR-068 amendment §3.1)
When running from an agent-spawned worktree (e.g. under
~/.local/share/cleo/worktrees/<hash>/<task>/), cleo docs add and
cleo changeset add automatically route the SSoT write back to the
canonical project root. The bytes land in the MAIN repo's SSoT — never
inside the worktree.
You can pass file paths relative to the worktree cwd as usual:
cd ~/.local/share/cleo/worktrees/<hash>/<task>/
cleo docs add T10389 docs/note.md --slug t10389-investigation --type note
The verbs detect a stray .cleo/tasks.db inside the worktree
(pre-T9803 leak or rogue write) and emit E_STRAY_WORKTREE_DB BEFORE
invoking the DB chokepoint, so the user gets actionable remediation
(rm -rf <worktree>/.cleo) instead of the deeper
E_WT_DB_ISOLATION_VIOLATION exception.
Suppress the routing log line with CLEO_QUIET=1 if you need clean
stderr in automation. The fix-pack closes T10353 + T10354 + T10294's
3-guard collision class (E_PATH_TRAVERSAL + E_FILE_ERROR +
E_WT_DB_ISOLATION_VIOLATION).
Core Principle: MAINTAIN, DON'T DUPLICATE
BEFORE creating ANY new file, you MUST:
1. Search for existing documentation on the topic
2. Identify the canonical location for this information
3. UPDATE the existing file instead of creating a new one
4. Only create new files when NO suitable location exists
Workflow Phases
Phase 1: Discovery (MANDATORY)
Before writing anything, discover what exists. Prefer the SSoT over Glob/Grep
when scanning canonical docs — cleo docs list returns slug + owner + type
without forcing a filesystem walk.
cleo docs list --project
cleo docs list --type {TYPE} --project
cleo docs fetch {SUSPECTED_SLUG}
Glob: pattern="docs/**/*.md"
Grep: pattern="{TOPIC_KEYWORDS}" path="docs/"
Grep: pattern="{RELATED_TERMS}" path="docs/" output_mode="files_with_matches"
Invoke /ct-docs-lookup for deeper documentation research.
Phase 2: Assess
| Question | Action |
|---|
| Does a doc file for this topic exist? | UPDATE that file |
| Is the info scattered across files? | CONSOLIDATE into canonical location |
| Is there a related doc that should include this? | ADD section to that file |
| Is this truly new with no home? | CREATE minimal new file |
Phase 3: Write/Update
Invoke /ct-docs-write for content creation.
For EXISTING files:
- Read the current content
- Identify the correct section for new info
- Add/update content IN PLACE
- Preserve existing structure
- Update any version numbers or dates
For CONSOLIDATION:
- Identify all files with related content
- Choose the canonical location
- Move content to canonical file
- Add deprecation notices to old locations
- Update cross-references
For NEW files (last resort):
- Confirm no existing location is suitable
- Follow project's doc structure conventions
- Add to appropriate docs/ subdirectory
- Update any index or TOC files
- Keep minimal - single topic focus
Phase 4: Review
Invoke /ct-docs-review for quality validation.
Checklist:
Anti-Duplication Checklist
Before completing, verify:
Task System Integration
@skills/_shared/task-system-integration.md
Task Workflow
cleo show {TASK_ID}
cleo start {TASK_ID}
cleo complete {TASK_ID}
cleo research link {TASK_ID} {RESEARCH_ID}
Subagent Protocol
@skills/_shared/subagent-protocol-base.md
Output Requirements
- MUST write documentation output to:
{{OUTPUT_DIR}}/
- MUST append ONE line to:
{{MANIFEST_PATH}}
- MUST return ONLY: "Documentation complete. Manifest appended to pipeline_manifest."
- MUST NOT return documentation content in response
Output File Format
Write to {{OUTPUT_DIR}}/:
# Documentation Update: {TITLE}
**Date**: {DATE} | **Agent**: ct-documentor | **Status**: complete
---
## Summary
{What was updated and why}
## Changes Made
### File: {path/to/file.md}
- {Change 1}
- {Change 2}
## Files NOT Created (Avoided Duplication)
- {Considered creating X but updated Y instead}
- {Found existing coverage in Z}
## Verification
- [ ] Changes don't duplicate existing content
- [ ] Cross-references updated
- [ ] Examples tested
- [ ] Style guide compliance verified via ct-docs-review
Manifest Entry
Append ONE line to {{MANIFEST_PATH}}:
{"id":"docs-{TOPIC}-{DATE}","file":"{DATE}_docs-{TOPIC}.md","title":"Documentation Update: {TITLE}","date":"{DATE}","status":"complete","agent_type":"documentation","topics":["documentation","{topic}"],"key_findings":["Updated {file} with {change}","Consolidated {topic} docs into {canonical-location}","Avoided duplication by updating existing {file}"],"actionable":false,"needs_followup":[],"linked_tasks":["{TASK_ID}"]}
Completion Requirements
See references/
Progressive disclosure — load on demand only:
references/chain-orchestration.md — when to invoke lookup/write/review, input shapes, review loop budget
references/doc-types-and-templates.md — Diátaxis grid plus CLEO-native (ADR, agent-output, skill) templates
references/style-coordination.md — tone pillars, forbidden phrases, link/code/table discipline
references/anti-patterns.md — twelve documentation coordination failure modes