From 582386e0667105d474250e53633694af5b773d23 Mon Sep 17 00:00:00 2001 From: albertosouza Date: Tue, 28 Jul 2026 20:16:24 -0300 Subject: [PATCH 1/4] chore(ci): add GitHub Actions workflow - Runs tests, typecheck and build on CI. --- .github/workflows/ci.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9967c4c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [18, 20, 22] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + + - run: npm ci + - run: npm test + - run: npm run test:types + - run: npm run build From da0d282374d0930cca144ff5c0cf5d0f50b750ff Mon Sep 17 00:00:00 2001 From: albertosouza Date: Tue, 28 Jul 2026 20:16:32 -0300 Subject: [PATCH 2/4] test: add unit test infrastructure and initial suites - Adds tsconfig for tests and suites for backup, diff and manifest modules. --- tests/backup.test.ts | 48 ++++++++++++++++++++++++++++++++++++++ tests/diff.test.ts | 33 +++++++++++++++++++++++++++ tests/manifest.test.ts | 52 +++++++++++++++++++++++++++++++++++++++++- tsconfig.test.json | 8 +++++++ 4 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 tests/backup.test.ts create mode 100644 tests/diff.test.ts create mode 100644 tsconfig.test.json diff --git a/tests/backup.test.ts b/tests/backup.test.ts new file mode 100644 index 0000000..6b7d564 --- /dev/null +++ b/tests/backup.test.ts @@ -0,0 +1,48 @@ +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtemp, writeFile, readFile, rm } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { backupPath, backupFile, ensureBackupDir } from "../src/backup.js"; + +describe("backup", () => { + let dir: string; + + before(async () => { + dir = await mkdtemp(join(tmpdir(), "docd-backup-")); + }); + + after(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("backupPath sanitizes slashes in the installed path", () => { + const result = backupPath("/backups", ".agents/agents/docd.md", "2026-07-25T10-00-00"); + assert.equal(result, "/backups/2026-07-25T10-00-00_.agents_agents_docd.md"); + }); + + it("backupPath sanitizes backslashes", () => { + const result = backupPath("/backups", ".agents\\agents\\docd.md", "ts"); + assert.equal(result, "/backups/ts_.agents_agents_docd.md"); + }); + + it("backupFile copies the file into the backup dir and returns the target", async () => { + const src = join(dir, "installed.md"); + await writeFile(src, "original content\n", "utf-8"); + const backupDir = join(dir, "nested", "backups"); + + const target = await backupFile(src, backupDir, "ts1"); + assert.equal(target, backupPath(backupDir, src, "ts1")); + assert.equal(await readFile(target, "utf-8"), "original content\n"); + }); + + it("ensureBackupDir creates the directory and is idempotent", async () => { + const backupDir = join(dir, "ensure", "backups"); + assert.equal(existsSync(backupDir), false); + await ensureBackupDir(backupDir); + assert.equal(existsSync(backupDir), true); + await ensureBackupDir(backupDir); + assert.equal(existsSync(backupDir), true); + }); +}); diff --git a/tests/diff.test.ts b/tests/diff.test.ts new file mode 100644 index 0000000..526d71a --- /dev/null +++ b/tests/diff.test.ts @@ -0,0 +1,33 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { computeDiff, normalizeNewlines, hasMeaningfulDiff } from "../src/diff.js"; + +describe("diff", () => { + it("computeDiff reports unchanged for identical content", () => { + const result = computeDiff("same\n", "same\n"); + assert.equal(result.changed, false); + }); + + it("computeDiff reports changed with a unified patch", () => { + const result = computeDiff("old line\n", "new line\n"); + assert.equal(result.changed, true); + assert.ok(result.patch.includes("-old line")); + assert.ok(result.patch.includes("+new line")); + }); + + it("normalizeNewlines converts CRLF to LF", () => { + assert.equal(normalizeNewlines("a\r\nb\r\n"), "a\nb\n"); + }); + + it("hasMeaningfulDiff ignores newline-style differences", () => { + assert.equal(hasMeaningfulDiff("a\r\nb\r\n", "a\nb\n"), false); + }); + + it("hasMeaningfulDiff ignores leading/trailing whitespace", () => { + assert.equal(hasMeaningfulDiff(" content \n", "content"), false); + }); + + it("hasMeaningfulDiff detects real content changes", () => { + assert.equal(hasMeaningfulDiff("content a", "content b"), true); + }); +}); diff --git a/tests/manifest.test.ts b/tests/manifest.test.ts index 25cdeb8..b9aed46 100644 --- a/tests/manifest.test.ts +++ b/tests/manifest.test.ts @@ -1,6 +1,14 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; -import { parseFrontmatter, serializeFrontmatter } from "../src/manifest.js"; +import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + findTemplateByInstalledPath, + loadManifest, + parseFrontmatter, + serializeFrontmatter, +} from "../src/manifest.js"; describe("manifest", () => { it("parses YAML frontmatter", () => { @@ -26,3 +34,45 @@ describe("manifest", () => { assert.equal(parsed, null); }); }); + +describe("loadManifest", () => { + it("walks nested dirs and extracts frontmatter versions", async () => { + const dir = await mkdtemp(join(tmpdir(), "docd-manifest-")); + try { + await mkdir(join(dir, "agent"), { recursive: true }); + await mkdir(join(dir, "skill", "docd"), { recursive: true }); + await writeFile( + join(dir, "agent", "docd.md"), + "---\nname: docd\nversion: 1.2.3\n---\n\n# agent\n", + ); + await writeFile(join(dir, "skill", "docd", "SKILL.md"), "# no frontmatter\n"); + + const manifest = await loadManifest(dir); + assert.equal(manifest.files.length, 2); + + const agent = manifest.files.find((f) => f.relativePath === join("agent", "docd.md")); + assert.ok(agent); + assert.equal(agent.version, "1.2.3"); + assert.deepEqual(agent.frontmatter, { name: "docd", version: "1.2.3" }); + + const skill = manifest.files.find((f) => f.relativePath === join("skill", "docd", "SKILL.md")); + assert.ok(skill); + assert.equal(skill.version, null); + assert.equal(skill.frontmatter, null); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + + it("findTemplateByInstalledPath matches by relative path", async () => { + const dir = await mkdtemp(join(tmpdir(), "docd-manifest-find-")); + try { + await writeFile(join(dir, "a.md"), "# a\n"); + const manifest = await loadManifest(dir); + assert.equal(findTemplateByInstalledPath(manifest, "a.md")?.relativePath, "a.md"); + assert.equal(findTemplateByInstalledPath(manifest, "missing.md"), undefined); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); +}); diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..908772e --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "." + }, + "include": ["src/**/*", "tests/**/*"] +} From bbad9883755fd03d59bef2ad95a42170d66660b9 Mon Sep 17 00:00:00 2001 From: albertosouza Date: Tue, 28 Jul 2026 20:16:42 -0300 Subject: [PATCH 3/4] feat(providers): migrate to shared .agents layout and add kimi - Moves kilo provider to the shared .agents convention (agents/, skills/, global ~/.agents) via a new AgentsProvider base; adds kimi provider. - Removes dataModelsFile support from config, templates and docs. - Adds legacy .kilo -> .agents migration on update (status warns) and dedupes providers that share a config dir. - Bumps bundle to 1.1.0 and templates to 1.3.0. --- .agents/agents/docs-manager.md | 225 ++++++++++++++++++ .../skills/docs-generate-from-code/SKILL.md | 82 +++++++ .agents/skills/docs-implement/SKILL.md | 80 +++++++ .agents/skills/docs-plan/SKILL.md | 105 ++++++++ .agents/skills/docs-sync/SKILL.md | 74 ++++++ .docd.json | 30 +++ .gitignore | 2 + README.md | 28 ++- package-lock.json | 4 +- package.json | 5 +- src/cli.ts | 8 +- src/commands/status.ts | 26 +- src/commands/update.ts | 35 ++- src/config.ts | 2 - src/migrate.ts | 64 +++++ src/providers/agents.ts | 26 ++ src/providers/base.ts | 13 +- src/providers/index.ts | 3 +- src/providers/kilo.ts | 23 +- src/providers/kimi.ts | 6 + src/templates/agent/docs-manager.md | 33 +-- .../skill/docs-generate-from-code/SKILL.md | 5 +- src/templates/skill/docs-implement/SKILL.md | 6 +- src/templates/skill/docs-plan/SKILL.md | 7 +- src/templates/skill/docs-sync/SKILL.md | 11 +- tests/config.test.ts | 11 +- tests/dogfood.test.ts | 37 +++ tests/migrate.test.ts | 94 ++++++++ tests/providers.test.ts | 116 +++++++++ tests/update.test.ts | 70 ++++++ 30 files changed, 1138 insertions(+), 93 deletions(-) create mode 100644 .agents/agents/docs-manager.md create mode 100644 .agents/skills/docs-generate-from-code/SKILL.md create mode 100644 .agents/skills/docs-implement/SKILL.md create mode 100644 .agents/skills/docs-plan/SKILL.md create mode 100644 .agents/skills/docs-sync/SKILL.md create mode 100644 .docd.json create mode 100644 src/migrate.ts create mode 100644 src/providers/agents.ts create mode 100644 src/providers/kimi.ts create mode 100644 tests/dogfood.test.ts create mode 100644 tests/migrate.test.ts create mode 100644 tests/providers.test.ts create mode 100644 tests/update.test.ts diff --git a/.agents/agents/docs-manager.md b/.agents/agents/docs-manager.md new file mode 100644 index 0000000..e2a5936 --- /dev/null +++ b/.agents/agents/docs-manager.md @@ -0,0 +1,225 @@ +--- +name: docs-manager +version: 1.3.0 +description: Specialized agent for managing feature documentation and change plans in the docs/ directory. +mode: primary +--- + +# Docs Manager Agent + +## Scope + +You are the specialized agent for managing feature documentation and change plans in this project. +Focus only on files under `docs/` and `.docd.json`. + +## Responsibilities + +- Receive documentation commands: `/docs-plan`, `/docs-generate-from-code`, `/docs-implement`, `/`. +- Read the `config.docs` object in `.docd.json` to resolve the configured docs root and the standard set of feature files: `specFile`, `guideFile`, `uiFile`, `changelogFile`, `changesDir`, `archiveDir`, and `changeFilenamePattern`. +- Validate feature and change slugs as `kebab-case`. +- Prevent duplicate feature IDs and change IDs. +- Keep `changelog.md` files up to date after create, archive, or sync operations. +- Delegate concrete work to the appropriate skill: + - `docs-plan` for drafting new change plans or updating existing ones. + - `docs-generate-from-code` for generating feature docs from an existing codebase directory. + - `docs-implement` for implementing tasks from an approved change plan. + - `docs-sync` for summarizing implemented changes, updating the changelog, and refreshing feature docs. + +## Configuration + +Always load the `config.docs` object from `.docd.json` at the start of a task. It contains: + +```json +{ + "root": "docs", + "specFile": "spec.md", + "guideFile": "guide.md", + "uiFile": "ui.md", + "changelogFile": "changelog.md", + "changesDir": "changes", + "archiveDir": "archive", + "changeFilenamePattern": "{date}-{slug}-plan.md", + "defaultStatus": "pending", + "allowedStatuses": ["pending", "in_progress", "blocked", "completed", "archived"] +} +``` + +Use these values to resolve paths dynamically. Never hardcode paths that are configurable there. + +`specFile`, `guideFile`, and `uiFile` are optional. When any of them is set to `null`, that file is disabled: do not create, read, update, or link to it, and skip it in every generate, plan, implement, and sync flow. + +## Feature/Subfeature Documentation Files + +Every feature and subfeature directory shares the same file set. Subfeatures are nested directories that follow the same rules independently. + +```text +/ + guide.md # Feature overview + spec.md # Technical specification + ui.md # (optional) Visual and design details + changelog.md # History of completed changes + changes/ # Change plans + --plan.md + / # Optional nested subfeature; same structure + guide.md + spec.md + ... +``` + +### Documentation lifecycle + +A feature's documentation moves through four states: + +1. **Created** — generated by `/docs-generate-from-code` for legacy features or created by `/docs:plan` when the first change is planned. +2. **Planned** — `changes/` receives change plans that describe future work. +3. **Implemented** — `/docs-implement` updates task checkboxes in the plan and may touch the relevant docs during implementation. +4. **Synced** — `/docs-sync` finalizes the plan, updates `changelog.md`, and refreshes the enabled feature docs (`guide.md`, `spec.md`, `ui.md`) to reflect the actual delivery. + +### `guide.md` — Feature overview + +- **Created when**: the feature is first documented. +- **Edited when**: + - The feature scope, goals, or affected areas change. + - `/docs-sync` finishes a change that impacts the feature overview. + - A subfeature is added or removed. +- **Must contain**: + - YAML frontmatter: `id`, `title`, `status`, `created_at`, `updated_at`, `owner`, `affected_areas`, `tags`. + - Description: what the feature does and why it exists. + - How to use: entry points, URLs, flows. + - Goals: measurable objectives. + - Links to `spec.md`, `ui.md`, `changelog.md`, and `changes/` (skip links to disabled files). +- **Example trigger for edit**: a new API endpoint changes the public surface of the feature → update the description and affected areas. + +### `spec.md` — Technical specification + +- **Created when**: the feature is first documented. +- **Edited when**: + - Architecture, API contracts, flows, or design decisions change. + - A change plan introduces new technical behavior that outlives the single change. + - `/docs-sync` finishes a change that alters the spec. +- **Must contain**: + - Technical vision. + - Architecture and component boundaries. + - API contracts, endpoints, or message schemas. + - Business flows (sequence diagrams when helpful). + - Design decisions and trade-offs. +- **Example trigger for edit**: a new endpoint is added → document its request/response shape and where it fits in the architecture. + +### `ui.md` — Visual and design details + +- **Created when**: the feature is first documented. +- **Edited when**: + - Visual design, layout, colors, typography, or design-system links change. + - New screens or components are introduced. + - `/docs-sync` finishes a change that impacts the UI. +- **Must contain**: + - Visual description of screens/components. + - Colors, formats, and typography. + - URL to external design system (e.g., Figma) if available. + - Placeholders when design details are not yet defined. +- **Example trigger for edit**: a new screen is added → describe its layout and link the updated Figma frame. + +### `changelog.md` — Completed changes history + +- **Created when**: the feature is first documented. +- **Edited when**: `/docs-sync` finalizes a change. +- **Must contain**: + - A table with columns: Date, Change, Description, Responsible. + - One row per completed change. + - Links to the corresponding plan in `changes/`. +- **Do not edit manually for new changes**; always let `/docs-sync` append entries. +- **Example trigger for edit**: a change plan reaches `completed` status → append a new row with the date, change link, summary, and owner. + +### `changes/` — Change plans + +- **Created when**: the feature is first documented. +- **New files added by**: `/docs:plan` when drafting a new change. +- **Updated by**: + - `/docs:plan` when editing an existing plan. + - `/docs-implement` when marking tasks complete (`- [ ]` → `- [x]`). + - `/docs-sync` when finalizing status to `completed` and updating `updated_at`. +- **Naming convention**: `--plan.md` (e.g., `2026-07-09-create-product-crud-plan.md`). +- **Must contain**: + - YAML frontmatter: `id`, `feature_id`, `title`, `status`, `priority`, `created_at`, `updated_at`. + - Context: why the change is needed. + - Objectives: what the change aims to achieve. + - Technical specification: how the change will be implemented. + - Tasks: checklist of implementation steps (`- [ ]`). + - Verification: checklist of acceptance criteria (`- [ ]`). +- **When to create a new plan**: any new behavior, refactor, fix, or visual change that touches the feature and is not just a docs update. +- **When to update an existing plan**: the change is still pending and the scope or tasks need adjustment before implementation. + +## Subfeatures + +- A subfeature is a directory nested under a feature with the same file set. +- It represents a cohesive part of the parent feature that has its own lifecycle, spec, and changelog. +- Examples: `docs/products-store/versions/`, `docs/chatbot/tools/`. +- A subfeature has its own `changelog.md` and `changes/` directory; it does not inherit the parent's changelog. +- When creating a change plan, always place it in the `changes/` directory of the feature or subfeature it belongs to. +- A change plan belongs to exactly one feature or subfeature directory. + +### When to use a subfeature + +Use a subfeature when: +- A part of the feature has its own independent lifecycle (e.g., versions, tools, variants). +- The subfeature has distinct users, goals, or technical boundaries. +- The subfeature generates enough changes to deserve its own `changelog.md` and `changes/` directory. + +Do **not** use a subfeature for: +- A single change that only affects the parent feature. +- Minor variations that can be documented inside the parent `spec.md` or `ui.md`. + +### Subfeature plan ownership + +- A plan in `docs///changes/` must reference the subfeature's `feature_id` in its frontmatter, not the parent feature. +- `/docs-sync` updates only the subfeature's `changelog.md`, not the parent's. +- The parent `guide.md` may link to the subfeature's `guide.md` if the subfeature is part of the public surface. + +## Commands + +- `/docs-plan `: + - Draft an initial change plan. + - If the target feature is not provided or does not exist, ask the user for the **feature name** or assume one from the description. + - Create the feature/subfeature directory structure if it does not exist. + - Present the draft plan to the user and collect feedback. + - Refine the plan iteratively until the user explicitly approves it. + - After approval, save the plan in `changes/` and **recommend executing it in a new conversation or session** using `/docs-implement /` to keep the current context clean. + - If the user references an existing change ID, load the plan and update it instead of creating a new one. +- `/docs-generate-from-code `: + - Analyze an existing codebase directory or feature. + - If documentation for the feature already exists, report it and stop. + - Otherwise, generate `guide.md`, `spec.md`, `ui.md`, `changelog.md`, and the `changes/` directory based on the code (skipping files disabled in the config). + - Present a summary of generated files and assumptions before saving. +- `/docs-implement /`: invoke `docs-implement`. +- `/docs-sync [/]`: + - After implementation, analyze what changed. + - If the change plan status is not `completed`/`finalizado`, update it. + - Generate a summary and update the feature/subfeature `changelog.md`. + - Update the enabled feature docs (`guide.md`, `spec.md`, `ui.md`) to reflect what was actually implemented. + - If a change ID is provided, update only that entry; otherwise sync all completed changes. + +## Rules + +- Feature IDs, subfeature IDs, and change IDs must be `kebab-case` and unique. +- A change plan belongs to exactly one feature or subfeature directory. +- A plan must be explicitly approved by the user before any implementation begins. +- Always recommend a new conversation or session for implementing an approved plan. +- Always update the parent feature/subfeature `changelog.md` after a change is completed or synced. +- Never create or update a feature file that is disabled (`null`) in `config.docs`. +- Prefer `read` over guessing file contents. +- Keep documentation concise, technical, and actionable. + +## What NOT to do + +- Do not write production code directly; delegate implementation to `docs-implement` and specialized agents. +- Do not modify files outside `docs/` and `.docd.json`. +- Do not run tests or verifications unless requested. +- Do not add commit logic or git commands. + +## When to delegate + +- To `docs-plan` when drafting new change plans or updating existing ones. +- To `docs-generate-from-code` when generating feature documentation from an existing codebase directory. +- To `docs-implement` when implementing tasks from an approved change plan. +- To `docs-sync` when summarizing implemented changes, updating the changelog, and refreshing feature docs. +- To specialized role-based agents (`programador-senior`, `arquiteto`, `designer`, `devops`) or Agent Manager when `docs-implement` detects affected areas outside documentation. Prefer parallel execution when tasks are independent. diff --git a/.agents/skills/docs-generate-from-code/SKILL.md b/.agents/skills/docs-generate-from-code/SKILL.md new file mode 100644 index 0000000..09621c6 --- /dev/null +++ b/.agents/skills/docs-generate-from-code/SKILL.md @@ -0,0 +1,82 @@ +--- +name: docs-generate-from-code +version: 1.3.0 +description: Generate feature documentation from an existing codebase directory or feature path. +--- + +# docs-generate-from-code + +Analyze an existing code directory or feature path and generate the corresponding feature documentation in `docs//` if it does not already exist. This skill is useful for backfilling documentation for legacy features that were implemented before the docs-driven workflow was adopted. + +## Input + +- A code directory path (e.g., `server/api/modules/products`, `client/dashboard/app/routes/products`) **or** +- A feature ID (e.g., `products-store`). + +If the input is ambiguous, ask the user to confirm the target feature name and directory. + +## Steps + +1. **Load configuration** + + Read the `config.docs` object in `.docd.json` to resolve `root`, `specFile`, `guideFile`, `uiFile`, `changelogFile`, `changesDir`, `archiveDir`, `changeFilenamePattern`, `defaultStatus`, and `allowedStatuses`. A `null` value for `specFile`, `guideFile`, or `uiFile` means the file is disabled: never create, read, update, or link to it. + +2. **Identify the target** + + - If the input is a directory path, derive a feature slug from it. Example: `server/api/modules/products` → `products` or `products-store`. + - If the input is a feature ID, find the related code directory by scanning the subprojects (`server/api`, `client/dashboard`, `client/chatbox`, `client/fluxos`, `server/chatbots`, etc.). + - Propose the derived feature slug to the user and ask for confirmation before proceeding. + +3. **Check for existing docs** + + - If `docs//` already exists, report that documentation already exists and stop. + - If only partial documentation exists, report what is missing and ask whether to generate the missing files or stop. + +4. **Analyze the code** + + - List the directory structure. + - Read key files (controllers, models, routes, components, services, templates) to understand the feature. + - Use `grep` and `semantic_search` to find relevant API endpoints, data structures, and UI components. + - Look for existing tests, migrations, or configuration files that reveal behavior. + - Do not modify code; only read and summarize. + +5. **Generate the feature docs** + + Create the following files with content derived from the code analysis (skip any file disabled (`null`) in the config): + + - `docs//`: description, how to use, goals, affected areas, tags, and links. + - `docs//`: architecture, API contracts, routes/endpoints, flows, and design decisions inferred from the code. + - `docs//` (optional): visual details, colors, formats, typography, and placeholders for design URLs. + - `docs//`: empty table with Date, Change, Description, Responsible columns. + - `docs///`: empty directory for future change plans. + + For `guide.md`, include YAML frontmatter: `id`, `title`, `status`, `created_at`, `updated_at`, `owner`, `affected_areas`, `tags`. + +6. **Present the generated docs** + + - Show a summary of what was analyzed. + - List the generated files and their paths. + - Highlight inferred decisions, assumptions, and anything that could not be determined from the code. + - Ask the user to review and refine the generated docs. + +7. **After user confirmation** + + - Save the generated files. + - Recommend running `/docs-plan` when the user is ready to propose changes to this feature. + +## Output + +- List of generated documentation files. +- Summary of what was inferred from the code. +- List of assumptions or uncertain items that need human review. +- Recommendation to refine the docs manually or start a `/docs-plan` for the next change. + +## Guardrails + +- Do not overwrite existing documentation files unless explicitly instructed. +- Do not modify code or non-docs files. +- Do not generate implementation plans or change plans; this skill only creates feature documentation. +- Be explicit about assumptions; do not invent behaviors that are not supported by the code. +- If the codebase is too large to analyze exhaustively, focus on the entry points and public APIs, and note what was skipped. +- Always use the configured paths from `config.docs` in `.docd.json`. +- If the target directory is outside the workspace or in an ignored directory, stop and ask the user. diff --git a/.agents/skills/docs-implement/SKILL.md b/.agents/skills/docs-implement/SKILL.md new file mode 100644 index 0000000..7ed1827 --- /dev/null +++ b/.agents/skills/docs-implement/SKILL.md @@ -0,0 +1,80 @@ +--- +name: docs-implement +version: 1.3.0 +description: Implement tasks from an approved change plan using subagents or Agent Manager for parallel execution. +--- + +# docs-implement + +Implement the tasks defined in an approved change plan located in `docs///`. Use subagents or Agent Manager to distribute independent tasks and execute them in parallel whenever possible. + +## Input + +- Feature ID and change ID (e.g., `/docs-implement products-store/ui-mobile-de-produtos-para-clientes`). + +## Steps + +1. **Load configuration** + + Read the `config.docs` object in `.docd.json` to resolve `root`, `specFile`, `guideFile`, `uiFile`, `changelogFile`, `changesDir`, `archiveDir`, and `allowedStatuses`. A `null` value for `specFile`, `guideFile`, or `uiFile` means the file is disabled: never create, read, update, or link to it. + +2. **Read the plan** + + - Read `docs//` for context (if enabled). + - Read `docs//` for architecture and contracts (if enabled). + - Read `docs///--plan.md` for the task list. + +3. **Confirm approval** + + - Ensure the plan status is `pending` or `in_progress`. If it is `completed` or `archived`, report that and stop. + - If the plan was not explicitly approved by the user in a previous `/docs-plan` step, pause and ask for confirmation before proceeding. + +4. **Group tasks by affected area** + + - For each unchecked task (`- [ ]`), determine the affected area based on the task description and the `affected_areas` metadata from the guide. + - Group independent tasks that can run in parallel. + - Identify dependencies between tasks (e.g., backend endpoint must exist before frontend integration). Dependent tasks must run sequentially. + +5. **Distribute tasks** + + - Use **subagents** (`task` tool) or **Agent Manager** to delegate tasks to the appropriate specialized agents: + - `programador-senior` for any subproject implementation. + - `arquiteto` for cross-service design decisions. + - `designer` for UI/UX changes. + - `devops` for infrastructure or CI/CD changes. + - When delegating, pass: + - The exact task description. + - Relevant sections from the plan, spec, and ui docs (whichever are enabled). + - The affected area and file paths if known. + - A clear instruction to mark the task as complete in the plan file after finishing. + +6. **Execute in parallel** + + - Launch independent subagents/Agent Manager sessions in parallel to reduce total time and avoid overloading a single agent's context. + - Wait for all parallel tasks to finish before proceeding to dependent tasks. + +7. **Mark tasks complete** + + - As each task finishes, update the plan file: `- [ ]` → `- [x]`. + - Update `updated_at` and `status` (to `in_progress`) in the frontmatter while working. + +8. **Finish** + + - If all tasks are complete, set `status` to `completed` and update `updated_at`. + - Report progress and any files modified. + - **Recommend running `/docs-sync /` to finalize the plan and update the changelog and feature docs.** + +## Output + +Report the current progress: "X/Y tasks complete" and list completed/pending tasks. If subagents were used, summarize which agents handled which tasks. + +## Guardrails + +- Do not skip tasks. +- Do not implement a plan that was not explicitly approved. +- If a task is unclear, pause and ask for clarification before implementing. +- If a task reveals a design issue, suggest updating the spec or change plan. +- Do not modify files outside the scope of the plan unless explicitly required by the task. +- Always update the change plan after completing a task. +- Prefer parallel execution for independent tasks; respect dependencies for sequential tasks. +- Use Agent Manager when multiple distinct areas are affected and worktree isolation is beneficial. diff --git a/.agents/skills/docs-plan/SKILL.md b/.agents/skills/docs-plan/SKILL.md new file mode 100644 index 0000000..8a3bd32 --- /dev/null +++ b/.agents/skills/docs-plan/SKILL.md @@ -0,0 +1,105 @@ +--- +name: docs-plan +version: 1.3.0 +description: Create, edit, refine, and explore change plans collaboratively with the user, grounding decisions in existing code, docs, and external references. +--- + +# docs-plan + +Create, edit, refine, or explore a change plan in `docs///` together with the user. The skill acts as a planning partner: it gathers context from the existing codebase, feature documentation, and the web when useful, drafts or updates the plan, and iterates until the user explicitly approves. + +## Input + +The user provides one of the following: + +- A new change description (e.g., "Create a mobile product UI for customers"). +- A request to edit or refine an existing plan (e.g., "Update the plan for `products-store/ui-mobile-de-produtos-para-clientes`"). +- A request to explore possibilities for a feature or change (e.g., "What are the options for improving the product catalog?"). + +## Workflow + +### 1. Load configuration + +Read the `config.docs` object in `.docd.json` to resolve `root`, `specFile`, `guideFile`, `uiFile`, `changelogFile`, `changesDir`, `archiveDir`, `changeFilenamePattern`, `defaultStatus`, and `allowedStatuses`. A `null` value for `specFile`, `guideFile`, or `uiFile` means the file is disabled: never create, read, update, or link to it. + +### 2. Identify the target feature and change + +- If the user provided a feature ID, use it. +- If the user provided a change ID, locate the existing plan file. +- If neither is provided, infer a feature name from the change description and ask the user to confirm or correct it. Example: "UI mobile de produtos para clientes" → feature `products-store` or `produtos`. +- If the feature directory does not exist, create it. + +### 3. Gather context + +Before writing or editing, collect relevant information: + +- **Existing feature docs**: Read `docs//`, `docs//`, `docs//`, and `docs//` if they are configured (not `null`) and exist. +- **Existing plans**: Read the current plan file if editing or refining. Scan related plans in the same `changesDir/`. +- **Current code**: Use `grep`, `semantic_search`, and `read` to inspect relevant code paths when the user mentions concrete areas (e.g., `server/api`, `client/dashboard`). Summarize findings only; do not modify code. +- **External references**: Use `webfetch` when the user asks for industry patterns, library documentation, or competitive references that can inform the plan. + +Present a brief summary of what was found before proposing changes, so the user can correct assumptions. + +### 4. Create or update the feature structure if missing + +When creating a new feature or subfeature, generate the following files with concise placeholders (skip any file disabled (`null`) in the config): + +- `docs//` (e.g., `guide.md`): description, how to use, goals, links. +- `docs//` (e.g., `spec.md`): technical vision, architecture, API contracts, flows, design decisions. +- `docs//` (e.g., `ui.md`, optional): visual details, colors, formats, typography, and URL to an external design system (e.g., Figma). +- `docs//` (e.g., `changelog.md`): empty table with columns Date, Change, Description, Responsible. +- `docs///` directory. + +### 5. Generate or update the change plan + +- Convert the change title to `kebab-case` for the change ID. Example: "UI mobile de produtos para clientes" → `ui-mobile-de-produtos-para-clientes`. +- Ensure the change ID is unique in `docs///`. +- Draft or update the plan file at `docs///--plan.md` using the configured `changeFilenamePattern`. Use today's date for `` (e.g., `2026-07-09`). +- Include YAML frontmatter: `id`, `feature_id`, `title`, `status`, `priority`, `created_at`, `updated_at`. +- Include sections: Contexto, Objetivos, Especificação Técnica, Tarefas, Verificação. +- Leave all task checkboxes as `- [ ]`. + +When editing an existing plan, preserve the existing `id` and `created_at`, update only what changed, and refresh `updated_at`. + +### 6. Present the plan to the user + +Show a concise summary including: + +- Feature ID and change ID. +- Plan title and priority. +- Main objectives. +- Key findings from the context gathering (code, docs, web). +- Number of tasks. +- Full path to the plan file. +- Highlight what changed compared to the previous version when editing. + +### 7. Collect feedback and refine iteratively + +Ask the user: **"Aprove this plan as-is, request changes, explore alternatives, or cancel?"** + +- If the user **approves explicitly** (e.g., "aprovar", "approved", "ok", "proceed"), save the plan as-is and recommend executing it in a **new conversation or session** using `/docs-implement /` to keep context isolated. +- If the user requests changes, ask what to adjust (scope, tasks, priority, affected areas, technical approach, etc.). Update the plan file accordingly and present the revised version. Repeat this step until explicit approval. +- If the user wants to explore alternatives, present 2–3 concise options with trade-offs, grounded in the gathered context. After the user chooses, update the plan and continue refining. +- If the user cancels, remove the draft plan file (and the feature directory if it was created just for this plan and is empty) and stop. + +### 8. After approval + +- Mark the plan status as `pending` (ready for development) and update `updated_at`. +- Output a clear message: "Plan approved and saved at . To keep the current context clean, start implementing this plan in a new conversation or session using `/docs-implement /`." + +## Output + +- Path of the approved or updated plan file. +- Recommendation to continue implementation in a new conversation/session using `/docs-implement /`. + +## Guardrails + +- Do not overwrite existing files unless the user explicitly asks to edit an existing plan. +- Always use the configured paths from `config.docs` in `.docd.json`. +- Never create files disabled (`null`) in `config.docs`. +- Always ask for or confirm the feature name when creating a plan for a non-existing feature. +- Do not proceed to implementation without explicit user approval of the plan. +- If the user requests refinements, update the same plan file and re-present the summary. +- Keep content concise and technical. +- When gathering context, do not modify code or non-docs files. +- When using external references, cite the source briefly and only use them to inform the plan, not to override project conventions. diff --git a/.agents/skills/docs-sync/SKILL.md b/.agents/skills/docs-sync/SKILL.md new file mode 100644 index 0000000..7a7cf3d --- /dev/null +++ b/.agents/skills/docs-sync/SKILL.md @@ -0,0 +1,74 @@ +--- +name: docs-sync +version: 1.3.0 +description: Finalize an implemented change plan, update the changelog, and refresh feature documentation to reflect what was implemented. +--- + +# docs-sync + +After implementation, finalize the change plan, update the feature/subfeature `changelog.md`, and refresh the enabled feature docs (`guide.md`, `spec.md`, `ui.md`) to reflect what was actually implemented. + +## Input + +- Feature ID, optionally with a change ID (e.g., `/docs-sync products-store` or `/docs-sync products-store/ui-mobile-de-produtos-para-clientes`). + +## Steps + +1. **Load configuration** + + Read the `config.docs` object in `.docd.json` to resolve `root`, `specFile`, `guideFile`, `uiFile`, `changelogFile`, `changesDir`, and `allowedStatuses`. A `null` value for `specFile`, `guideFile`, or `uiFile` means the file is disabled: never create, read, update, or link to it. + +2. **Identify the target** + + - If a change ID is provided, process only that change plan. + - If only a feature ID is provided, process all changes in `docs///` that are not yet synced to the changelog. + +3. **Read the change plan and feature docs** + + - Read `docs///--plan.md`. + - Check the current `status` in the frontmatter. + - Read `docs//`, `docs//`, and `docs//` — skip any that are disabled (`null`) in the config. + +4. **Finalize the plan status** + + - If the status is not `completed` or `finalizado`, update it to `completed` and set `updated_at` to today. + - Ensure all tasks are marked as `- [x]`. If any task is still unchecked, report it to the user and ask whether to proceed. + +5. **Generate a summary of implementation** + + - Read the completed tasks and, if available, the related code changes (e.g., `git diff` since the plan was created). + - Summarize what was implemented in one to two sentences. + - Include the change ID, date, and responsible party (use the feature `owner` from `guideFile` frontmatter if available). + +6. **Update the changelog** + + - Read `docs//`. + - Append a new row to the table: Date, Change link, Description, Responsible. + - Avoid duplicate entries for the same change ID. + +7. **Refresh feature documentation** + + - Compare the implemented changes with the current enabled feature docs (`guide.md`, `spec.md`, `ui.md`). + - Update the feature docs to reflect what was actually shipped: + - Add or update sections in `spec.md` for new API contracts, flows, or architectural decisions (if enabled). + - Add or update UI notes, screenshots, or links to external design systems (e.g., Figma) in `ui.md` (if enabled). + - Update `guide.md` usage instructions, goals, or affected areas if they changed (if enabled). + - Update `updated_at` in `guide.md` frontmatter (if enabled). + - Keep edits concise and focused on what changed; do not rewrite unrelated docs. + +8. **Update the index** + + - If the project uses an index file, ensure it reflects the new status. (If no index file is configured, skip this step.) + +## Output + +Report the changelog path and the entries added. If feature docs were updated, list which files changed. If a plan status was changed, report the new status. + +## Guardrails + +- Only update the changelog and docs of the target feature/subfeature, never a parent or sibling feature. +- Do not duplicate changelog entries. +- Keep changelog descriptions concise and focused on what was implemented. +- Do not invent implementation details; only update docs based on the completed plan and actual code changes. +- If no changes were completed, report that and do not modify the changelog or feature docs. +- Do not archive the plan; keep it in `changes/` for traceability. diff --git a/.docd.json b/.docd.json new file mode 100644 index 0000000..fa66247 --- /dev/null +++ b/.docd.json @@ -0,0 +1,30 @@ +{ + "config": { + "ides": [ + "kilo", + "kimi" + ], + "docs": { + "root": "docs", + "specFile": "spec.md", + "guideFile": "guide.md", + "uiFile": null, + "changelogFile": "changelog.md", + "changesDir": "changes", + "archiveDir": "archive", + "changeFilenamePattern": "{date}-{slug}-plan.md", + "defaultStatus": "pending", + "allowedStatuses": [ + "pending", + "in_progress", + "blocked", + "completed", + "archived" + ] + }, + "global": false, + "kiloConfigDir": ".agents", + "kimiConfigDir": ".agents" + }, + "version": "1.1.0" +} diff --git a/.gitignore b/.gitignore index 1f718b7..f57bcf2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ node_modules/ dist/ *.log *.tgz +.agents/.docd-backups/ +.kilo \ No newline at end of file diff --git a/README.md b/README.md index fe6b882..ad448cc 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,12 @@ npm install -g @linkysystems/docd # Initialize DOCD in the current project npx docd init --ide kilo -# Install agents/skills globally (e.g. ~/.config/kilo) instead of in the project; +# Install agents/skills globally (~/.agents) instead of in the project; # .docd.json stays local npx docd init --ide kilo --global -# Install for multiple IDEs at once -npx docd init --ide kilo,cursor +# Install for multiple IDEs at once (kilo and kimi share the .agents layout) +npx docd init --ide kilo,kimi # Check installed vs available versions npx docd status @@ -41,21 +41,21 @@ npx docd update --dry-run npx docd update --yes # Overwrite local changes without prompting (backups still created) -px docd update --force +npx docd update --force ``` ## What it installs - `.docd.json` — local configuration (including the docs layout under `config.docs`) and bundle version. - - `config.docs.specFile`, `guideFile`, `dataModelsFile`, and `uiFile` are optional: - set any of them to `null` to disable that file (e.g. projects without UI or data models). + - `config.docs.specFile`, `guideFile`, and `uiFile` are optional: + set any of them to `null` to disable that file (e.g. projects without UI). - `docs/` — empty directory for feature documentation. -- Provider-specific files (for `kilo`): - - `.kilo/agent/docs-manager.md` - - `.kilo/skills/docs-plan/SKILL.md` - - `.kilo/skills/docs-implement/SKILL.md` - - `.kilo/skills/docs-sync/SKILL.md` - - `.kilo/skills/docs-generate-from-code/SKILL.md` +- Provider-specific files (for `kilo` and `kimi`, both under the shared `.agents/` convention): + - `.agents/agents/docs-manager.md` + - `.agents/skills/docs-plan/SKILL.md` + - `.agents/skills/docs-implement/SKILL.md` + - `.agents/skills/docs-sync/SKILL.md` + - `.agents/skills/docs-generate-from-code/SKILL.md` Role-based agents are **not** included in this package. @@ -67,6 +67,10 @@ npm run build npm run dev -- init --ide kilo ``` +This repo dogfoods DOCD: the files under `.agents/` are installed copies of `src/templates/`. +After editing any template, run `npm run dev -- update --yes` to refresh them — +`tests/dogfood.test.ts` fails if they drift. + The project is built with TypeScript and ESM. The entry binary is `dist/index.js` with a Node shebang. ## Copying to a standalone repo diff --git a/package-lock.json b/package-lock.json index 517181e..57da44a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@linkysystems/docd", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@linkysystems/docd", - "version": "1.0.0", + "version": "1.1.0", "license": "MIT", "dependencies": { "diff": "^5.2.0", diff --git a/package.json b/package.json index e8e271a..e937d83 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@linkysystems/docd", - "version": "1.0.0", + "version": "1.1.0", "description": "CLI to install and update the DOCD — DOCs-Driven Development methodology in any project.", "license": "MIT", "type": "module", @@ -10,7 +10,8 @@ "scripts": { "build": "tsc && rm -rf dist/templates && cp -r src/templates dist/templates", "dev": "tsx src/index.ts", - "test": "tsx --test tests/**/*.test.ts", + "test": "tsx --test tests/*.test.ts", + "test:types": "tsc -p tsconfig.test.json", "prepublishOnly": "npm run build" }, "files": [ diff --git a/src/cli.ts b/src/cli.ts index 0234790..441c9cb 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -74,7 +74,7 @@ ${pc.bold("docd")} — install and update DOCD (DOCs-Driven Development) in any Usage: docd init --ide kilo - docd init --ide kilo,cursor + docd init --ide kilo,kimi docd init --global docd status docd changes @@ -84,9 +84,9 @@ Usage: docd update --force Options: - --ide Comma-separated IDE providers (default: kilo) - --global Install/update agents and skills in the global IDE config - (e.g. ~/.config/kilo); .docd.json stays local + --ide Comma-separated IDE providers: kilo, kimi (default: kilo) + --global Install/update agents and skills in the global shared config + (~/.agents); .docd.json stays local --dry-run Show changes without applying --yes Apply all updates without prompting --force Overwrite local changes without prompting diff --git a/src/commands/status.ts b/src/commands/status.ts index 7ccec69..a62bc5e 100644 --- a/src/commands/status.ts +++ b/src/commands/status.ts @@ -7,6 +7,8 @@ import { getPackageVersion, getTemplatesDir } from "../paths.js"; import { loadManifest, parseFrontmatter } from "../manifest.js"; import { findProvider, parseIdeArgs } from "../providers/index.js"; import { hasMeaningfulDiff } from "../diff.js"; +import { findLegacyFiles } from "../migrate.js"; +import type { Provider } from "../providers/base.js"; export interface StatusOptions { cwd: string; @@ -32,14 +34,36 @@ export async function runStatus(options: StatusOptions): Promise { const manifest = await loadManifest(getTemplatesDir()); const globalInstall = config.config.global ?? false; + const groups = new Map(); for (const ide of ides) { const provider = findProvider(ide); if (!provider) { console.log(pc.red(`Unknown provider "${ide}"`)); continue; } + const dir = provider.getPaths(cwd, globalInstall).configDir; + const group = groups.get(dir); + if (group) { + group.names.push(provider.name); + } else { + groups.set(dir, { names: [provider.name], provider }); + } + } - console.log(pc.bold(`Provider: ${provider.name}${globalInstall ? " (global)" : ""}`)); + for (const { names, provider } of groups.values()) { + console.log(pc.bold(`Provider: ${names.join(", ")}${globalInstall ? " (global)" : ""}`)); + + const recordedDir = config.config[provider.configKey]; + if (!globalInstall && typeof recordedDir === "string" && recordedDir !== provider.configDir) { + const legacyFiles = findLegacyFiles(provider, cwd, manifest); + if (legacyFiles.length > 0) { + console.log( + pc.yellow( + ` legacy layout detected in ${recordedDir} (${legacyFiles.length} file(s)); run "docd update" to migrate`, + ), + ); + } + } const outdated: string[] = []; const modified: string[] = []; diff --git a/src/commands/update.ts b/src/commands/update.ts index 4fd45af..25dc22c 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -9,6 +9,7 @@ import { loadManifest, parseFrontmatter, serializeFrontmatter } from "../manifes import { findProvider, parseIdeArgs } from "../providers/index.js"; import { computeDiff, hasMeaningfulDiff } from "../diff.js"; import { backupFile, ensureBackupDir } from "../backup.js"; +import { findLegacyFiles, migrateLegacyLayout } from "../migrate.js"; import type { Provider } from "../providers/base.js"; import type { TemplateFile } from "../manifest.js"; @@ -42,6 +43,8 @@ export async function runUpdate(options: UpdateOptions): Promise { let updatedCount = 0; let skippedCount = 0; let abortAll = false; + let configChanged = false; + const processedDirs = new Map(); for (const ide of ides) { const provider = findProvider(ide); @@ -51,6 +54,28 @@ export async function runUpdate(options: UpdateOptions): Promise { } const paths = provider.getPaths(cwd, globalInstall); + const alreadyProcessed = processedDirs.get(paths.configDir); + if (alreadyProcessed) { + console.log( + pc.dim(`provider "${ide}" shares ${paths.configDir} with "${alreadyProcessed}"; skipping duplicate pass`), + ); + continue; + } + processedDirs.set(paths.configDir, provider.name); + + const recordedDir = config.config[provider.configKey]; + if (!globalInstall && typeof recordedDir === "string" && recordedDir !== provider.configDir) { + const legacyFiles = findLegacyFiles(provider, cwd, manifest); + if (legacyFiles.length > 0) { + console.log(pc.cyan(`Migrating ${legacyFiles.length} file(s) from legacy layout ${recordedDir}:`)); + updatedCount += await migrateLegacyLayout(provider, cwd, manifest, options.dryRun ?? false); + } + if (!options.dryRun) { + config.config[provider.configKey] = provider.configDir; + configChanged = true; + } + } + await ensureBackupDir(paths.backupDir); for (const template of manifest.files) { @@ -144,10 +169,14 @@ export async function runUpdate(options: UpdateOptions): Promise { } } - if (!options.dryRun && updatedCount > 0) { - config.version = bundleVersion; + if (!options.dryRun && (updatedCount > 0 || configChanged)) { + if (updatedCount > 0) { + config.version = bundleVersion; + } await writeConfig(cwd, config); - console.log(pc.green(`\nUpdated .docd.json version to ${bundleVersion}.`)); + if (updatedCount > 0) { + console.log(pc.green(`\nUpdated .docd.json version to ${bundleVersion}.`)); + } } console.log(); diff --git a/src/config.ts b/src/config.ts index 1ed35cb..d5585c5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,7 +6,6 @@ export interface DocsManagerConfig { root: string; specFile: string | null; guideFile: string | null; - dataModelsFile: string | null; uiFile: string | null; changelogFile: string; changesDir: string; @@ -30,7 +29,6 @@ export const DEFAULT_DOCS_CONFIG: DocsManagerConfig = { root: "docs", specFile: "spec.md", guideFile: "guide.md", - dataModelsFile: "data-models.md", uiFile: "ui.md", changelogFile: "changelog.md", changesDir: "changes", diff --git a/src/migrate.ts b/src/migrate.ts new file mode 100644 index 0000000..1e4bfec --- /dev/null +++ b/src/migrate.ts @@ -0,0 +1,64 @@ +import { mkdir, rename } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import pc from "picocolors"; +import type { Provider } from "./providers/base.js"; +import type { Manifest } from "./manifest.js"; + +export function legacyInstalledPath(provider: Provider, templateRelativePath: string): string | null { + const legacy = provider.legacy; + if (!legacy) return null; + const parts = templateRelativePath.split("/"); + const [kind] = parts; + if (kind === "agent") { + return [legacy.configDir, legacy.agentDirName, ...parts.slice(1)].join("/"); + } + if (kind === "skill") { + return [legacy.configDir, legacy.skillDirName, ...parts.slice(1)].join("/"); + } + return null; +} + +export function findLegacyFiles(provider: Provider, cwd: string, manifest: Manifest): string[] { + const found: string[] = []; + for (const template of manifest.files) { + const legacyRel = legacyInstalledPath(provider, template.relativePath); + if (legacyRel && existsSync(resolve(cwd, legacyRel))) { + found.push(legacyRel); + } + } + return found; +} + +export async function migrateLegacyLayout( + provider: Provider, + cwd: string, + manifest: Manifest, + dryRun = false, +): Promise { + let migrated = 0; + for (const template of manifest.files) { + const legacyRel = legacyInstalledPath(provider, template.relativePath); + if (!legacyRel) continue; + const legacyAbs = resolve(cwd, legacyRel); + if (!existsSync(legacyAbs)) continue; + + const newRel = provider.resolveInstalledPath(template.relativePath, false); + const newAbs = resolve(cwd, newRel); + if (existsSync(newAbs)) { + console.log(pc.yellow(` kept legacy ${legacyRel} (${newRel} already exists)`)); + continue; + } + + if (dryRun) { + console.log(pc.yellow(`[dry-run] would migrate ${legacyRel} -> ${newRel}`)); + continue; + } + + await mkdir(dirname(newAbs), { recursive: true }); + await rename(legacyAbs, newAbs); + migrated++; + console.log(pc.green(` migrated ${legacyRel} -> ${newRel}`)); + } + return migrated; +} diff --git a/src/providers/agents.ts b/src/providers/agents.ts new file mode 100644 index 0000000..a2f7727 --- /dev/null +++ b/src/providers/agents.ts @@ -0,0 +1,26 @@ +import { homedir } from "node:os"; +import { resolve } from "node:path"; +import { Provider, ProviderPaths, getInstalledPath } from "./base.js"; + +export abstract class AgentsProvider implements Provider { + abstract name: string; + abstract configKey: string; + configDir = ".agents"; + agentDirName = "agents"; + skillDirName = "skills"; + globalConfigDir = resolve(homedir(), ".agents"); + + getPaths(cwd: string, globalInstall = false): ProviderPaths { + const base = globalInstall ? this.globalConfigDir : `${cwd}/${this.configDir}`; + return { + configDir: base, + agentDir: `${base}/${this.agentDirName}`, + skillDir: `${base}/${this.skillDirName}`, + backupDir: `${base}/.docd-backups`, + }; + } + + resolveInstalledPath(templateRelativePath: string, globalInstall = false): string { + return getInstalledPath(this, templateRelativePath, globalInstall); + } +} diff --git a/src/providers/base.ts b/src/providers/base.ts index f9e144d..b2fee90 100644 --- a/src/providers/base.ts +++ b/src/providers/base.ts @@ -5,11 +5,20 @@ export interface ProviderPaths { backupDir: string; } +export interface ProviderLegacyDirs { + configDir: string; + agentDirName: string; + skillDirName: string; +} + export interface Provider { name: string; configKey: string; configDir: string; + agentDirName: string; + skillDirName: string; globalConfigDir?: string; + legacy?: ProviderLegacyDirs; getPaths(cwd: string, globalInstall?: boolean): ProviderPaths; resolveInstalledPath(templateRelativePath: string, globalInstall?: boolean): string; transformTemplate?(content: string): string; @@ -29,11 +38,11 @@ export function getInstalledPath( globalInstall && provider.globalConfigDir ? provider.globalConfigDir : provider.configDir; if (kind === "agent") { - return [base, "agent", ...parts.slice(1)].join("/"); + return [base, provider.agentDirName, ...parts.slice(1)].join("/"); } if (kind === "skill") { - return [base, "skills", ...parts.slice(1)].join("/"); + return [base, provider.skillDirName, ...parts.slice(1)].join("/"); } return [provider.configDir, ...parts].join("/"); diff --git a/src/providers/index.ts b/src/providers/index.ts index 7b2e45c..290cba6 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -1,7 +1,8 @@ import { KiloProvider } from "./kilo.js"; +import { KimiProvider } from "./kimi.js"; import type { Provider } from "./base.js"; -const providers = [new KiloProvider()]; +const providers = [new KiloProvider(), new KimiProvider()]; export function listProviders(): Provider[] { return providers; diff --git a/src/providers/kilo.ts b/src/providers/kilo.ts index 2b19514..23e4c2a 100644 --- a/src/providers/kilo.ts +++ b/src/providers/kilo.ts @@ -1,24 +1,7 @@ -import { homedir } from "node:os"; -import { resolve } from "node:path"; -import { Provider, ProviderPaths, getInstalledPath } from "./base.js"; +import { AgentsProvider } from "./agents.js"; -export class KiloProvider implements Provider { +export class KiloProvider extends AgentsProvider { name = "kilo"; configKey = "kiloConfigDir"; - configDir = ".kilo"; - globalConfigDir = resolve(homedir(), ".config", "kilo"); - - getPaths(cwd: string, globalInstall = false): ProviderPaths { - const base = globalInstall ? this.globalConfigDir : `${cwd}/${this.configDir}`; - return { - configDir: base, - agentDir: `${base}/agent`, - skillDir: `${base}/skills`, - backupDir: `${base}/.docd-backups`, - }; - } - - resolveInstalledPath(templateRelativePath: string, globalInstall = false): string { - return getInstalledPath(this, templateRelativePath, globalInstall); - } + legacy = { configDir: ".kilo", agentDirName: "agent", skillDirName: "skills" }; } diff --git a/src/providers/kimi.ts b/src/providers/kimi.ts new file mode 100644 index 0000000..e18a28c --- /dev/null +++ b/src/providers/kimi.ts @@ -0,0 +1,6 @@ +import { AgentsProvider } from "./agents.js"; + +export class KimiProvider extends AgentsProvider { + name = "kimi"; + configKey = "kimiConfigDir"; +} diff --git a/src/templates/agent/docs-manager.md b/src/templates/agent/docs-manager.md index b3d7962..e2a5936 100644 --- a/src/templates/agent/docs-manager.md +++ b/src/templates/agent/docs-manager.md @@ -1,6 +1,6 @@ --- name: docs-manager -version: 1.2.0 +version: 1.3.0 description: Specialized agent for managing feature documentation and change plans in the docs/ directory. mode: primary --- @@ -15,7 +15,7 @@ Focus only on files under `docs/` and `.docd.json`. ## Responsibilities - Receive documentation commands: `/docs-plan`, `/docs-generate-from-code`, `/docs-implement`, `/`. -- Read the `config.docs` object in `.docd.json` to resolve the configured docs root and the standard set of feature files: `specFile`, `guideFile`, `dataModelsFile`, `uiFile`, `changelogFile`, `changesDir`, `archiveDir`, and `changeFilenamePattern`. +- Read the `config.docs` object in `.docd.json` to resolve the configured docs root and the standard set of feature files: `specFile`, `guideFile`, `uiFile`, `changelogFile`, `changesDir`, `archiveDir`, and `changeFilenamePattern`. - Validate feature and change slugs as `kebab-case`. - Prevent duplicate feature IDs and change IDs. - Keep `changelog.md` files up to date after create, archive, or sync operations. @@ -34,7 +34,6 @@ Always load the `config.docs` object from `.docd.json` at the start of a task. I "root": "docs", "specFile": "spec.md", "guideFile": "guide.md", - "dataModelsFile": "data-models.md", "uiFile": "ui.md", "changelogFile": "changelog.md", "changesDir": "changes", @@ -47,7 +46,7 @@ Always load the `config.docs` object from `.docd.json` at the start of a task. I Use these values to resolve paths dynamically. Never hardcode paths that are configurable there. -`specFile`, `guideFile`, `dataModelsFile`, and `uiFile` are optional. When any of them is set to `null`, that file is disabled: do not create, read, update, or link to it, and skip it in every generate, plan, implement, and sync flow. +`specFile`, `guideFile`, and `uiFile` are optional. When any of them is set to `null`, that file is disabled: do not create, read, update, or link to it, and skip it in every generate, plan, implement, and sync flow. ## Feature/Subfeature Documentation Files @@ -57,7 +56,6 @@ Every feature and subfeature directory shares the same file set. Subfeatures are / guide.md # Feature overview spec.md # Technical specification - data-models.md # (optional) Model map, relationships, and business invariants ui.md # (optional) Visual and design details changelog.md # History of completed changes changes/ # Change plans @@ -75,7 +73,7 @@ A feature's documentation moves through four states: 1. **Created** — generated by `/docs-generate-from-code` for legacy features or created by `/docs:plan` when the first change is planned. 2. **Planned** — `changes/` receives change plans that describe future work. 3. **Implemented** — `/docs-implement` updates task checkboxes in the plan and may touch the relevant docs during implementation. -4. **Synced** — `/docs-sync` finalizes the plan, updates `changelog.md`, and refreshes the enabled feature docs (`guide.md`, `spec.md`, `data-models.md`, `ui.md`) to reflect the actual delivery. +4. **Synced** — `/docs-sync` finalizes the plan, updates `changelog.md`, and refreshes the enabled feature docs (`guide.md`, `spec.md`, `ui.md`) to reflect the actual delivery. ### `guide.md` — Feature overview @@ -89,7 +87,7 @@ A feature's documentation moves through four states: - Description: what the feature does and why it exists. - How to use: entry points, URLs, flows. - Goals: measurable objectives. - - Links to `spec.md`, `data-models.md`, `ui.md`, `changelog.md`, and `changes/` (skip links to disabled files). + - Links to `spec.md`, `ui.md`, `changelog.md`, and `changes/` (skip links to disabled files). - **Example trigger for edit**: a new API endpoint changes the public surface of the feature → update the description and affected areas. ### `spec.md` — Technical specification @@ -107,23 +105,6 @@ A feature's documentation moves through four states: - Design decisions and trade-offs. - **Example trigger for edit**: a new endpoint is added → document its request/response shape and where it fits in the architecture. -### `data-models.md` — Model map and invariants - -- **Created when**: the feature is first documented. -- **Edited when**: - - A model or entity is added, removed, or moved to a different location. - - Relationships between entities change. - - A business invariant or rule tied to the data changes. - - `/docs-sync` finishes a change that touches data structures. -- **Must contain**: - - Index of primary entities: purpose and file paths (ORM models, schemas, DTOs). - - Relationships between entities. - - Business invariants and rules not visible in code (e.g., "an order cannot have more than one approved payment"). - - Stable contracts (public API payloads, database schema), summarized only when other features depend on them. -- **Must NOT contain**: - - Field-by-field definitions, types, or validation rules. Those live in code (ORM schemas, validators, TypeScript types), which is the single source of truth — duplicating them here guarantees drift. -- **Example trigger for edit**: a new entity is introduced → add it to the index with its path and relationships, and document only the invariants the code cannot express. - ### `ui.md` — Visual and design details - **Created when**: the feature is first documented. @@ -207,14 +188,14 @@ Do **not** use a subfeature for: - `/docs-generate-from-code `: - Analyze an existing codebase directory or feature. - If documentation for the feature already exists, report it and stop. - - Otherwise, generate `guide.md`, `spec.md`, `data-models.md`, `ui.md`, `changelog.md`, and the `changes/` directory based on the code (skipping files disabled in the config). + - Otherwise, generate `guide.md`, `spec.md`, `ui.md`, `changelog.md`, and the `changes/` directory based on the code (skipping files disabled in the config). - Present a summary of generated files and assumptions before saving. - `/docs-implement /`: invoke `docs-implement`. - `/docs-sync [/]`: - After implementation, analyze what changed. - If the change plan status is not `completed`/`finalizado`, update it. - Generate a summary and update the feature/subfeature `changelog.md`. - - Update the enabled feature docs (`guide.md`, `spec.md`, `data-models.md`, `ui.md`) to reflect what was actually implemented. + - Update the enabled feature docs (`guide.md`, `spec.md`, `ui.md`) to reflect what was actually implemented. - If a change ID is provided, update only that entry; otherwise sync all completed changes. ## Rules diff --git a/src/templates/skill/docs-generate-from-code/SKILL.md b/src/templates/skill/docs-generate-from-code/SKILL.md index c51458c..09621c6 100644 --- a/src/templates/skill/docs-generate-from-code/SKILL.md +++ b/src/templates/skill/docs-generate-from-code/SKILL.md @@ -1,6 +1,6 @@ --- name: docs-generate-from-code -version: 1.2.0 +version: 1.3.0 description: Generate feature documentation from an existing codebase directory or feature path. --- @@ -19,7 +19,7 @@ If the input is ambiguous, ask the user to confirm the target feature name and d 1. **Load configuration** - Read the `config.docs` object in `.docd.json` to resolve `root`, `specFile`, `guideFile`, `dataModelsFile`, `uiFile`, `changelogFile`, `changesDir`, `archiveDir`, `changeFilenamePattern`, `defaultStatus`, and `allowedStatuses`. A `null` value for `specFile`, `guideFile`, `dataModelsFile`, or `uiFile` means the file is disabled: never create, read, update, or link to it. + Read the `config.docs` object in `.docd.json` to resolve `root`, `specFile`, `guideFile`, `uiFile`, `changelogFile`, `changesDir`, `archiveDir`, `changeFilenamePattern`, `defaultStatus`, and `allowedStatuses`. A `null` value for `specFile`, `guideFile`, or `uiFile` means the file is disabled: never create, read, update, or link to it. 2. **Identify the target** @@ -46,7 +46,6 @@ If the input is ambiguous, ask the user to confirm the target feature name and d - `docs//`: description, how to use, goals, affected areas, tags, and links. - `docs//`: architecture, API contracts, routes/endpoints, flows, and design decisions inferred from the code. - - `docs//` (optional): an index of the models found (purpose + file paths), their relationships, and business invariants not visible in code. Do not copy field definitions, types, or validation rules — reference the code paths instead. - `docs//` (optional): visual details, colors, formats, typography, and placeholders for design URLs. - `docs//`: empty table with Date, Change, Description, Responsible columns. - `docs///`: empty directory for future change plans. diff --git a/src/templates/skill/docs-implement/SKILL.md b/src/templates/skill/docs-implement/SKILL.md index 9580cf5..7ed1827 100644 --- a/src/templates/skill/docs-implement/SKILL.md +++ b/src/templates/skill/docs-implement/SKILL.md @@ -1,6 +1,6 @@ --- name: docs-implement -version: 1.2.0 +version: 1.3.0 description: Implement tasks from an approved change plan using subagents or Agent Manager for parallel execution. --- @@ -16,7 +16,7 @@ Implement the tasks defined in an approved change plan located in `docs//`, `docs//`, `docs//`, `docs//`, and `docs//` if they are configured (not `null`) and exist. +- **Existing feature docs**: Read `docs//`, `docs//`, `docs//`, and `docs//` if they are configured (not `null`) and exist. - **Existing plans**: Read the current plan file if editing or refining. Scan related plans in the same `changesDir/`. - **Current code**: Use `grep`, `semantic_search`, and `read` to inspect relevant code paths when the user mentions concrete areas (e.g., `server/api`, `client/dashboard`). Summarize findings only; do not modify code. - **External references**: Use `webfetch` when the user asks for industry patterns, library documentation, or competitive references that can inform the plan. @@ -46,7 +46,6 @@ When creating a new feature or subfeature, generate the following files with con - `docs//` (e.g., `guide.md`): description, how to use, goals, links. - `docs//` (e.g., `spec.md`): technical vision, architecture, API contracts, flows, design decisions. -- `docs//` (e.g., `data-models.md`, optional): model index with file paths, relationships, and business invariants (never field-level copies of code). - `docs//` (e.g., `ui.md`, optional): visual details, colors, formats, typography, and URL to an external design system (e.g., Figma). - `docs//` (e.g., `changelog.md`): empty table with columns Date, Change, Description, Responsible. - `docs///` directory. diff --git a/src/templates/skill/docs-sync/SKILL.md b/src/templates/skill/docs-sync/SKILL.md index 5be2c8d..7a7cf3d 100644 --- a/src/templates/skill/docs-sync/SKILL.md +++ b/src/templates/skill/docs-sync/SKILL.md @@ -1,12 +1,12 @@ --- name: docs-sync -version: 1.2.0 +version: 1.3.0 description: Finalize an implemented change plan, update the changelog, and refresh feature documentation to reflect what was implemented. --- # docs-sync -After implementation, finalize the change plan, update the feature/subfeature `changelog.md`, and refresh the enabled feature docs (`guide.md`, `spec.md`, `data-models.md`, `ui.md`) to reflect what was actually implemented. +After implementation, finalize the change plan, update the feature/subfeature `changelog.md`, and refresh the enabled feature docs (`guide.md`, `spec.md`, `ui.md`) to reflect what was actually implemented. ## Input @@ -16,7 +16,7 @@ After implementation, finalize the change plan, update the feature/subfeature `c 1. **Load configuration** - Read the `config.docs` object in `.docd.json` to resolve `root`, `specFile`, `guideFile`, `dataModelsFile`, `uiFile`, `changelogFile`, `changesDir`, and `allowedStatuses`. A `null` value for `specFile`, `guideFile`, `dataModelsFile`, or `uiFile` means the file is disabled: never create, read, update, or link to it. + Read the `config.docs` object in `.docd.json` to resolve `root`, `specFile`, `guideFile`, `uiFile`, `changelogFile`, `changesDir`, and `allowedStatuses`. A `null` value for `specFile`, `guideFile`, or `uiFile` means the file is disabled: never create, read, update, or link to it. 2. **Identify the target** @@ -27,7 +27,7 @@ After implementation, finalize the change plan, update the feature/subfeature `c - Read `docs///--plan.md`. - Check the current `status` in the frontmatter. - - Read `docs//`, `docs//`, `docs//`, and `docs//` — skip any that are disabled (`null`) in the config. + - Read `docs//`, `docs//`, and `docs//` — skip any that are disabled (`null`) in the config. 4. **Finalize the plan status** @@ -48,10 +48,9 @@ After implementation, finalize the change plan, update the feature/subfeature `c 7. **Refresh feature documentation** - - Compare the implemented changes with the current enabled feature docs (`guide.md`, `spec.md`, `data-models.md`, `ui.md`). + - Compare the implemented changes with the current enabled feature docs (`guide.md`, `spec.md`, `ui.md`). - Update the feature docs to reflect what was actually shipped: - Add or update sections in `spec.md` for new API contracts, flows, or architectural decisions (if enabled). - - Update `data-models.md` only when the model map (entities/paths), relationships, or business invariants changed — never copy field definitions from code (if enabled). - Add or update UI notes, screenshots, or links to external design systems (e.g., Figma) in `ui.md` (if enabled). - Update `guide.md` usage instructions, goals, or affected areas if they changed (if enabled). - Update `updated_at` in `guide.md` frontmatter (if enabled). diff --git a/tests/config.test.ts b/tests/config.test.ts index cec196d..4166895 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -33,7 +33,6 @@ describe("config", () => { const config = defaultConfig("1.0.0", ["kilo"]); const docs: DocsManagerConfig = { ...config.config.docs, - dataModelsFile: null, uiFile: null, }; config.config.docs = docs; @@ -41,7 +40,6 @@ describe("config", () => { const loaded = await readConfig(dir); assert.ok(loaded); - assert.equal(loaded.config.docs.dataModelsFile, null); assert.equal(loaded.config.docs.uiFile, null); assert.equal(loaded.config.docs.guideFile, "guide.md"); assert.equal(loaded.config.docs.specFile, "spec.md"); @@ -49,4 +47,13 @@ describe("config", () => { await rm(dir, { recursive: true, force: true }); } }); + + it("readConfig returns null when no .docd.json exists", async () => { + const dir = await mkdtemp(join(tmpdir(), "docd-config-empty-")); + try { + assert.equal(await readConfig(dir), null); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); }); diff --git a/tests/dogfood.test.ts b/tests/dogfood.test.ts new file mode 100644 index 0000000..fb072ac --- /dev/null +++ b/tests/dogfood.test.ts @@ -0,0 +1,37 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; +import { loadManifest } from "../src/manifest.js"; +import { getTemplatesDir } from "../src/paths.js"; +import { KiloProvider } from "../src/providers/kilo.js"; + +describe("dogfooding", () => { + it("installed .agents copies are in sync with src/templates", async () => { + const cwd = process.cwd(); + const provider = new KiloProvider(); + const manifest = await loadManifest(getTemplatesDir()); + assert.ok(manifest.files.length > 0); + + const stale: string[] = []; + for (const template of manifest.files) { + const installedRelative = provider.resolveInstalledPath(template.relativePath, false); + const installedAbsolute = resolve(cwd, installedRelative); + if (!existsSync(installedAbsolute)) { + stale.push(`${installedRelative} (missing)`); + continue; + } + const installedContent = await readFile(installedAbsolute, "utf-8"); + if (installedContent.trimEnd() !== template.content.trimEnd()) { + stale.push(`${installedRelative} (drifted)`); + } + } + + assert.deepEqual( + stale, + [], + `Dogfooded .agents files are out of sync with src/templates; run "npm run dev -- update --yes":\n${stale.join("\n")}`, + ); + }); +}); diff --git a/tests/migrate.test.ts b/tests/migrate.test.ts new file mode 100644 index 0000000..8d9f830 --- /dev/null +++ b/tests/migrate.test.ts @@ -0,0 +1,94 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtemp, mkdir, writeFile, rm, readFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { findLegacyFiles, legacyInstalledPath, migrateLegacyLayout } from "../src/migrate.js"; +import { KiloProvider } from "../src/providers/kilo.js"; +import { KimiProvider } from "../src/providers/kimi.js"; +import type { Manifest } from "../src/manifest.js"; + +const manifest: Manifest = { + files: [ + { sourcePath: "t/agent/docs-manager.md", relativePath: "agent/docs-manager.md", content: "agent", version: "1", frontmatter: null }, + { sourcePath: "t/skill/docs-plan/SKILL.md", relativePath: "skill/docs-plan/SKILL.md", content: "skill", version: "1", frontmatter: null }, + ], +}; + +async function seedLegacy(dir: string): Promise { + await mkdir(join(dir, ".kilo/agent"), { recursive: true }); + await mkdir(join(dir, ".kilo/skills/docs-plan"), { recursive: true }); + await writeFile(join(dir, ".kilo/agent/docs-manager.md"), "legacy agent\n", "utf-8"); + await writeFile(join(dir, ".kilo/skills/docs-plan/SKILL.md"), "legacy skill\n", "utf-8"); +} + +describe("migrate", () => { + it("maps template paths to the legacy kilo layout", () => { + const kilo = new KiloProvider(); + assert.equal(legacyInstalledPath(kilo, "agent/docs-manager.md"), ".kilo/agent/docs-manager.md"); + assert.equal(legacyInstalledPath(kilo, "skill/docs-plan/SKILL.md"), ".kilo/skills/docs-plan/SKILL.md"); + assert.equal(legacyInstalledPath(kilo, "docs/spec.md"), null); + }); + + it("returns null for providers without a legacy layout", () => { + assert.equal(legacyInstalledPath(new KimiProvider(), "agent/docs-manager.md"), null); + }); + + it("moves legacy files into the new layout, preserving content", async () => { + const dir = await mkdtemp(join(tmpdir(), "docd-migrate-")); + try { + await seedLegacy(dir); + const kilo = new KiloProvider(); + + assert.deepEqual(findLegacyFiles(kilo, dir, manifest).sort(), [ + ".kilo/agent/docs-manager.md", + ".kilo/skills/docs-plan/SKILL.md", + ]); + + const migrated = await migrateLegacyLayout(kilo, dir, manifest); + assert.equal(migrated, 2); + assert.equal(await readFile(join(dir, ".agents/agents/docs-manager.md"), "utf-8"), "legacy agent\n"); + assert.equal( + await readFile(join(dir, ".agents/skills/docs-plan/SKILL.md"), "utf-8"), + "legacy skill\n", + ); + assert.equal(existsSync(join(dir, ".kilo/agent/docs-manager.md")), false); + assert.deepEqual(findLegacyFiles(kilo, dir, manifest), []); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + + it("keeps the legacy file when the new target already exists", async () => { + const dir = await mkdtemp(join(tmpdir(), "docd-migrate-")); + try { + await seedLegacy(dir); + await mkdir(join(dir, ".agents/agents"), { recursive: true }); + await writeFile(join(dir, ".agents/agents/docs-manager.md"), "current\n", "utf-8"); + const kilo = new KiloProvider(); + + const migrated = await migrateLegacyLayout(kilo, dir, manifest); + assert.equal(migrated, 1); + assert.equal(await readFile(join(dir, ".agents/agents/docs-manager.md"), "utf-8"), "current\n"); + assert.equal(existsSync(join(dir, ".kilo/agent/docs-manager.md")), true); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + + it("does not write anything in dry-run mode", async () => { + const dir = await mkdtemp(join(tmpdir(), "docd-migrate-")); + try { + await seedLegacy(dir); + const kilo = new KiloProvider(); + + const migrated = await migrateLegacyLayout(kilo, dir, manifest, true); + assert.equal(migrated, 0); + assert.equal(existsSync(join(dir, ".agents/agents/docs-manager.md")), false); + assert.equal(existsSync(join(dir, ".kilo/agent/docs-manager.md")), true); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); +}); diff --git a/tests/providers.test.ts b/tests/providers.test.ts new file mode 100644 index 0000000..90bbc7f --- /dev/null +++ b/tests/providers.test.ts @@ -0,0 +1,116 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { getInstalledPath, Provider } from "../src/providers/base.js"; +import { listProviders, findProvider, parseIdeArgs } from "../src/providers/index.js"; +import { KiloProvider } from "../src/providers/kilo.js"; +import { KimiProvider } from "../src/providers/kimi.js"; + +function stubProvider(globalConfigDir?: string): Provider { + return { + name: "stub", + configKey: "stubConfigDir", + configDir: ".stub", + agentDirName: "agent", + skillDirName: "skills", + globalConfigDir, + getPaths: () => { + throw new Error("not used in these tests"); + }, + resolveInstalledPath: () => { + throw new Error("not used in these tests"); + }, + }; +} + +describe("providers registry", () => { + it("lists kilo and kimi and finds them by name", () => { + assert.ok(listProviders().some((p) => p.name === "kilo")); + assert.ok(listProviders().some((p) => p.name === "kimi")); + assert.equal(findProvider("kilo")?.name, "kilo"); + assert.equal(findProvider("kimi")?.name, "kimi"); + }); + + it("returns undefined for unknown providers", () => { + assert.equal(findProvider("nope"), undefined); + }); +}); + +describe("parseIdeArgs", () => { + it("defaults to kilo when undefined", () => { + assert.deepEqual(parseIdeArgs(undefined), ["kilo"]); + }); + + it("splits a comma-separated string and trims entries", () => { + assert.deepEqual(parseIdeArgs("kilo, cursor ,,"), ["kilo", "cursor"]); + }); + + it("flattens arrays containing comma-separated values", () => { + assert.deepEqual(parseIdeArgs(["kilo,cursor", "other"]), ["kilo", "cursor", "other"]); + }); +}); + +describe("getInstalledPath", () => { + it("maps agent templates to the agent dir", () => { + assert.equal(getInstalledPath(stubProvider(), "agent/docd.md"), ".stub/agent/docd.md"); + }); + + it("maps skill templates to the skills dir", () => { + assert.equal(getInstalledPath(stubProvider(), "skill/docd/SKILL.md"), ".stub/skills/docd/SKILL.md"); + }); + + it("uses the global config dir for agents when global install is on", () => { + assert.equal( + getInstalledPath(stubProvider("/global/stub"), "agent/docd.md", true), + "/global/stub/agent/docd.md", + ); + }); + + it("falls back to the local config dir when the provider has no global dir", () => { + assert.equal(getInstalledPath(stubProvider(), "agent/docd.md", true), ".stub/agent/docd.md"); + }); + + it("keeps other template kinds under the local config dir even for global installs", () => { + assert.equal( + getInstalledPath(stubProvider("/global/stub"), "docs/spec.md", true), + ".stub/docs/spec.md", + ); + }); +}); + +describe("KiloProvider.getPaths", () => { + it("builds local paths under the shared .agents dir", () => { + const paths = new KiloProvider().getPaths("/proj"); + assert.deepEqual(paths, { + configDir: "/proj/.agents", + agentDir: "/proj/.agents/agents", + skillDir: "/proj/.agents/skills", + backupDir: "/proj/.agents/.docd-backups", + }); + }); + + it("builds global paths under ~/.agents", () => { + const paths = new KiloProvider().getPaths("/proj", true); + assert.ok(paths.configDir.endsWith(".agents")); + assert.ok(paths.agentDir.endsWith(".agents/agents")); + assert.ok(paths.backupDir.endsWith(".agents/.docd-backups")); + }); + + it("maps agent templates to the agents dir", () => { + assert.equal( + new KiloProvider().resolveInstalledPath("agent/docs-manager.md"), + ".agents/agents/docs-manager.md", + ); + }); +}); + +describe("KimiProvider.getPaths", () => { + it("shares the same .agents layout as kilo", () => { + const kimi = new KimiProvider().getPaths("/proj"); + const kilo = new KiloProvider().getPaths("/proj"); + assert.deepEqual(kimi, kilo); + }); + + it("has its own config key", () => { + assert.equal(new KimiProvider().configKey, "kimiConfigDir"); + }); +}); diff --git a/tests/update.test.ts b/tests/update.test.ts new file mode 100644 index 0000000..6141a0d --- /dev/null +++ b/tests/update.test.ts @@ -0,0 +1,70 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtemp, mkdir, writeFile, rm, readFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { runUpdate } from "../src/commands/update.js"; +import { defaultConfig, readConfig, writeConfig } from "../src/config.js"; + +async function captureLogs(fn: () => Promise): Promise { + const logs: string[] = []; + const original = console.log; + console.log = (...args: unknown[]) => { + logs.push(args.map(String).join(" ")); + }; + try { + await fn(); + } finally { + console.log = original; + } + return logs; +} + +describe("update", () => { + it("processes each shared config dir only once when ides resolve to the same layout", async () => { + const dir = await mkdtemp(join(tmpdir(), "docd-update-")); + try { + const config = defaultConfig("0.0.0", ["kilo", "kimi"]); + config.config.kiloConfigDir = ".agents"; + config.config.kimiConfigDir = ".agents"; + await writeConfig(dir, config); + + const logs = await captureLogs(() => runUpdate({ cwd: dir, yes: true })); + + const installs = logs.filter((l) => l.includes("installed .agents/agents/docs-manager.md")); + assert.equal(installs.length, 1); + assert.ok(logs.some((l) => l.includes("shares") && l.includes("skipping duplicate pass"))); + assert.equal(existsSync(join(dir, ".agents/agents/docs-manager.md")), true); + assert.equal(existsSync(join(dir, ".agents/skills/docs-plan/SKILL.md")), true); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + + it("migrates a legacy .kilo layout and updates the recorded config dir", async () => { + const dir = await mkdtemp(join(tmpdir(), "docd-update-")); + try { + const config = defaultConfig("0.0.0", ["kilo"]); + config.config.kiloConfigDir = ".kilo"; + await writeConfig(dir, config); + await mkdir(join(dir, ".kilo/agent"), { recursive: true }); + await mkdir(join(dir, ".kilo/skills/docs-plan"), { recursive: true }); + await writeFile(join(dir, ".kilo/agent/docs-manager.md"), "legacy agent\n", "utf-8"); + await writeFile(join(dir, ".kilo/skills/docs-plan/SKILL.md"), "legacy skill\n", "utf-8"); + + const logs = await captureLogs(() => runUpdate({ cwd: dir, yes: true })); + + assert.ok(logs.some((l) => l.includes("Migrating 2 file(s) from legacy layout .kilo"))); + assert.ok(logs.some((l) => l.includes("migrated .kilo/agent/docs-manager.md"))); + assert.equal(existsSync(join(dir, ".kilo/agent/docs-manager.md")), false); + assert.equal(existsSync(join(dir, ".agents/agents/docs-manager.md")), true); + + const updated = await readConfig(dir); + assert.ok(updated); + assert.equal(updated.config.kiloConfigDir, ".agents"); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); +}); From 8f83e9ad23609910f3ac45f67990143ef48fa6f3 Mon Sep 17 00:00:00 2001 From: albertosouza Date: Tue, 28 Jul 2026 20:16:50 -0300 Subject: [PATCH 4/4] docs: add docd-cli feature documentation - Backfills guide, spec and changelog; data-models doc dropped. --- docs/docd-cli/changelog.md | 7 ++++ docs/docd-cli/changes/.gitkeep | 0 docs/docd-cli/guide.md | 55 +++++++++++++++++++++++++++++++ docs/docd-cli/spec.md | 59 ++++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 docs/docd-cli/changelog.md create mode 100644 docs/docd-cli/changes/.gitkeep create mode 100644 docs/docd-cli/guide.md create mode 100644 docs/docd-cli/spec.md diff --git a/docs/docd-cli/changelog.md b/docs/docd-cli/changelog.md new file mode 100644 index 0000000..11e5c77 --- /dev/null +++ b/docs/docd-cli/changelog.md @@ -0,0 +1,7 @@ +# docd-cli — Changelog + +| Date | Change | Description | Responsible | +|------|--------|-------------|-------------| +| 2026-07-25 | docs backfill | Initial feature documentation generated from code (`docs-generate-from-code`) | albertosouza | +| 2026-07-27 | .agents convention + kimi provider | Providers migrated to the shared `.agents/` layout (`.agents/agents/`, `.agents/skills/`, global `~/.agents`); added `kimi` provider; removed `dataModelsFile` support and the `data-models.md` doc | albertosouza | +| 2026-07-27 | review fixes | Legacy `.kilo`→`.agents` migration in `update` (warn-only in `status`), dedupe of providers sharing a config dir, package-lock bump to 1.1.0, dogfood drift test for `.agents/**` vs `src/templates/**` | albertosouza | diff --git a/docs/docd-cli/changes/.gitkeep b/docs/docd-cli/changes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/docd-cli/guide.md b/docs/docd-cli/guide.md new file mode 100644 index 0000000..fe41b22 --- /dev/null +++ b/docs/docd-cli/guide.md @@ -0,0 +1,55 @@ +--- +id: docd-cli +title: docd CLI +status: active +created_at: 2026-07-25 +updated_at: 2026-07-27 +owner: albertosouza +affected_areas: + - src/cli.ts + - src/commands/ + - src/providers/ + - src/templates/ + - src/config.ts + - src/manifest.ts +tags: + - cli + - tooling + - docd +--- + +# docd CLI + +Command-line tool (`@linkysystems/docd`, binary `docd`) that installs and updates the **DOCD — DOCs-Driven Development** methodology in any project. It scaffolds the local configuration (`.docd.json`), the `docs/` directory, and agent/skill files for the supported providers (`kilo` and `kimi`), both installed under the shared `.agents/` convention (`.agents/agents/`, `.agents/skills/`). + +## How to use + +```bash +# Initialize DOCD in the current project (kilo, kimi, or both) +docd init --ide kilo,kimi + +# Install agents/skills globally (~/.agents); .docd.json stays local +docd init --ide kilo --global + +# Check installed vs available template versions +docd status + +# List open change plans under docs/ +docd changes + +# Update installed templates to the current bundle +docd update [--dry-run] [--yes] [--force] +``` + +During development (this repo), run via tsx: `npm run dev -- `. + +## Goals + +- One-command bootstrap of the docs-driven workflow in any repository. +- Keep installed agent/skill templates up to date with the published bundle, with backups and interactive diff review before overwriting local modifications. +- Stay provider-agnostic: project config lives in `.docd.json`; only agents/skills are provider-specific. + +## Notes + +- This documentation was backfilled from the code with `docs-generate-from-code`; see [spec.md](spec.md) for architecture details and [changelog.md](changelog.md) for the change history. +- This very repository is initialized with DOCD (dogfooding): its `.docd.json` disables `uiFile` because a CLI has no UI. diff --git a/docs/docd-cli/spec.md b/docs/docd-cli/spec.md new file mode 100644 index 0000000..e9937ba --- /dev/null +++ b/docs/docd-cli/spec.md @@ -0,0 +1,59 @@ +# docd CLI — Spec + +## Architecture + +TypeScript + ESM, Node.js >= 18. Entry point `src/index.ts` (built to `dist/index.js` with a Node shebang, exposed as the `docd` binary). Argument parsing with `mri` in `src/cli.ts`, which dispatches to one module per command in `src/commands/`. Shared concerns live in flat modules at `src/`: `config.ts`, `manifest.ts`, `paths.ts`, `diff.ts`, `backup.ts`. Provider abstraction in `src/providers/` (`base.ts` defines the `Provider` interface; `agents.ts` is a shared base class for IDEs that follow the `.agents/` convention; `kilo.ts` and `kimi.ts` are the concrete implementations, differing only in `name`/`configKey`). Bundled templates in `src/templates/{agent,skill}/` are the payload that gets installed into target projects. + +Dependencies: `mri` (args), `prompts` (interactive prompts), `picocolors` (colors), `diff` (patches), `yaml` (frontmatter). Tests use `tsx --test` (`tests/*.test.ts`). + +## CLI contract + +Parsed in `src/cli.ts`: + +- `init --ide [--global]` — creates `.docd.json`, `docs/`, and installs templates per provider. No-ops if `.docd.json` already exists; skips template files that already exist. +- `status` — compares installed bundle version (`config.version` in `.docd.json`) with the package version, and reports per-provider files that are missing, outdated (frontmatter `version` mismatch), or locally modified (content diff). +- `changes` — walks `docs/` looking for `changes/` directories (skipping `archive/`), and lists change-plan `.md` files whose frontmatter `status` is not `completed`/`archived` (closed statuses are filtered from `allowedStatuses` in config). +- `update [--dry-run] [--yes] [--force] [--global]` — reconciles installed templates with the bundle (see flow below). +- `--cwd`, `-h/--help`, `-v/--version`. + +## Key flows + +### init (`src/commands/init.ts`) + +1. Resolve ides (default `kilo`), build default config via `defaultConfig()` and record each provider's config dir (`kiloConfigDir`, `kimiConfigDir`). +2. Abort if `.docd.json` exists (message suggests `docd update`). +3. Write `.docd.json`, create `docs/` root. +4. Load the template manifest (`loadManifest`) and, per provider, create the agent and skill dirs and copy each template to its installed path (`provider.resolveInstalledPath`), applying `transformTemplate` when the provider defines one (none of the current providers does; fallback only ensures a trailing newline). Since `kilo` and `kimi` share the `.agents/` layout, installing both writes the same files once (the second provider skips existing files). + +### update (`src/commands/update.ts`) + +- Providers that resolve to the same config dir (e.g. `kilo` and `kimi`, both on `.agents/`) are processed once per `update`/`status` run; duplicate passes are skipped (`status` merges their names into a single section). +- Legacy layout migration (`src/migrate.ts`): when the config dir recorded in `.docd.json` (`config.config[provider.configKey]`) differs from the provider's current `configDir` and the provider declares `legacy` dirs (Kilo: `.kilo/agent`, `.kilo/skills`), installed files found at the legacy paths are moved into the new layout (skipped when the target already exists) and the recorded key is rewritten. `status` only warns; `update` migrates (`--dry-run` reports without writing). + +For each provider and each manifest template: + +- Missing installed file → install it (or report in `--dry-run`). +- Needs update when the template frontmatter `version` differs from the installed one **or** there is a meaningful content diff (`hasMeaningfulDiff` ignores newline style and leading/trailing whitespace). +- Before overwriting, a timestamped backup is copied to the provider backup dir (`.agents/.docd-backups/_`). +- Without `--yes`/`--force`, an interactive prompt offers sobrescrever/manter/ver diff/abortar (diff rendered with `computeDiff`). +- On success, bumps `version` in `.docd.json` to the package version. + +### Template versioning (`src/manifest.ts`) + +Templates are `.md` files with YAML frontmatter (`name`, `version`, `description`). `parseFrontmatter`/`serializeFrontmatter` round-trip the frontmatter; the manifest walks `src/templates/` recursively (or `dist/templates/` when running from the built output — see `getTemplatesDir` in `src/paths.ts`). + +### Provider path mapping (`src/providers/base.ts`) + +`getInstalledPath` maps template relative paths to installed paths: `agent/*` → `//*`, `skill/*` → `//*` (subdirectory names are provider fields); anything else stays under `configDir`. Both current providers use the shared `.agents/` convention: project installs land in `.agents/agents/` and `.agents/skills/`; with `--global` they go to `~/.agents`, while `.docd.json` always stays in the project. + +## Design decisions (inferred) + +- **Provider-agnostic config**: `.docd.json` holds everything project-level (docs layout, ides, global flag); providers only control where agents/skills land. +- **Version + content dual check**: updates trigger on frontmatter version bump or real content change, so hand-edited templates are detected even without a version change. +- **Non-destructive updates**: backups are always created before overwrite; `--dry-run` never writes. +- **Optional doc files**: `specFile`, `guideFile`, and `uiFile` accept `null` to disable them (e.g. no-UI projects). + +## Assumptions / not derivable from code + +- The `cursor` IDE mentioned in older README examples has no provider implemented (`src/providers/index.ts` registers only Kilo and Kimi). +- Publishing/release process for `@linkysystems/docd` is not in the repo (only `prepublishOnly` build hook).