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.
Instrucciones de origen · Vista previa de solo lectura
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.
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.
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 is insufficient.
list
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.
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.
--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.
# Typo → E_UNKNOWN_FLAG with suggestion
$ 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).
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.
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.
# Local invocation — uses the globally-installed `cleo` on PATH.
node scripts/sweep-manual-doc-writes.mjs
# CI / monorepo build — point at the just-built local CLI bundle.
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.
bytes preserved verbatim from git cc48ca10e; archived because the pnpm/changesets "@cleocode/cleo": patch frontmatter does not satisfy the changeset DocKind schema.
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.
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", // discriminator
"attachmentId": "<id>", // assigned by the docs store
"ownerId": "<T#### | SG-#### | …>",
"slug": "<kebab-slug>", // omitted only when --slug not passed
"type": "<docKind>", // omitted only when --type not passed
"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:
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.
# Near-duplicate slug — warn mode is the project default.
$ 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.
# (write proceeds because mode=warn)
# Same input under `mode: block` (configured in .cleo/canon.yml) — exits 6.
$ 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" }
]
}
}
# Intentional add (true near-twin, not an update) — bypass + audit-log.
$ cleo docs add T123 file.md --slug cant-spec --type spec --allow-similar
# appends one JSONL line to .cleo/audit/similar-bypass.jsonl
Project-level overrides live in .cleo/canon.yml:
similarity:
warnThreshold: 0.85 # 0..1 — score above this triggers the hint
mode: warn # 'warn' (default) | 'block'
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)
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 # latest version
cleo docs versions --for T1234 # list every SHA version
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 # canonical: every spec in this project
cleo docs list --task T1234 # canonical: everything attached to a task
cleo docs versions --for T1234 # ADVANCED: list every SHA version
cleo docs sync --from docs/legacy.md --for T1234 --type note --slug legacy-doc # ADVANCED: back-fill on-disk file into SSoT
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.
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:
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.
# SSoT-first discovery (preferred — canonical six-verb path)
cleo docs list --project # all docs for this project
cleo docs list --type {TYPE} --project # docs filtered by canonical type
cleo docs fetch {SUSPECTED_SLUG} # check if a slug exists
# Filesystem fallback (only for un-migrated content)
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:
No formal language ("utilize", "offerings", "cannot")
"People/companies" not "users"
No excessive exclamation points
Important information leads, not buried
No verbose text without value
Headings state the point
Descriptive link text (never "here")
No "easy" or "simple"
Code examples actually work
Anti-Duplication Checklist
Before completing, verify:
Searched for existing docs on this topic via cleo docs list first
Did NOT create a file that duplicates existing content
Updated existing file if one existed
Added deprecation notice if consolidating
Cross-references are updated
No orphaned documentation created
Every canonical-type write went through cleo docs add --type X --slug Y
Slug retrieval verified via cleo docs fetch <slug> before declaring done
Task System Integration
@skills/_shared/task-system-integration.md
Task Workflow
# 1. Read task details
cleo show {TASK_ID}
# 2. Start task
cleo start {TASK_ID}
# 3. Execute documentation workflow (phases 1-4)
# 4. Complete task when done
cleo complete {TASK_ID}
# 5. Link research if applicable
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
Discovery phase completed (searched existing docs via cleo docs list)
Core principle followed (maintain, don't duplicate)
Coordinator pattern followed: every doc-type routed to its owning skill
Every canonical-type write went through cleo docs add --type X --slug Y
/ct-docs-write invoked for note/llm-readme content
/ct-spec-writer invoked when writing specs (REQ-XXX requirements)
/ct-adr-recorder invoked when promoting consensus → ADR
/ct-docs-review invoked for quality validation
Anti-duplication checklist verified
Output file written with "Files NOT Created" section
Manifest entry appended
Task completed via cleo complete
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