From bc68af6a4d887c0085a1d4f9d6f49450e894ab4f Mon Sep 17 00:00:00 2001 From: Paleo Date: Sun, 21 Jun 2026 12:52:37 +0200 Subject: [PATCH 1/7] feat: self-documenting docmap CLI + setup-guide skill --- AGENTS.md | 2 +- README.md | 2 +- alignfirst-skills.md | 51 +++----- docs/docmap-architecture.md | 23 ++-- docs/workspace-architecture.md | 4 +- packages/docmap/README.md | 36 ++++-- packages/docmap/package.json | 3 +- packages/docmap/src/cli.ts | 121 ++++++++++++++++-- packages/docmap/src/formatter.ts | 54 ++++++++ packages/docmap/templates/guide.md | 68 ++++++++++ packages/docmap/test/docmap.test.ts | 93 ++++++++++++-- .../docmap/test/fixtures/large/bulk/doc-01.md | 8 ++ .../docmap/test/fixtures/large/bulk/doc-02.md | 8 ++ .../docmap/test/fixtures/large/bulk/doc-03.md | 8 ++ .../docmap/test/fixtures/large/bulk/doc-04.md | 8 ++ .../docmap/test/fixtures/large/bulk/doc-05.md | 8 ++ .../docmap/test/fixtures/large/bulk/doc-06.md | 8 ++ .../docmap/test/fixtures/large/bulk/doc-07.md | 8 ++ .../docmap/test/fixtures/large/bulk/doc-08.md | 8 ++ .../docmap/test/fixtures/large/bulk/doc-09.md | 8 ++ .../docmap/test/fixtures/large/bulk/doc-10.md | 8 ++ .../docmap/test/fixtures/large/bulk/doc-11.md | 8 ++ .../docmap/test/fixtures/large/bulk/doc-12.md | 8 ++ .../docmap/test/fixtures/large/bulk/doc-13.md | 8 ++ .../docmap/test/fixtures/large/bulk/doc-14.md | 8 ++ .../docmap/test/fixtures/large/bulk/doc-15.md | 8 ++ .../docmap/test/fixtures/large/bulk/doc-16.md | 8 ++ .../docmap/test/fixtures/large/bulk/doc-17.md | 8 ++ .../docmap/test/fixtures/large/bulk/doc-18.md | 8 ++ .../docmap/test/fixtures/large/bulk/doc-19.md | 8 ++ .../docmap/test/fixtures/large/bulk/doc-20.md | 8 ++ .../test/fixtures/large/nested-a/inner.md | 8 ++ .../fixtures/large/only-subs/deep/leaf.md | 8 ++ packages/workspace/README.md | 6 +- skills/alignfirst-setup-guide/SKILL.md | 56 ++++++++ .../assets/dev-server.mjs | 0 .../assets/workspace.md | 0 .../assets/workspace.mjs | 0 .../references/alignfirst-skills-setup.md | 41 ++++++ .../references/alignfirst-upgrade-from-v1.md | 4 +- .../references/alignfirst-upgrade-from-v2.md | 4 +- .../references/alignfirst-upgrade.md | 20 +-- .../references/docmap-bootstrapping.md} | 0 .../docmap-migrate-existing-docs.md} | 0 .../references/docmap-migrate-skills.md} | 4 +- .../references/docmap-setup.md | 30 +++++ .../references/workspace-setup.md} | 39 ++---- skills/docmap/SKILL.md | 86 ------------- skills/docmap/references/installation.md | 79 ------------ 49 files changed, 701 insertions(+), 301 deletions(-) create mode 100644 packages/docmap/templates/guide.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-01.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-02.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-03.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-04.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-05.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-06.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-07.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-08.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-09.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-10.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-11.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-12.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-13.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-14.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-15.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-16.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-17.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-18.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-19.md create mode 100644 packages/docmap/test/fixtures/large/bulk/doc-20.md create mode 100644 packages/docmap/test/fixtures/large/nested-a/inner.md create mode 100644 packages/docmap/test/fixtures/large/only-subs/deep/leaf.md create mode 100644 skills/alignfirst-setup-guide/SKILL.md rename skills/{workspace-guide => alignfirst-setup-guide}/assets/dev-server.mjs (100%) rename skills/{workspace-guide => alignfirst-setup-guide}/assets/workspace.md (100%) rename skills/{workspace-guide => alignfirst-setup-guide}/assets/workspace.mjs (100%) create mode 100644 skills/alignfirst-setup-guide/references/alignfirst-skills-setup.md rename migrations/upgrade-from-v1.md => skills/alignfirst-setup-guide/references/alignfirst-upgrade-from-v1.md (89%) rename migrations/upgrade-from-v2.md => skills/alignfirst-setup-guide/references/alignfirst-upgrade-from-v2.md (88%) rename migrations/upgrade.md => skills/alignfirst-setup-guide/references/alignfirst-upgrade.md (74%) rename skills/{docmap/references/bootstrapping-documentation.md => alignfirst-setup-guide/references/docmap-bootstrapping.md} (100%) rename skills/{docmap/references/migrate-existing-docs.md => alignfirst-setup-guide/references/docmap-migrate-existing-docs.md} (100%) rename skills/{docmap/references/migrate-skills-to-docmap.md => alignfirst-setup-guide/references/docmap-migrate-skills.md} (92%) create mode 100644 skills/alignfirst-setup-guide/references/docmap-setup.md rename skills/{workspace-guide/SKILL.md => alignfirst-setup-guide/references/workspace-setup.md} (94%) delete mode 100644 skills/docmap/SKILL.md delete mode 100644 skills/docmap/references/installation.md diff --git a/AGENTS.md b/AGENTS.md index cbfeba5..841c666 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ ## Docmap - Seek Documentation -**Before any investigation or code exploration**, run `npm run docmap` to list the documentation index. This is mandatory for every task — do not skip it. Browse subdirectories or read files by passing them as arguments (`npm run docmap -- topic-a docs/topic-b/doc.md`), or list everything (`npm run docmap -- --recursive`). +**Before any investigation or code exploration**, run `npm run docmap`, then read the relevant documentation. Mandatory for every task. ## AlignFirst - Ticket ID, Commit Message, Branch Name diff --git a/README.md b/README.md index 52b7123..41517a9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Collaborative spec/plan/AAD/review protocols. See [alignfirst-skills.md](alignfi ## Docmap - Agent-discoverable documentation -`@paleo/docmap` — a lightweight table of contents that lets agents navigate documentation files. This way we have one set of docs, shared by humans and AI agents. See [packages/docmap/README.md](packages/docmap/README.md). +`@paleo/docmap` — a lightweight table of contents that lets agents navigate documentation files. This way we have one set of docs, shared by humans and AI agents. Run `docmap --guide` for authoring conventions; the `alignfirst-setup-guide` skill installs it in a new repo. See [packages/docmap/README.md](packages/docmap/README.md). ## Workspaces - Local environments with worktrees diff --git a/alignfirst-skills.md b/alignfirst-skills.md index cb5eb9e..b732349 100644 --- a/alignfirst-skills.md +++ b/alignfirst-skills.md @@ -19,38 +19,20 @@ npx skills add https://github.com/paleo/alignfirst --global --skill alignfirst - ### Configure your project (optional) -This adds `.plans` to `.gitignore` and an AlignFirst section to your `AGENTS.md` (or `CLAUDE.md`). Give your agent this prompt: +This adds `.plans` to `.gitignore` and an AlignFirst section to your `AGENTS.md` (or `CLAUDE.md`). Install the setup-guide skill temporarily: -```markdown -I just installed the alignfirst skill. Help me configure it: - -1. Create `.plans/` directory if it doesn't exist, and add `.plans` to `.gitignore` if needed. -2. Check if `AGENTS.md` or `CLAUDE.md` exists. If one exists, use it. If neither exists, create `AGENTS.md`. This file is our INSTRUCTION_FILE. -3. Look at our git branches (`git branch -a`) to detect our ticket ID format (e.g., `ABC-###`, `PROJ-###`, or numeric). - - If no pattern is found, ask me for our ticket ID format: - - > "I couldn't detect a ticket ID format from the branch names. Please provide the ticket ID format (e.g., "numeric", `ABC-###`, etc.) or type 'skip' to omit." - -4. From our recent commit messages (`git log --oneline -20`), deduce the commit message convention (e.g., `: [] description`, `(): description`, `[] description`, etc.). - - If no pattern is found, ask me for our commit message convention: - - > "I couldn't detect a commit message convention. Please describe it (e.g., `feat: [AB-123] short description`, `type(scope): description`, etc.) or type 'skip' to omit." - -5. Detect the default branch with `git remote show origin | grep "HEAD branch"` (e.g., `main`, `master`, `develop`). +```bash +npx skills add https://github.com/paleo/alignfirst --skill alignfirst-setup-guide +``` -6. Insert the following into the INSTRUCTION_FILE (skip any part already present): - - Add this line where it feels appropriate: "Always ignore the `.plans` directory when searching the codebase." - - If a ticket ID format was found, add this section (include each convention line only if one was detected or provided): +Then ask your agent: - > ## AlignFirst - Ticket ID, Commit Message, Default Branch - > - > _Ticket ID:_ Format is `{DETECTED_FORMAT}`. Use the ticket ID if explicitly provided. Otherwise, deduce it from the current branch name (no confirmation needed). If the branch name is unavailable, get it via `git branch --show-current`. Only ask the user as a last resort. - > - > _Commit message convention:_ `{DETECTED_CONVENTION}` - > - > _Default branch:_ `{DETECTED_DEFAULT_BRANCH}` +```text +Use your alignfirst-setup-guide skill. Configure the AlignFirst skills in this project. ``` +Once done, you can uninstall the setup-guide skill — it won't be used by your project anymore. + > **Note (2026-03-09):** On Cursor, to make the skills available as commands (using `/`), I had to create a symlink: `cd ~/.cursor/ && ln -s ../.agents/skills .` ## Usage @@ -152,28 +134,25 @@ Specs, plans, and summaries should be written in well-organized (git-ignored) lo ## Upgrade from v1 or v2 -1. Install the docmap skill: +1. Install the setup-guide skill: ```bash - npx skills add https://github.com/paleo/alignfirst --skill docmap + npx skills add https://github.com/paleo/alignfirst --skill alignfirst-setup-guide ``` - Then, ask your agent to install the docmap CLI: +2. Ask your agent to run the upgrade: ```text - Use your docmap skill. Install docmap CLI in this project. + Use your alignfirst-setup-guide skill. Upgrade AlignFirst in this project. ``` - At the end, the agent will suggest available instructions: ignore them, we will handle that in the prompt of step 2. - -2. Give your agent **[this upgrade prompt](https://raw.githubusercontent.com/paleo/alignfirst/refs/heads/main/migrations/upgrade.md)**. -3. Install the new alignfirst skill: +3. Install the new alignfirst skills: ```bash npx skills add https://github.com/paleo/alignfirst --global --skill alignfirst --skill al --skill alplan --skill alspec --skill aldescription --skill alreview --skill alread --skill almerge ``` -> **Note:** We recommend installing the alignfirst skills globally so they're easier to update. For the docmap skill, prefer a local/project installation. +> **Note:** We recommend installing the alignfirst skills globally so they're easier to update. ## License diff --git a/docs/docmap-architecture.md b/docs/docmap-architecture.md index 2c58fb9..4cd4388 100644 --- a/docs/docmap-architecture.md +++ b/docs/docmap-architecture.md @@ -20,21 +20,24 @@ Three TypeScript modules in `packages/docmap/src/`, compiled to `packages/docmap | --- | --- | | `src/parser.ts` | YAML frontmatter extraction (`extractMetadata`) and stripping (`stripFrontmatter`). | | `src/formatter.ts` | Directory reading, listing/formatting, name validation, `--check`, and file resolution (`readDocFile`, direct path then fuzzy basename search; returns `undefined` when nothing resolves). | -| `src/cli.ts` | Argument parsing, flow orchestration, package-manager detection for tips. | +| `src/cli.ts` | Argument parsing, flow orchestration, package-manager detection, and help/guide rendering (the `--guide` body is read from `templates/guide.md`). | Entry point: `packages/docmap/bin/docmap.mjs` → imports `packages/docmap/dist/cli.js` → calls `main()` → returns an exit code. +The `--guide` text lives as Markdown in `packages/docmap/templates/guide.md` with two placeholders: `{{PM}}` for a bare invocation and `{{PM_ARGS}}` for one that forwards arguments. They differ only for npm, whose `run` script needs a `--` separator before args (`npm run docmap` vs `npm run docmap --`); every other manager forwards args verbatim, so both placeholders resolve to the same command. `renderGuide()` reads the file via `new URL("../templates/guide.md", import.meta.url)` — the path resolves from both `src/cli.ts` (dev/test) and `dist/cli.js` (published), each one level under the package root — and substitutes both tokens. `templates/` is listed in `package.json` `files` so it ships with the package. + ## CLI Flow `main()` in `src/cli.ts` drives everything: -1. **Parse args** — `parseArgs()` extracts `--recursive`, `--root`, `--check`, collects positional **paths**, and collects unknown `--flags`. Tokens starting with `--` are never paths; an unknown one is recorded and later warned about on stderr (so a stale `docmap --dir topic-a` still works — `--dir` is skipped, `topic-a` is a positional). `parseArgs` is pure (no writes). +1. **Parse args** — `parseArgs()` extracts the mode flags `--help`, `--guide`, `--search `, `--recursive`, `--root `, `--check`, collects positional **paths**, and collects unknown `--flags`. `--search` consumes the next token as its value (same shape as `--root`); without a following token it is recorded as unknown. Tokens starting with `--` are never paths; an unknown one is recorded and later warned about on stderr (so a stale `docmap --dir topic-a` still works — `--dir` is skipped, `topic-a` is a positional). `parseArgs` is pure (no writes). 2. **Resolve base directory** — `--root` or default `docs/` relative to `cwd`. The **display prefix** is `relative(cwd, baseDir)` — `docs` by default, or the root's path for a custom `--root` (e.g. `config/docs`, or `../shared` outside cwd). All displayed paths carry this prefix, so they stay real and openable; the empty prefix (root === cwd) yields bare paths. -3. **`--check`** → `checkAll()` validates every file and directory name (shell-safe regex) and every `.md` frontmatter. Returns exit code 1 if any issues. (Only `--check` returns non-zero.) -4. **Classify positionals** — each path is normalized (trailing slashes stripped, leading display prefix removed; the bare prefix → root), resolved under the base dir, then `statSync`-tested. Existing directory → listing bucket; everything else (existing file, or missing) → read bucket. `statSync` throwing on a missing path is caught and treated as "not a directory". The filesystem is the source of truth — no extension heuristic. -5. **Listings** — produced for each directory-classified path via `listDirectory()`/`formatDirectory()` or `formatRecursive()`. With no positionals at all, or with `--recursive` and no directory positionals, the root is listed. Files-only with `--recursive` off produces no listing. -6. **Reads** — each read-bucket path goes through `readDocFile()` (direct path, then fuzzy basename search). A resolved result is wrapped in `` tags; an unresolved one becomes a single generic `⚠ Not found: ` line (same wording for a mistyped file or directory — classification can't read intent, and a not-found read keeps exit code 0). Reads follow listings, separated by a blank line. -7. **Tip** — After a listing, one contextual tip (`formatTip`) is appended explaining that several paths can be passed at once; the example shows directory args only when subdirs exist and file args (carrying the display prefix) only when files exist. The package manager is auto-detected from lockfiles. +3. **Mode dispatch** — modes are tried in precedence and each returns early after printing only its own output: `--help` (full help) → `--guide` (authoring guide) → `--search` (frontmatter match) → `--check` (validation) → listing/read. `--help`/`--guide`/`--search` always return `0`; only `--check` can return `1`. Help and guide text is rendered from the auto-detected package manager so every shown command is copy-pasteable. +4. **`--search`** → `searchDocs()` walks the tree (`collectAllFiles`), reads each file, and keeps the ones whose joined `title + summary + read_when` text contains every whitespace-split term (case-insensitive; body text is not searched). Matches render as file bullets, or a single `No documents match: ` line. +5. **Classify positionals** — each path is normalized (trailing slashes stripped, leading display prefix removed; the bare prefix → root), resolved under the base dir, then `statSync`-tested. Existing directory → listing bucket; everything else (existing file, or missing) → read bucket. `statSync` throwing on a missing path is caught and treated as "not a directory". The filesystem is the source of truth — no extension heuristic. +6. **Listings** — produced for each directory-classified path via `listDirectory()`/`formatDirectory()` or `formatRecursive()`. With no positionals at all, or with `--recursive` and no directory positionals, the root is listed. A **bare** invocation (no positionals, none of `--recursive`/`--check`/`--help`/`--guide`/`--search`) lists recursively when the tree holds fewer than 20 `.md` files (`collectAllFiles` count), and is the only case prefixed with short help; at ≥20 it keeps the top-level listing. An explicit `--recursive` is unaffected. Files-only with `--recursive` off produces no listing. +7. **Reads** — each read-bucket path goes through `readDocFile()` (direct path, then fuzzy basename search). A resolved result is wrapped in `` tags; an unresolved one becomes a single generic `⚠ Not found: ` line (same wording for a mistyped file or directory — classification can't read intent, and a not-found read keeps exit code 0). Reads follow listings, separated by a blank line. +8. **Tip** — After a listing, one contextual tip (`formatTip`) is appended explaining that several paths can be passed at once; the example shows directory args only when subdirs exist and file args (carrying the display prefix) only when files exist. The package manager is auto-detected from lockfiles, falling back to bare `docmap` when no lockfile is found. ## Frontmatter Contract @@ -74,8 +77,8 @@ Warnings (⚠) appear inline for name issues or frontmatter errors. | `npm -w @paleo/docmap test` | Run tests with Vitest | | `npm run lint` | Biome linter (root) | -Tests live in `packages/docmap/test/docmap.test.ts` and use fixture directories under `packages/docmap/test/fixtures/` (basic, errors, empty, nested, bad-names, subdirs-only). Each fixture is a self-contained `docs/`-like tree passed via `--root`. +Tests live in `packages/docmap/test/docmap.test.ts` and use fixture directories under `packages/docmap/test/fixtures/` (basic, errors, empty, nested, bad-names, subdirs-only, classify, no-frontmatter, large). Each fixture is a self-contained `docs/`-like tree passed via `--root`. The `large` fixture (≥20 `.md` files) exercises the top-level listing kept above the recursive-default threshold. -## Agent Skill +## Authoring Guide and Setup -The `skills/docmap/` directory ships a reusable agent skill that teaches AI agents how to use docmap, write documents, install it in a project, bootstrap a `docs/` directory, and migrate documentation from skills. It is distributed separately from the npm package. +The authoring conventions (frontmatter contract, naming, workflow) ship with the CLI as `templates/guide.md`: `docmap --guide` prints that file, rendered with the project's package-manager prefix. Project setup — bootstrapping a `docs/` tree, wiring docmap into a repo, and migrating existing docs or skills — lives in the separate `alignfirst-setup-guide` skill, distributed outside the npm package. diff --git a/docs/workspace-architecture.md b/docs/workspace-architecture.md index 4aa2e6c..93a87e5 100644 --- a/docs/workspace-architecture.md +++ b/docs/workspace-architecture.md @@ -1,6 +1,6 @@ --- title: Workspace Package Architecture -summary: Internals of the `@paleo/workspace` kernel — foreground self-exit, stop/teardown signal mechanics, cross-worktree callback dispatch, the `workspace remove` re-exec, the concurrency-cap race, and registry liveness. Complements the `workspace-guide` skill (the consumer-facing blueprint). +summary: Internals of the `@paleo/workspace` kernel — foreground self-exit, stop/teardown signal mechanics, cross-worktree callback dispatch, the `workspace remove` re-exec, the concurrency-cap race, and registry liveness. Complements the workspace setup blueprint (`skills/alignfirst-setup-guide/references/workspace-setup.md`), the consumer-facing guide. read_when: - onboarding to the @paleo/workspace codebase - changing dev-server start/stop, foreground, or eviction behavior @@ -10,7 +10,7 @@ read_when: # Workspace Package Architecture -For the consumer-facing blueprint — concepts, config fields, the CLI surface, and how to adapt the system to a repository — see the [`workspace-guide` skill](../skills/workspace-guide/SKILL.md). This document covers package internals and edge-case behavior that don't belong in that guide. +For the consumer-facing blueprint — concepts, config fields, the CLI surface, and how to adapt the system to a repository — see the [workspace setup blueprint](../skills/alignfirst-setup-guide/references/workspace-setup.md). This document covers package internals and edge-case behavior that don't belong in that guide. ## Foreground self-exit diff --git a/packages/docmap/README.md b/packages/docmap/README.md index 3e6527f..2b2e5ba 100644 --- a/packages/docmap/README.md +++ b/packages/docmap/README.md @@ -2,24 +2,30 @@ A lightweight documentation system for AI agents and humans. Keep project docs in a `docs/` folder with YAML frontmatter, browse and read them from the terminal. -Docmap is both a **npm package** (this CLI; it lists, reads, and validates docs) and an **agent skill** `docmap` (conventions and workflows that teach AI agents how to write, organize, and migrate documentation). You need both: the package provides the tooling, the skill provides the knowledge. +The CLI lists, reads, and validates docs, and ships its own authoring guide: run `docmap --guide` to learn the conventions for writing and organizing documents. _Inspired by the [OpenClaw](https://github.com/openclaw/openclaw/) docs system, which uses [Mintlify](https://www.mintlify.com/). This project doesn't depend on Mintlify._ ## Installation -Start by installing the skill: +Install the CLI as a dev dependency and add a `docmap` script: ```bash -npx skills add https://github.com/paleo/alignfirst --skill docmap +npm install -D @paleo/docmap ``` -> **Note:** We recommend installing the docmap skill locally in each project. +```json +{ + "scripts": { + "docmap": "docmap" + } +} +``` -Start a new session, then ask your agent to do its magic: +To bootstrap a `docs/` directory, wire docmap into a project, and migrate existing docs or skills, install the `alignfirst-setup-guide` skill and let an agent drive the setup: -```text -Use your docmap skill. Install docmap CLI in this project. +```bash +npx skills add https://github.com/paleo/alignfirst --skill alignfirst-setup-guide ``` ## How It Works @@ -53,9 +59,15 @@ read_when: Targets are **positional arguments**. The CLI inspects the filesystem and classifies each one itself — a directory is listed, a file is read — so you never have to pre-declare which is which. ```bash -# List root-level documents +# Short help, then the listing (lists recursively when the doc set is small) npx @paleo/docmap +# Full help +npx @paleo/docmap --help + +# Authoring guide (conventions for writing documents) +npx @paleo/docmap --guide + # List one or more subdirectories npx @paleo/docmap topic-a npx @paleo/docmap topic-a topic-b @@ -70,6 +82,9 @@ npx @paleo/docmap docs/topic-a/doc-1.md docs/topic-b/doc-2.md # Mix directories and files in one call npx @paleo/docmap topic-a docs/topic-b/doc-2.md +# Search frontmatter (title, summary, read_when); every term must match +npx @paleo/docmap --search "api endpoint" + # Validate all files (names, frontmatter) npx @paleo/docmap --check @@ -77,6 +92,8 @@ npx @paleo/docmap --check npx @paleo/docmap --root path/to/docs ``` +A bare invocation lists recursively when the tree holds fewer than 20 documents, and falls back to a top-level listing above that threshold. An explicit `--recursive` always walks the whole tree. + ### Classification Each positional path is resolved against the docs root: @@ -91,6 +108,9 @@ Listings display each path prefixed with the docs root **relative to your workin | Option | Description | | --- | --- | +| `--help` | Print full help and exit. | +| `--guide` | Print the authoring guide (conventions for writing documents) and exit. | +| `--search ` | List documents whose frontmatter (title, summary, read_when) matches every whitespace-separated term. | | `--recursive` | Walk the entire tree. Applies to directory listings (root or positional). | | `--check` | Validate all files and directories. Reports name and frontmatter issues. | | `--root ` | Use a custom directory as the docs root instead of `docs/`. | diff --git a/packages/docmap/package.json b/packages/docmap/package.json index b988104..2b5bef7 100644 --- a/packages/docmap/package.json +++ b/packages/docmap/package.json @@ -26,7 +26,8 @@ }, "files": [ "bin", - "dist" + "dist", + "templates" ], "publishConfig": { "access": "public" diff --git a/packages/docmap/src/cli.ts b/packages/docmap/src/cli.ts index 0037843..4db6b4b 100644 --- a/packages/docmap/src/cli.ts +++ b/packages/docmap/src/cli.ts @@ -1,16 +1,21 @@ -import { existsSync, statSync } from "node:fs"; +import { existsSync, readFileSync, statSync } from "node:fs"; import { dirname, join, relative, resolve } from "node:path"; import { checkAll, collectAllFiles, + countFilesUpTo, formatDirectory, formatRecursive, isUnder, listDirectory, readDocFile, + searchDocs, type FormatResult, } from "./formatter.js"; +// A bare invocation over fewer .md files than this lists recursively by default. +const SMALL_SET_THRESHOLD = 20; + export interface MainOptions { argv?: string[]; stdout?: { write(s: string): void }; @@ -24,7 +29,7 @@ export function main(options?: MainOptions): number { const stderr = options?.stderr ?? process.stderr; const cwd = options?.cwd ?? process.cwd(); - const { paths, unknownFlags, recursive, root, check } = parseArgs(argv); + const { paths, unknownFlags, recursive, root, check, help, guide, search } = parseArgs(argv); const baseDir = root ? resolve(cwd, root) : resolve(cwd, "docs"); // cwd-relative form of baseDir, prepended to every displayed path so output is copy-pasteable. // A `--root` outside cwd yields a `..`-leading prefix; paths still strip and resolve correctly. @@ -32,15 +37,48 @@ export function main(options?: MainOptions): number { for (const flag of unknownFlags) stderr.write(`Unknown option: ${flag} (ignored)\n`); + const pm = detectPackageManager(cwd); + + // Mode precedence (each prints only its own output, then returns): help → guide → search → + // check → listing/read. + if (help) { + stdout.write(renderHelp(pm, { full: true })); + return 0; + } + if (guide) { + stdout.write(renderGuide(pm)); + return 0; + } + if (search !== undefined) { + const terms = search.split(/\s+/).filter((term) => term.length > 0); + const lines = searchDocs(baseDir, terms, prefix); + stdout.write( + lines.length > 0 ? `${lines.join("\n")}\n` : `No documents match: ${terms.join(" ")}\n`, + ); + return 0; + } if (check) { const issues = checkAll(baseDir, "", prefix); for (const issue of issues) stdout.write(`${issue.path}: ${issue.message}\n`); return issues.length > 0 ? 1 : 0; } + // A bare invocation over a small doc set lists recursively and is the only case that + // prefixes the listing with short help. + const bare = paths.length === 0 && !recursive; + const smallSet = bare && countFilesUpTo(baseDir, SMALL_SET_THRESHOLD) < SMALL_SET_THRESHOLD; + if (smallSet) stdout.write(`${renderHelp(pm, { full: false })}\n`); + const { dirs, files } = classifyTargets(baseDir, paths, prefix); - const listing = renderListing(baseDir, dirs, recursive, paths.length === 0, cwd, prefix); + const listing = renderListing( + baseDir, + dirs, + recursive || smallSet, + paths.length === 0, + cwd, + prefix, + ); if (listing) stdout.write(listing); const reads = renderReads(baseDir, files, prefix); @@ -58,6 +96,9 @@ export interface ParsedArgs { recursive: boolean; root: string | undefined; check: boolean; + help: boolean; + guide: boolean; + search: string | undefined; } export function parseArgs(argv: string[]): ParsedArgs { @@ -67,15 +108,24 @@ export function parseArgs(argv: string[]): ParsedArgs { let recursive = false; let root: string | undefined; let check = false; + let help = false; + let guide = false; + let search: string | undefined; for (let i = 0; i < args.length; ++i) { const arg = args[i]; if (arg === "--root" && i + 1 < args.length) { root = args[++i]; + } else if (arg === "--search" && i + 1 < args.length) { + search = args[++i]; } else if (arg === "--recursive") { recursive = true; } else if (arg === "--check") { check = true; + } else if (arg === "--help") { + help = true; + } else if (arg === "--guide") { + guide = true; } else if (arg.startsWith("--")) { unknownFlags.push(arg); } else { @@ -83,7 +133,48 @@ export function parseArgs(argv: string[]): ParsedArgs { } } - return { paths, unknownFlags, recursive, root, check }; + return { paths, unknownFlags, recursive, root, check, help, guide, search }; +} + +interface HelpOptions { + full: boolean; +} + +interface PackageManagerCommands { + base: string; + withArgs: string; +} + +function renderHelp(pm: PackageManagerCommands, { full }: HelpOptions): string { + const lines = [ + "docmap — browse and read a project's docs/ tree of Markdown files.", + "", + "Browse:", + ` ${pm.base} # list root documents`, + ` ${pm.withArgs} topic-a # list a subdirectory`, + ` ${pm.withArgs} topic-a/doc.md # read a document`, + ` ${pm.withArgs} --recursive # list every document`, + "", + `To write documentation, run \`${pm.withArgs} --guide\` first.`, + ]; + if (full) { + lines.push( + "", + "More:", + ` ${pm.withArgs} --search "term1 term2" # match frontmatter (title, summary, read_when)`, + ` ${pm.withArgs} --check # validate names and frontmatter`, + ` ${pm.withArgs} --root # use a custom docs root`, + "", + "Positional paths are classified by the filesystem: a directory is listed, a file is read,", + "an unmatched name falls back to a fuzzy basename search. The docs/ prefix is optional on input.", + ); + } + return `${lines.join("\n")}\n`; +} + +function renderGuide(pm: PackageManagerCommands): string { + const template = readFileSync(new URL("../templates/guide.md", import.meta.url), "utf-8"); + return template.replaceAll("{{PM_ARGS}}", pm.withArgs).replaceAll("{{PM}}", pm.base); } interface ClassifiedTargets { @@ -168,8 +259,8 @@ function formatTip( displayExample(prefix, "dir-name-b", "doc-2.md"), ); if (examples.length === 0) return; - const pm = detectPackageManager(cwd); - return `Tip: Pass several paths in one call — directories are listed, files are read. E.g. \`${pm} ${examples.join(" ")}\`.\n`; + const { withArgs } = detectPackageManager(cwd); + return `Tip: Pass several paths in one call — directories are listed, files are read. E.g. \`${withArgs} ${examples.join(" ")}\`.\n`; } function displayExample(prefix: string, dir: string, file: string): string { @@ -211,16 +302,22 @@ function renderReads(baseDir: string, files: FileTarget[], prefix: string): stri return `${blocks.join("\n\n")}\n`; } -function detectPackageManager(cwd: string): string { +function detectPackageManager(cwd: string): PackageManagerCommands { let dir = cwd; while (true) { - if (existsSync(join(dir, "package-lock.json"))) return "npm run docmap --"; - if (existsSync(join(dir, "pnpm-lock.yaml"))) return "pnpm docmap"; - if (existsSync(join(dir, "yarn.lock"))) return "yarn docmap"; + if (existsSync(join(dir, "package-lock.json"))) + return { base: "npm run docmap", withArgs: "npm run docmap --" }; + if (existsSync(join(dir, "pnpm-lock.yaml"))) return sameCommand("pnpm docmap"); + if (existsSync(join(dir, "yarn.lock"))) return sameCommand("yarn docmap"); if (existsSync(join(dir, "bun.lockb")) || existsSync(join(dir, "bun.lock"))) - return "bun run docmap"; + return sameCommand("bun run docmap"); const parent = dirname(dir); - if (parent === dir) return "npx docmap"; + if (parent === dir) return sameCommand("docmap"); dir = parent; } } + +// Only npm needs a `--` separator before forwarded args; every other manager passes them verbatim. +function sameCommand(command: string): PackageManagerCommands { + return { base: command, withArgs: command }; +} diff --git a/packages/docmap/src/formatter.ts b/packages/docmap/src/formatter.ts index 23a2cfe..ec73b35 100644 --- a/packages/docmap/src/formatter.ts +++ b/packages/docmap/src/formatter.ts @@ -239,6 +239,34 @@ export function readDocFile( return { path: displayPath(prefix, found), content: stripFrontmatter(content) }; } +export function searchDocs(baseDir: string, terms: string[], prefix: string): string[] { + const needles = terms.map((term) => term.toLowerCase()); + const lines: string[] = []; + for (const rel of collectAllFiles(baseDir, "")) { + const slash = rel.lastIndexOf("/"); + const relDir = slash === -1 ? "" : rel.slice(0, slash); + const name = slash === -1 ? rel : rel.slice(slash + 1); + const content = readFileSync(join(baseDir, rel), "utf-8"); + const meta = extractMetadata(content); + const title = meta.title ?? extractFallbackTitle(content); + const haystack = [title, meta.summary, ...meta.readWhen] + .filter((part): part is string => part !== undefined) + .join(" ") + .toLowerCase(); + if (!needles.every((needle) => haystack.includes(needle))) continue; + const entry: FileEntry = { + name, + title, + summary: meta.summary, + readWhen: meta.readWhen, + error: meta.error, + nameError: validateName(name), + }; + lines.push(...formatFileBullets([entry], relDir, prefix)); + } + return lines; +} + export function collectAllFiles(dirPath: string, prefix: string): string[] { const result: string[] = []; let entries: Dirent[]; @@ -258,6 +286,32 @@ export function collectAllFiles(dirPath: string, prefix: string): string[] { return result; } +// Counts `.md` files (recursively, CHANGELOG* excluded) but stops walking as soon as `limit` +// is reached, so a multi-thousand-file tree is not fully traversed just to compare against a +// small threshold. The returned count is capped at `limit`. +export function countFilesUpTo(dirPath: string, limit: number): number { + let count = 0; + const walk = (dir: string): boolean => { + let entries: Dirent[]; + try { + entries = readdirSync(dir, { withFileTypes: true }); + } catch { + return false; + } + for (const entry of entries) { + if (entry.isDirectory()) { + if (walk(join(dir, entry.name))) return true; + } else if (entry.name.endsWith(".md") && !shouldSkipFile(entry.name)) { + count++; + if (count >= limit) return true; + } + } + return false; + }; + walk(dirPath); + return count; +} + function displayPath(...parts: string[]): string { return parts.filter((part) => part.length > 0).join("/"); } diff --git a/packages/docmap/templates/guide.md b/packages/docmap/templates/guide.md new file mode 100644 index 0000000..fe6d8a8 --- /dev/null +++ b/packages/docmap/templates/guide.md @@ -0,0 +1,68 @@ +# Authoring Documentation + +All project documentation lives in the `docs/` directory. The `docmap` CLI lets humans and AI agents discover and read documents without leaving the terminal. + +## Browsing with CLI + +Targets are positional paths; the CLI classifies each by inspecting the filesystem (directory → list, file → read). Pass several at once. The `docs/` prefix shown in listings is optional on input. + +```bash +{{PM}} # list root docs +{{PM_ARGS}} topic-a topic-b/sub-topic-c # list subdirectories +{{PM_ARGS}} --recursive # list everything +{{PM_ARGS}} doc-1.md topic-a/doc-2.md # read documents (frontmatter stripped) +{{PM_ARGS}} topic-a topic-a/doc-1.md # mix listing and reading +{{PM_ARGS}} --search "term1 term2" # search frontmatter +{{PM_ARGS}} --check # validate all files +``` + +## Workflow + +1. **Understand the subject** — clarify what to document. Ask the user if unclear. +2. **Determine placement** — scan existing docs (`{{PM_ARGS}} --recursive`). Decide: new file, existing file, or subdirectory. Discuss if unclear. +3. **Write** — follow the conventions below. + +## Writing Guidelines + +- **Audience:** an experienced newcomer — technically capable but unfamiliar with this project. +- Be brief and specific — no obvious information, no generic best practices. +- Typical document: 40–80 lines. +- Prefer referencing source files over large code blocks. +- If the title makes the purpose obvious, omit the `summary`. + +## File and Directory Naming + +- Use **lowercase-with-dashes** (kebab-case) for new files and directories. +- Uppercase is allowed (e.g. `RELEASING.md`). +- Names must be **shell-safe**: no spaces, quotes, or special characters. Verify with `{{PM_ARGS}} --check`. +- Use `.md` for all documents. +- Use short, descriptive names. +- Group related documents into subdirectories; nesting is allowed. + +## YAML Frontmatter + +`.md` files may start with a YAML frontmatter block. Add it when it adds value — especially when the filename or heading alone is not explicit enough. It is optional; when absent, the CLI falls back to the first `# heading` for the title. + +| Field | Required | Description | +| --- | --- | --- | +| `title` | No | Display name shown in listings. Falls back to the first `# heading` when absent. | +| `summary` | No | One concise sentence. Omit when the title already makes the purpose obvious. | +| `read_when` | No | A YAML list of short, action-oriented hints. Each completes: *"Read this document when you are…"* | + +## Document Body + +After the closing `---`, write standard Markdown — headings, code blocks, tables, lists as needed. + +```markdown +--- +title: Your Title Here +summary: A one-sentence description of what this document covers. +read_when: + - first situation when this document is useful + - second situation +--- + +# Your Title Here + +Start your content here… +``` diff --git a/packages/docmap/test/docmap.test.ts b/packages/docmap/test/docmap.test.ts index 0fa5945..966a1fe 100644 --- a/packages/docmap/test/docmap.test.ts +++ b/packages/docmap/test/docmap.test.ts @@ -15,6 +15,7 @@ const fixtures = { badNames: resolve(__dirname, "fixtures/bad-names"), noFrontmatter: resolve(__dirname, "fixtures/no-frontmatter"), classify: resolve(__dirname, "fixtures/classify"), + large: resolve(__dirname, "fixtures/large"), }; const TIP = "Pass several paths in one call"; @@ -50,10 +51,11 @@ function invoke(argv: string[], cwd: string) { return { code, stdout: stdout.join(""), stderr: stderr.join("") }; } -describe("default listing (basic fixture)", () => { - it("lists root files with titles and summaries", () => { +describe("recursive-by-default for small sets (basic fixture)", () => { + it("lists root files with titles and summaries, prefixed by short help", () => { const { code, stdout } = run([], fixtures.basic); expect(code).toBe(0); + expect(stdout).toContain("--guide"); expect(stdout).toContain(dp(fixtures.basic, "code-style.md")); expect(stdout).toContain("Code Style"); expect(stdout).toContain("Conventions and formatting rules for the codebase."); @@ -61,18 +63,43 @@ describe("default listing (basic fixture)", () => { expect(stdout).toContain("Getting Started"); }); - it("shows Sub-directories section", () => { + it("recurses into subdirectories without a flag", () => { const { stdout } = run([], fixtures.basic); + expect(stdout).toContain("## `backend/`"); + expect(stdout).toContain(dp(fixtures.basic, "backend/api-guide.md")); + expect(stdout).toContain("## `frontend/`"); + expect(stdout).toContain(dp(fixtures.basic, "frontend/components.md")); + expect(stdout).not.toContain("## Sub-directories"); + }); + + it("does not print short help for a positional directory", () => { + const { stdout } = run(["backend"], fixtures.basic); + expect(stdout).not.toContain("--guide"); + }); +}); + +describe("top-level listing for large sets (large fixture)", () => { + it("counts files recursively for the threshold: 20 docs nested under bulk/ still stay top-level", () => { + const { code, stdout } = run([], fixtures.large); + expect(code).toBe(0); expect(stdout).toContain("## Sub-directories"); - expect(stdout).toContain("- backend/"); - expect(stdout).toContain("- frontend/"); + expect(stdout).toContain("- bulk/"); + expect(stdout).toContain("- nested-a/"); + // Top-level mode does not descend into subdirs, so the nested docs are not expanded. + expect(stdout).not.toContain(dp(fixtures.large, "bulk/doc-01.md")); + expect(stdout).not.toContain(dp(fixtures.large, "nested-a/inner.md")); }); - it("shows a tip with both directory and file examples", () => { - const { stdout } = run([], fixtures.basic); + it("does not prefix the listing with short help", () => { + const { stdout } = run([], fixtures.large); + expect(stdout).not.toContain("--guide"); + }); + + it("shows a tip with only the directory example when no files sit at the top level", () => { + const { stdout } = run([], fixtures.large); expect(stdout).toContain(TIP); expect(stdout).toContain(DIR_EXAMPLE); - expect(stdout).toContain(FILE_EXAMPLE); + expect(stdout).not.toContain(FILE_EXAMPLE); }); }); @@ -311,7 +338,7 @@ describe("tip conditions", () => { }); it("only subdirs (no files) shows only the directory example", () => { - const { stdout } = run([], fixtures.subdirsOnly); + const { stdout } = run(["only-subs"], fixtures.large); expect(stdout).toContain(TIP); expect(stdout).toContain(DIR_EXAMPLE); expect(stdout).not.toContain(FILE_EXAMPLE); @@ -379,6 +406,54 @@ describe("--check", () => { }); }); +describe("--help", () => { + it("prints full help with the extra-option markers and no listing", () => { + const { code, stdout } = run(["--help"], fixtures.basic); + expect(code).toBe(0); + expect(stdout).toContain("--guide"); + expect(stdout).toContain("--search"); + expect(stdout).toContain("--check"); + expect(stdout).not.toContain("# Documentation"); + expect(stdout).not.toContain(dp(fixtures.basic, "code-style.md")); + expect(stdout).not.toContain(" { + it("prints the authoring guide and no listing", () => { + const { code, stdout } = run(["--guide"], fixtures.basic); + expect(code).toBe(0); + expect(stdout).toContain("# Authoring Documentation"); + expect(stdout).toContain("YAML Frontmatter"); + expect(stdout).not.toContain("# Documentation"); + expect(stdout).not.toContain(dp(fixtures.basic, "code-style.md")); + expect(stdout).not.toContain(" { + it("matches a single term against frontmatter", () => { + const { code, stdout } = run(["--search", "database"], fixtures.basic); + expect(code).toBe(0); + expect(stdout).toContain(dp(fixtures.basic, "backend/database.md")); + expect(stdout).not.toContain(dp(fixtures.basic, "code-style.md")); + expect(stdout).not.toContain(dp(fixtures.basic, "backend/api-guide.md")); + }); + + it("requires every term to match (AND) and excludes non-matching files", () => { + const { code, stdout } = run(["--search", "guide api"], fixtures.basic); + expect(code).toBe(0); + expect(stdout).toContain(dp(fixtures.basic, "backend/api-guide.md")); + expect(stdout).not.toContain(dp(fixtures.basic, "backend/database.md")); + }); + + it("reports when nothing matches", () => { + const { code, stdout } = run(["--search", "zzznomatch"], fixtures.basic); + expect(code).toBe(0); + expect(stdout).toContain("No documents match: zzznomatch"); + }); +}); + describe("CHANGELOG file exclusion", () => { it("does not list CHANGELOG.md in default listing", () => { const { stdout } = run([], fixtures.basic); diff --git a/packages/docmap/test/fixtures/large/bulk/doc-01.md b/packages/docmap/test/fixtures/large/bulk/doc-01.md new file mode 100644 index 0000000..60d32ce --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-01.md @@ -0,0 +1,8 @@ +--- +title: Doc 01 +summary: Root document number 01. +--- + +# Doc 01 + +Body. diff --git a/packages/docmap/test/fixtures/large/bulk/doc-02.md b/packages/docmap/test/fixtures/large/bulk/doc-02.md new file mode 100644 index 0000000..af16867 --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-02.md @@ -0,0 +1,8 @@ +--- +title: Doc 02 +summary: Root document number 02. +--- + +# Doc 02 + +Body. diff --git a/packages/docmap/test/fixtures/large/bulk/doc-03.md b/packages/docmap/test/fixtures/large/bulk/doc-03.md new file mode 100644 index 0000000..e2b7f37 --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-03.md @@ -0,0 +1,8 @@ +--- +title: Doc 03 +summary: Root document number 03. +--- + +# Doc 03 + +Body. diff --git a/packages/docmap/test/fixtures/large/bulk/doc-04.md b/packages/docmap/test/fixtures/large/bulk/doc-04.md new file mode 100644 index 0000000..47e04f6 --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-04.md @@ -0,0 +1,8 @@ +--- +title: Doc 04 +summary: Root document number 04. +--- + +# Doc 04 + +Body. diff --git a/packages/docmap/test/fixtures/large/bulk/doc-05.md b/packages/docmap/test/fixtures/large/bulk/doc-05.md new file mode 100644 index 0000000..2de0dc3 --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-05.md @@ -0,0 +1,8 @@ +--- +title: Doc 05 +summary: Root document number 05. +--- + +# Doc 05 + +Body. diff --git a/packages/docmap/test/fixtures/large/bulk/doc-06.md b/packages/docmap/test/fixtures/large/bulk/doc-06.md new file mode 100644 index 0000000..c1161f4 --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-06.md @@ -0,0 +1,8 @@ +--- +title: Doc 06 +summary: Root document number 06. +--- + +# Doc 06 + +Body. diff --git a/packages/docmap/test/fixtures/large/bulk/doc-07.md b/packages/docmap/test/fixtures/large/bulk/doc-07.md new file mode 100644 index 0000000..3c4cf33 --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-07.md @@ -0,0 +1,8 @@ +--- +title: Doc 07 +summary: Root document number 07. +--- + +# Doc 07 + +Body. diff --git a/packages/docmap/test/fixtures/large/bulk/doc-08.md b/packages/docmap/test/fixtures/large/bulk/doc-08.md new file mode 100644 index 0000000..0ba078d --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-08.md @@ -0,0 +1,8 @@ +--- +title: Doc 08 +summary: Root document number 08. +--- + +# Doc 08 + +Body. diff --git a/packages/docmap/test/fixtures/large/bulk/doc-09.md b/packages/docmap/test/fixtures/large/bulk/doc-09.md new file mode 100644 index 0000000..c188580 --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-09.md @@ -0,0 +1,8 @@ +--- +title: Doc 09 +summary: Root document number 09. +--- + +# Doc 09 + +Body. diff --git a/packages/docmap/test/fixtures/large/bulk/doc-10.md b/packages/docmap/test/fixtures/large/bulk/doc-10.md new file mode 100644 index 0000000..7af38e4 --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-10.md @@ -0,0 +1,8 @@ +--- +title: Doc 10 +summary: Root document number 10. +--- + +# Doc 10 + +Body. diff --git a/packages/docmap/test/fixtures/large/bulk/doc-11.md b/packages/docmap/test/fixtures/large/bulk/doc-11.md new file mode 100644 index 0000000..c27aba1 --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-11.md @@ -0,0 +1,8 @@ +--- +title: Doc 11 +summary: Root document number 11. +--- + +# Doc 11 + +Body. diff --git a/packages/docmap/test/fixtures/large/bulk/doc-12.md b/packages/docmap/test/fixtures/large/bulk/doc-12.md new file mode 100644 index 0000000..3b7e0af --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-12.md @@ -0,0 +1,8 @@ +--- +title: Doc 12 +summary: Root document number 12. +--- + +# Doc 12 + +Body. diff --git a/packages/docmap/test/fixtures/large/bulk/doc-13.md b/packages/docmap/test/fixtures/large/bulk/doc-13.md new file mode 100644 index 0000000..52f55b8 --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-13.md @@ -0,0 +1,8 @@ +--- +title: Doc 13 +summary: Root document number 13. +--- + +# Doc 13 + +Body. diff --git a/packages/docmap/test/fixtures/large/bulk/doc-14.md b/packages/docmap/test/fixtures/large/bulk/doc-14.md new file mode 100644 index 0000000..8a475e6 --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-14.md @@ -0,0 +1,8 @@ +--- +title: Doc 14 +summary: Root document number 14. +--- + +# Doc 14 + +Body. diff --git a/packages/docmap/test/fixtures/large/bulk/doc-15.md b/packages/docmap/test/fixtures/large/bulk/doc-15.md new file mode 100644 index 0000000..c54c4e1 --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-15.md @@ -0,0 +1,8 @@ +--- +title: Doc 15 +summary: Root document number 15. +--- + +# Doc 15 + +Body. diff --git a/packages/docmap/test/fixtures/large/bulk/doc-16.md b/packages/docmap/test/fixtures/large/bulk/doc-16.md new file mode 100644 index 0000000..3788738 --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-16.md @@ -0,0 +1,8 @@ +--- +title: Doc 16 +summary: Root document number 16. +--- + +# Doc 16 + +Body. diff --git a/packages/docmap/test/fixtures/large/bulk/doc-17.md b/packages/docmap/test/fixtures/large/bulk/doc-17.md new file mode 100644 index 0000000..2e9a73b --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-17.md @@ -0,0 +1,8 @@ +--- +title: Doc 17 +summary: Root document number 17. +--- + +# Doc 17 + +Body. diff --git a/packages/docmap/test/fixtures/large/bulk/doc-18.md b/packages/docmap/test/fixtures/large/bulk/doc-18.md new file mode 100644 index 0000000..6f9a16b --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-18.md @@ -0,0 +1,8 @@ +--- +title: Doc 18 +summary: Root document number 18. +--- + +# Doc 18 + +Body. diff --git a/packages/docmap/test/fixtures/large/bulk/doc-19.md b/packages/docmap/test/fixtures/large/bulk/doc-19.md new file mode 100644 index 0000000..a0d06f8 --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-19.md @@ -0,0 +1,8 @@ +--- +title: Doc 19 +summary: Root document number 19. +--- + +# Doc 19 + +Body. diff --git a/packages/docmap/test/fixtures/large/bulk/doc-20.md b/packages/docmap/test/fixtures/large/bulk/doc-20.md new file mode 100644 index 0000000..c0bc5de --- /dev/null +++ b/packages/docmap/test/fixtures/large/bulk/doc-20.md @@ -0,0 +1,8 @@ +--- +title: Doc 20 +summary: Root document number 20. +--- + +# Doc 20 + +Body. diff --git a/packages/docmap/test/fixtures/large/nested-a/inner.md b/packages/docmap/test/fixtures/large/nested-a/inner.md new file mode 100644 index 0000000..ccb5bc4 --- /dev/null +++ b/packages/docmap/test/fixtures/large/nested-a/inner.md @@ -0,0 +1,8 @@ +--- +title: Inner A +summary: Nested document under nested-a. +--- + +# Inner A + +Body. diff --git a/packages/docmap/test/fixtures/large/only-subs/deep/leaf.md b/packages/docmap/test/fixtures/large/only-subs/deep/leaf.md new file mode 100644 index 0000000..b32d795 --- /dev/null +++ b/packages/docmap/test/fixtures/large/only-subs/deep/leaf.md @@ -0,0 +1,8 @@ +--- +title: Leaf +summary: Deeply nested leaf document. +--- + +# Leaf + +Body. diff --git a/packages/workspace/README.md b/packages/workspace/README.md index 7dbb82b..51ef36e 100644 --- a/packages/workspace/README.md +++ b/packages/workspace/README.md @@ -9,16 +9,16 @@ Each project writes two custom scripts on top, using these entry points: ## Setup -The `workspace-guide` skill is a setup-time companion. Install the skill (globally or locally): +The `alignfirst-setup-guide` skill is a setup-time companion. Install the skill (globally or locally): ```bash -npx skills add https://github.com/paleo/alignfirst --skill workspace-guide +npx skills add https://github.com/paleo/alignfirst --skill alignfirst-setup-guide ``` Then, in your project, ask your agent: ```text -Use your workspace-guide skill. Set up worktree-based local environments in this project. +Use your alignfirst-setup-guide skill. Set up worktree-based local environments in this project. ``` The agent reads the skill, adapts the reference scripts to your stack, installs `@paleo/workspace` as a dev dependency, and wires the npm scripts. After that, you can uninstall the skill, it won't be used by your project anymore. diff --git a/skills/alignfirst-setup-guide/SKILL.md b/skills/alignfirst-setup-guide/SKILL.md new file mode 100644 index 0000000..7fb9423 --- /dev/null +++ b/skills/alignfirst-setup-guide/SKILL.md @@ -0,0 +1,56 @@ +--- +name: alignfirst-setup-guide +description: >- + Setup-time companion to install or upgrade any subset of {docmap, workspace, alignfirst skills} in a consumer repo. Use when asked to install, set up, or upgrade docmap, the workspace worktree system, or the AlignFirst skills. +compatibility: Requires git and a Node.js package manager (npm, pnpm, yarn, or bun). +license: CC0 1.0 +metadata: + author: Paleo + version: "0.14.0" + repository: https://github.com/paleo/alignfirst +--- + +# AlignFirst Setup Guide + +A one-time companion for setting up a consumer repository. It installs any subset of three independent tools: + +- **docmap** — agent-discoverable documentation under `docs/`. +- **workspace** — worktree-based concurrent local dev environments. +- **alignfirst skills** — collaborative spec/plan/AAD/review protocols. + +Pick any combination. Each has its own reference; this skill investigates the repo, agrees on scope, then follows the matching reference(s). + +Follow these four steps in order. Do not skip ahead: complete each before starting the next. + +## Step 1 — Investigate + +Detect the stack and **package manager**: check the `packageManager` field in `package.json`, else the root lockfile — `package-lock.json` → npm, `pnpm-lock.yaml` → pnpm, `yarn.lock` → yarn, `bun.lockb`/`bun.lock` → bun — falling back to npm. Record the result — every reference in Step 3 reuses this one detection rather than repeating it. Then detect each tool's existing footprint: + +- **docmap**: a `docmap` script, a `@paleo/docmap` dependency, or a `docs/` directory. +- **workspace**: a `workspace` script or a `@paleo/workspace` dependency. +- **alignfirst skills**: an installed alignfirst skill, a `.plans/` directory, or an `## AlignFirst` section in `AGENTS.md` / `CLAUDE.md`. + +## Step 2 — Discuss + +Present the findings and agree with the user on which tools to install or upgrade. Any combination is valid. + +## Step 3 — Set up + +Require a clean working tree first (`git status`). If it isn't clean, stop and ask the user to commit or stash. Then follow the matching reference(s): + +- [docmap-setup.md](references/docmap-setup.md) — install the docmap CLI, then optionally bootstrap or migrate docs. +- [workspace-setup.md](references/workspace-setup.md) — implement the worktree system (adapt the [asset scripts](assets/), install `@paleo/workspace`). +- [alignfirst-skills-setup.md](references/alignfirst-skills-setup.md) — install the AlignFirst skills and configure the project. + +**Upgrading from an older AlignFirst** (v1/v2): route through [alignfirst-upgrade.md](references/alignfirst-upgrade.md), which detects the version and follows [alignfirst-upgrade-from-v1.md](references/alignfirst-upgrade-from-v1.md) or [alignfirst-upgrade-from-v2.md](references/alignfirst-upgrade-from-v2.md). + +## Step 4 — After setup + +This skill is temporary. Once setup is done, the user can uninstall it. Provide them the command: + +```sh +npx skills remove alignfirst-setup-guide --yes + +# prune the entry in skills-lock.json +node --input-type=module -e 'import {readFileSync as r,writeFileSync as w} from "node:fs";const f="skills-lock.json",j=JSON.parse(r(f));delete j.skills["alignfirst-setup-guide"];w(f,JSON.stringify(j,null,2)+"\n")' +``` diff --git a/skills/workspace-guide/assets/dev-server.mjs b/skills/alignfirst-setup-guide/assets/dev-server.mjs similarity index 100% rename from skills/workspace-guide/assets/dev-server.mjs rename to skills/alignfirst-setup-guide/assets/dev-server.mjs diff --git a/skills/workspace-guide/assets/workspace.md b/skills/alignfirst-setup-guide/assets/workspace.md similarity index 100% rename from skills/workspace-guide/assets/workspace.md rename to skills/alignfirst-setup-guide/assets/workspace.md diff --git a/skills/workspace-guide/assets/workspace.mjs b/skills/alignfirst-setup-guide/assets/workspace.mjs similarity index 100% rename from skills/workspace-guide/assets/workspace.mjs rename to skills/alignfirst-setup-guide/assets/workspace.mjs diff --git a/skills/alignfirst-setup-guide/references/alignfirst-skills-setup.md b/skills/alignfirst-setup-guide/references/alignfirst-skills-setup.md new file mode 100644 index 0000000..17603d3 --- /dev/null +++ b/skills/alignfirst-setup-guide/references/alignfirst-skills-setup.md @@ -0,0 +1,41 @@ +# AlignFirst Skills Setup + +Configure a consumer repo for the AlignFirst skills: add `.plans` to `.gitignore` and an AlignFirst section to the instruction file. + +## Step 1 — Configure the project + +1. Create the `.plans/` directory if it doesn't exist, and add `.plans` to `.gitignore` if needed. +2. Check if `AGENTS.md` or `CLAUDE.md` exists. If one exists, use it. If neither exists, create `AGENTS.md`. This file is the INSTRUCTION_FILE. +3. Look at git branches (`git branch -a`) to detect the ticket ID format (e.g., `ABC-###`, `PROJ-###`, or numeric). + - If no pattern is found, ask the user: + + > "I couldn't detect a ticket ID format from the branch names. Please provide the ticket ID format (e.g., "numeric", `ABC-###`, etc.) or type 'skip' to omit." + +4. From recent commit messages (`git log --oneline -20`), deduce the commit message convention (e.g., `: [] description`, `(): description`, `[] description`, etc.). + - If no pattern is found, ask the user: + + > "I couldn't detect a commit message convention. Please describe it (e.g., `feat: [AB-123] short description`, `type(scope): description`, etc.) or type 'skip' to omit." + +5. Detect the default branch with `git remote show origin | grep "HEAD branch"` (e.g., `main`, `master`, `develop`). + +6. Insert the following into the INSTRUCTION_FILE (skip any part already present): + - Add this line where it feels appropriate: "Always ignore the `.plans` directory when searching the codebase." + - If a ticket ID format was found, add this section (include each convention line only if one was detected or provided): + + > ## AlignFirst - Ticket ID, Commit Message, Default Branch + > + > _Ticket ID:_ Format is `{DETECTED_FORMAT}`. Use the ticket ID if explicitly provided. Otherwise, deduce it from the current branch name (no confirmation needed). If the branch name is unavailable, get it via `git branch --show-current`. Only ask the user as a last resort. + > + > _Commit message convention:_ `{DETECTED_CONVENTION}` + > + > _Default branch:_ `{DETECTED_DEFAULT_BRANCH}` + +## Step 2 — Install the skills + +Can you see the `alignfirst` skill? If the skills are not already installed, then ask the user to install them. **Print this command without executing it** so the user can run it themselves: + +```text +npx skills add https://github.com/paleo/alignfirst --global --skill alignfirst --skill al --skill alplan --skill alspec --skill aldescription --skill alreview --skill alread --skill almerge +``` + +We recommend installing these skills globally. After installation, the user must restart their agent (new session) for the skills to load. diff --git a/migrations/upgrade-from-v1.md b/skills/alignfirst-setup-guide/references/alignfirst-upgrade-from-v1.md similarity index 89% rename from migrations/upgrade-from-v1.md rename to skills/alignfirst-setup-guide/references/alignfirst-upgrade-from-v1.md index ffc4dfb..5540994 100644 --- a/migrations/upgrade-from-v1.md +++ b/skills/alignfirst-setup-guide/references/alignfirst-upgrade-from-v1.md @@ -80,7 +80,7 @@ If `AGENTS.md` (or `CLAUDE.md`) exists, edit it: Check if any `.md` files remain in `_docs/`. -- **If files remain**: Use the **docfront** skill to migrate them. Specifically, use its "Migrate Existing Documents" capability to bring the `_docs/` contents into a new `docs/` directory (do not modify in-place). After a successful migration, delete the old directory: +- **If files remain**: Migrate them by following [docmap-migrate-existing-docs.md](docmap-migrate-existing-docs.md) to bring the `_docs/` contents into a new `docs/` directory (do not modify in-place). After a successful migration, delete the old directory: ```bash rm -rf _docs @@ -101,4 +101,4 @@ Summarize what was done: - `_plans` renamed to `.plans` - `.gitignore` updated - `AGENTS.md` cleaned -- Whether documentation was migrated to `docs/` via docfront, or no remaining docs were found +- Whether documentation was migrated to `docs/` via docmap, or no remaining docs were found diff --git a/migrations/upgrade-from-v2.md b/skills/alignfirst-setup-guide/references/alignfirst-upgrade-from-v2.md similarity index 88% rename from migrations/upgrade-from-v2.md rename to skills/alignfirst-setup-guide/references/alignfirst-upgrade-from-v2.md index 5182553..4b7390c 100644 --- a/migrations/upgrade-from-v2.md +++ b/skills/alignfirst-setup-guide/references/alignfirst-upgrade-from-v2.md @@ -71,7 +71,7 @@ If `AGENTS.md` (or `CLAUDE.md`) exists, edit it: Check if any skill directories remain in `{SKILLS_DIR}/` (after deleting alignfirst and technical-documentation-authoring). -- **If skills remain**: Use the **docfront** skill to migrate them. Specifically, use its "Migrate Skills to Docfront Documentation" capability. This includes a discussion phase where the agent and user decide which skills to migrate and how to organize them. +- **If skills remain**: Migrate them by following [docmap-migrate-skills.md](docmap-migrate-skills.md). This includes a discussion phase where the agent and user decide which skills to migrate and how to organize them. - **If no skills remain**: Skip this step. @@ -84,4 +84,4 @@ Summarize what was done: - `_plans` renamed to `.plans` - `.gitignore` updated - `AGENTS.md` cleaned -- Whether custom skills were migrated to `docs/` via docfront, or no remaining skills were found +- Whether custom skills were migrated to `docs/` via docmap, or no remaining skills were found diff --git a/migrations/upgrade.md b/skills/alignfirst-setup-guide/references/alignfirst-upgrade.md similarity index 74% rename from migrations/upgrade.md rename to skills/alignfirst-setup-guide/references/alignfirst-upgrade.md index 3ec0e9e..6a0e957 100644 --- a/migrations/upgrade.md +++ b/skills/alignfirst-setup-guide/references/alignfirst-upgrade.md @@ -8,21 +8,7 @@ This prompt detects your current AlignFirst version (v1 or v2) and runs the appr 1. If this is a git repository, verify the working tree is clean. **Do not proceed with uncommitted changes.** -2. Verify you have access to the **docfront** skill. If you don't, stop and tell the user: - - > "I don't have access to the docfront skill. Please install it first: - > - > ```bash - > npx skills add paleo/docfront --skill docfront - > ```" - -3. Verify the **docfront CLI** is available (e.g., check if `package.json` has a `docfront` script, or try running `npx docfront --help`). If not available, stop and tell the user: - - > "The docfront CLI is not installed in this project. Please install it first — ask your agent: - > - > ```text - > Use your docfront skill. Install docfront CLI in this project. - > ```" +2. Verify the **docmap CLI** is available (e.g., check if `package.json` has a `docmap` script, or try running `npx docmap --help`). If not, install it first by following [docmap-setup.md](docmap-setup.md). ## Step 2 — Ensure Conventions Section @@ -69,8 +55,8 @@ Check if the INSTRUCTION_FILE already contains an `## AlignFirst` section with t ## Step 4 — Route -- **If v1 detected**: Fetch and follow **[upgrade-from-v1.md](https://raw.githubusercontent.com/paleo/alignfirst/refs/heads/main/migrations/upgrade-from-v1.md)**. -- **If v2 detected**: Fetch and follow **[upgrade-from-v2.md](https://raw.githubusercontent.com/paleo/alignfirst/refs/heads/main/migrations/upgrade-from-v2.md)**. +- **If v1 detected**: Follow [alignfirst-upgrade-from-v1.md](alignfirst-upgrade-from-v1.md). +- **If v2 detected**: Follow [alignfirst-upgrade-from-v2.md](alignfirst-upgrade-from-v2.md). - **If neither**: Stop and tell the user: > "This project doesn't appear to have AlignFirst v1 or v2 installed. Use the installation instructions instead." diff --git a/skills/docmap/references/bootstrapping-documentation.md b/skills/alignfirst-setup-guide/references/docmap-bootstrapping.md similarity index 100% rename from skills/docmap/references/bootstrapping-documentation.md rename to skills/alignfirst-setup-guide/references/docmap-bootstrapping.md diff --git a/skills/docmap/references/migrate-existing-docs.md b/skills/alignfirst-setup-guide/references/docmap-migrate-existing-docs.md similarity index 100% rename from skills/docmap/references/migrate-existing-docs.md rename to skills/alignfirst-setup-guide/references/docmap-migrate-existing-docs.md diff --git a/skills/docmap/references/migrate-skills-to-docmap.md b/skills/alignfirst-setup-guide/references/docmap-migrate-skills.md similarity index 92% rename from skills/docmap/references/migrate-skills-to-docmap.md rename to skills/alignfirst-setup-guide/references/docmap-migrate-skills.md index f028b41..f0b554d 100644 --- a/skills/docmap/references/migrate-skills-to-docmap.md +++ b/skills/alignfirst-setup-guide/references/docmap-migrate-skills.md @@ -88,11 +88,11 @@ You must provide each subagent with: If `AGENTS.md` (or an equivalent top-level agent instructions file) exists, add or replace the documentation discovery section with the following. -_**Note**: Adapt the commands to the project's package manager (see the [installation reference](installation.md) for the full command table)._ +_**Note**: Adapt the `npm run docmap` command to the project's package manager (`pnpm docmap`, `yarn docmap`, `bun run docmap`)._ > ## Docmap - Seek Documentation > -> **Before any investigation or code exploration**, run `npm run docmap` to list the documentation index. This is mandatory for every task — do not skip it. Browse subdirectories or read files by passing them as arguments (`npm run docmap -- topic-a docs/topic-b/doc.md`), or list everything (`npm run docmap -- --recursive`). +> **Before any investigation or code exploration**, run `npm run docmap`, then read the relevant documentation. Mandatory for every task. Remove any references to the deleted skill directories. diff --git a/skills/alignfirst-setup-guide/references/docmap-setup.md b/skills/alignfirst-setup-guide/references/docmap-setup.md new file mode 100644 index 0000000..3e17f9b --- /dev/null +++ b/skills/alignfirst-setup-guide/references/docmap-setup.md @@ -0,0 +1,30 @@ +# Docmap Setup + +Install the docmap CLI in a consumer repo so humans and agents share one set of docs under `docs/`. + +## Install the CLI + +1. **Use the package manager** identified in the skill's Investigate step (fall back to npm). +2. **Add a `docmap` script** to the root `package.json`: + + ```json + "docmap": "docmap" + ``` + +3. **Install `@paleo/docmap`** as a dev dependency with the detected package manager: `npm install -D @paleo/docmap` (`pnpm add -D` / `yarn add -D` / `bun add -D`). +4. **Ensure a `docs/` directory** exists (`mkdir docs` if missing). +5. **Add the docmap section** to `AGENTS.md` (or `CLAUDE.md`), adapting the run command to the package manager (`pnpm docmap`, `yarn docmap`, `bun run docmap`): + + ```markdown + ## Docmap - Seek Documentation + + **Before any investigation or code exploration**, run `npm run docmap`, then read the relevant documentation. Mandatory for every task. + ``` + +## Then + +1. Read authoring and browsing conventions by running `npm run docmap -- --guide`. +2. Ask the user what to do next: + - [docmap-bootstrapping.md](docmap-bootstrapping.md) — create or extend documentation by exploring the codebase. + - [docmap-migrate-existing-docs.md](docmap-migrate-existing-docs.md) — bring an existing docs folder into docmap conventions. + - [docmap-migrate-skills.md](docmap-migrate-skills.md) — move internal knowledge from agent skills into `docs/`. diff --git a/skills/workspace-guide/SKILL.md b/skills/alignfirst-setup-guide/references/workspace-setup.md similarity index 94% rename from skills/workspace-guide/SKILL.md rename to skills/alignfirst-setup-guide/references/workspace-setup.md index 3eb9270..aad0e6d 100644 --- a/skills/workspace-guide/SKILL.md +++ b/skills/alignfirst-setup-guide/references/workspace-setup.md @@ -1,24 +1,14 @@ ---- -name: workspace-guide -description: >- - Blueprint for implementing a workspace system — multiple git-worktree dev environments side by side — in a repository. -compatibility: Requires git. Template scripts are in Node.js but the approach works with any runtime. -license: CC0 1.0 -metadata: - author: Paleo - version: "0.12.1" - repository: https://github.com/paleo/skills ---- - # Implementing Worktree-Based Concurrent Local Environments +Blueprint for implementing a workspace system — multiple git-worktree dev environments side by side — in a repository. Requires git. Template scripts are in Node.js but the approach works with any runtime. + Implement a system for running multiple local dev environments side by side via git worktrees. Adapt it to any repository, regardless of tech stack or database engine. **Node consumers** install the `@paleo/workspace` package and write two custom scripts that build a config object and call `runWorkspace(config)` / `runDevServer(config)`. The package owns the kernel — slot/dev-server registries, port math, branch lifecycle, process-group control, log polling, CLI parsing. Consumers supply project-specific callbacks (`finalizeWorktree`, `printSummary`, optional `purgeInfrastructure`, optional `devServerScript`) plus a `configFiles` list with patch functions, and resolve their own dev-limit ladder. **Non-Node consumers** reimplement the system from this design doc; the rationale sections below are self-contained. -The `assets/` directory contains reference scripts ([workspace.mjs](assets/workspace.mjs), [dev-server.mjs](assets/dev-server.mjs)) — thin wrappers around the package — plus a template for [agent documentation](assets/workspace.md). The scripts carry `ADAPT` comments and long explanatory blocks — scaffolding to guide _you_, not part of the deliverable. Strip them from the scripts you generate; keep only the rare comment explaining a non-obvious, project-specific choice (e.g. why a file is copied). Aim for lean wrappers. +The `assets/` directory contains reference scripts ([workspace.mjs](../assets/workspace.mjs), [dev-server.mjs](../assets/dev-server.mjs)) — thin wrappers around the package — plus a template for [agent documentation](../assets/workspace.md). The scripts carry `ADAPT` comments and long explanatory blocks — scaffolding to guide _you_, not part of the deliverable. Strip them from the scripts you generate; keep only the rare comment explaining a non-obvious, project-specific choice (e.g. why a file is copied). Aim for lean wrappers. ## Implementation Process @@ -126,7 +116,7 @@ Trade-off: mistakes in the main worktree's config also propagate. Keep it clean. This is the central piece. It handles the full worktree lifecycle: creation, setup, and removal. It can create a worktree for an existing branch, create a new branch with automatic deduplication, set up the local environment, and tear everything down. -The package's `runWorkspace(config: WorkspaceConfig)` performs the lifecycle below. See [assets/workspace.mjs](assets/workspace.mjs) for a populated reference config. +The package's `runWorkspace(config: WorkspaceConfig)` performs the lifecycle below. See [assets/workspace.mjs](../assets/workspace.mjs) for a populated reference config. **Lifecycle for setup (with `workspace setup `):** @@ -220,7 +210,7 @@ The rules below are not enforceable by the type system. Read them carefully: - Each worktree gets its own Docker stack on slot-scoped ports (host port remap; container port unchanged). `stop()` is local — no reference counting, no shared infra. - Registry liveness pruning is PID-based on spawn servers. If a user kills the spawn processes manually instead of running `dev down`, the entry is pruned and callback `stop()` never fires (e.g. Docker is orphaned). Always use `dev down`. -See [assets/dev-server.mjs](assets/dev-server.mjs) for a populated reference config. +See [assets/dev-server.mjs](../assets/dev-server.mjs) for a populated reference config. **Lifecycle:** @@ -374,7 +364,7 @@ Without the conventions, the agent creates branches and commits with inconsisten ### 2. Detailed workspace documentation (`docs/workspace.md`) -This is the file referenced above. It contains the step-by-step procedures: how to create a workspace, how to start the dev server, how to tear things down. See [assets/workspace.md](assets/workspace.md) for a starting point. +This is the file referenced above. It contains the step-by-step procedures: how to create a workspace, how to start the dev server, how to tear things down. See [assets/workspace.md](../assets/workspace.md) for a starting point. The agents need to know: @@ -393,21 +383,10 @@ The agents need to know: - [ ] **Decide on fatal log markers for `dev-server`** (or leave the array empty). Substrings that mean "unrecoverable startup failure" let the script fail fast instead of waiting for the timeout. - [ ] **Bootstrap the main worktree's config files manually once** (from `.example` files), since sibling worktrees inherit from the main worktree. - [ ] **Install `@paleo/workspace`** as a dev-dependency (Node consumers). -- [ ] **Write `workspace`** using [assets/workspace.mjs](assets/workspace.mjs) as a starting point. Search for `ADAPT` comments — then strip them (and the other scaffolding comments) from your final script. -- [ ] **Write `dev-server`** using [assets/dev-server.mjs](assets/dev-server.mjs) as a starting point. Same approach: adapt, then leave a lean script. +- [ ] **Write `workspace`** using [assets/workspace.mjs](../assets/workspace.mjs) as a starting point. Search for `ADAPT` comments — then strip them (and the other scaffolding comments) from your final script. +- [ ] **Write `dev-server`** using [assets/dev-server.mjs](../assets/dev-server.mjs) as a starting point. Same approach: adapt, then leave a lean script. - [ ] **Add npm scripts** (or Makefile targets, etc.): `workspace` and a single `dev` (don't reuse the app's own dev script name). - [ ] **Set the dev-server cap** by passing `devLimit` to `runDevServer` (default `5`). - [ ] **Update `.gitignore`** to ignore your shared and per-worktree directory (e.g. `.local-wt/`). -- [ ] **Write agent documentation** if applicable (see [assets/workspace.md](assets/workspace.md)). +- [ ] **Write agent documentation** if applicable (see [assets/workspace.md](../assets/workspace.md)). - [ ] **Update your main instruction file** (`AGENTS.md` / `CLAUDE.md`) with a pointer to the agent documentation and any conventions (branch naming, commit messages) the agent needs to follow. - -## Removing This Guide Skill - -Once the workspace system is implemented, committed, and tested, this guide has done its job — it is not needed for day-to-day work. As the very last step, give the developer the command to uninstall it (assuming it was installed with the [`skills` CLI](https://github.com/vercel-labs/skills)): - -```sh -npx skills remove workspace-guide --yes - -# prune the entry in skills-lock.json -node --input-type=module -e 'import {readFileSync as r,writeFileSync as w} from "node:fs";const f="skills-lock.json",j=JSON.parse(r(f));delete j.skills["workspace-guide"];w(f,JSON.stringify(j,null,2)+"\n")' -``` diff --git a/skills/docmap/SKILL.md b/skills/docmap/SKILL.md deleted file mode 100644 index 3dbf507..0000000 --- a/skills/docmap/SKILL.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -name: docmap -description: "Conventions for writing, organizing, and browsing documentation in a docs/ directory using docmap. Use when creating documents, restructuring documentation, or unsure about frontmatter format and file naming conventions." -license: CC0 1.0 -metadata: - author: Paleo - version: "0.6.0" - repository: https://github.com/paleo/alignfirst ---- - -# Authoring Documentation - -All project documentation lives in the `docs/` directory. The `docmap` CLI lets both humans and AI agents discover and read documents without leaving the terminal. - -## Browsing with CLI - -> Run commands via the project's package manager (e.g. `npm run docmap --`, `pnpm docmap`, `yarn docmap`). - -Targets are positional paths; the CLI classifies each by inspecting the filesystem (directory → list, file → read). Pass several at once. The root prefix shown in listings (`docs/` by default) is optional on input. - -```bash -docmap # list root docs -docmap topic-a topic-b/sub-topic-c # list subdirectories -docmap --recursive # list everything -docmap doc-1.md docs/topic-a/doc-2.md # read documents (frontmatter stripped) -docmap topic-a docs/topic-a/doc-1.md # mix listing and reading -docmap --check # validate all files -``` - -## Workflow - -1. **Understand the Subject** — clarify what needs to be documented. Ask the user if unclear. -2. **Determine Placement** — scan existing docs (`docmap --recursive`). Decide: new file, existing file, which subdirectory. Discuss with the user if unclear. -3. **Write** — follow the conventions below. - -## Writing Guidelines - -- **Target audience:** an experienced newcomer — someone technically capable but unfamiliar with this specific project. -- Be brief and specific — no obvious information, no generic best practices. -- Typical document: 40–80 lines. -- Prefer referencing source files over large code blocks. -- If the title makes the purpose obvious, omit the `summary`. - -## File and Directory Naming - -- Use **lowercase-with-dashes** (kebab-case) for new files and directories. -- Uppercase is allowed by the CLI (e.g. `RELEASING.md`). -- Names must be **shell-safe**: no spaces, no quotes, no special characters. The CLI validates this for both files and directories. Use `docmap --check` to verify. -- Use `.md` (Markdown) for all documents. -- Use short, descriptive names. -- Group related documents into subdirectories. Subdirectories can be nested. - -## YAML Frontmatter - -`.md` files can start with a YAML frontmatter block. Add it when it adds value — especially when the filename or heading alone is not explicit enough. It is not required; when frontmatter is absent, the CLI falls back to the first `# heading` in the document body for the title. Fields: - -| Field | Required | Description | -| --- | --- | --- | -| `title` | No | A human-readable display name shown in listings. Falls back to the first `# heading` when absent. | -| `summary` | No | One concise sentence. If the title already makes the purpose obvious, omit the summary to avoid redundancy. | -| `read_when` | No | A YAML list of short, action-oriented hints. Each hint completes: *"Read this document when you are…"* | - -## Document Body - -After the closing `---` of the frontmatter, write standard Markdown. There are no constraints on the body format — use headings, code blocks, tables, and lists as needed. - -```markdown ---- -title: Your Title Here -summary: A one-sentence description of what this document covers. -read_when: - - first situation when this document is useful - - second situation ---- - -# Your Title Here - -Start your content here… -``` - -## References - -- [Installing Docmap CLI](references/installation.md) — how to add docmap CLI to a project. -- [Bootstrapping Documentation](references/bootstrapping-documentation.md) — instructions for creating or extending project documentation by exploring the codebase. -- [Migrate Existing Documents](references/migrate-existing-docs.md) — guide for bringing an existing folder of Markdown documents into docmap conventions (naming, frontmatter). -- [Migrate Skills to Docmap Documentation](references/migrate-skills-to-docmap.md) — guide for moving internal project documentation from agent skills into `docs/`. diff --git a/skills/docmap/references/installation.md b/skills/docmap/references/installation.md deleted file mode 100644 index 92ae3f5..0000000 --- a/skills/docmap/references/installation.md +++ /dev/null @@ -1,79 +0,0 @@ -# Installing Docmap CLI - -## 1. Detect the Package Manager - -If you already know the package manager, skip this step. - -Otherwise, check in order: - -1. **`packageManager` field in `package.json`**. -2. **Lockfile in the repo root:** - -| Lockfile | Package Manager | -| --- | --- | -| `package-lock.json` | npm | -| `pnpm-lock.yaml` | pnpm | -| `yarn.lock` | yarn | -| `bun.lockb` or `bun.lock` | bun | - -If neither is found, fall back to **npm**. - -## 2. Add a Script - -In the root `package.json`, add a `docmap` script (all package managers support this): - -```json -{ - "scripts": { - "docmap": "docmap" - } -} -``` - -## 3. Install - -Install docmap as a dev dependency using the detected package manager: - -| Package Manager | Command | -| --- | --- | -| npm | `npm install -D @paleo/docmap` | -| pnpm | `pnpm add -D @paleo/docmap` | -| yarn | `yarn add -D @paleo/docmap` | -| bun | `bun add -D @paleo/docmap` | - -## 4. Ensure a `docs/` Directory Exists - -If the project does not already have a `docs/` directory, create one: - -```bash -mkdir docs -``` - -## 5. Add to `AGENTS.md` - -If the project has an `AGENTS.md` (or equivalent top-level agent instructions file like `CLAUDE.md`), we want to add a section. Replace the `npm run` commands with the correct form for the project's package manager: - -| Package Manager | Run script | Run with args | -| --- | --- | --- | -| npm | `npm run docmap` | `npm run docmap -- --recursive` | -| pnpm | `pnpm docmap` | `pnpm docmap --recursive` | -| yarn | `yarn docmap` | `yarn docmap --recursive` | -| bun | `bun run docmap` | `bun run docmap --recursive` | - -Section to add: - -```markdown -## Docmap - Seek Documentation - -**Before any investigation or code exploration**, run `npm run docmap` to list the documentation index. This is mandatory for every task — do not skip it. Browse subdirectories or read files by passing them as arguments (`npm run docmap -- topic-a docs/topic-b/doc.md`), or list everything (`npm run docmap -- --recursive`). -``` - -When done, output the following block **verbatim** as your final message to the user — do not paraphrase or omit it: - -> **Instructions available:** -> -> - **Bootstrap the documentation** — the agent will analyse the codebase, propose a document layout, and write the files. -> - **Migrate existing docs** — if the project already has documentation, the agent will bring it in line with docmap conventions (kebab-case filenames, frontmatter fields, etc.). -> - **Migrate existing skills to `docs/`** — if the project stores internal knowledge as agent skills, ask the agent to move that content into `docs/`. -> -> Just ask your agent and it will be done. From d2c4ea843cbb0966f390dc8cdb823eaf136fbc97 Mon Sep 17 00:00:00 2001 From: Paleo Date: Sun, 21 Jun 2026 13:10:49 +0200 Subject: [PATCH 2/7] feat: use package-runner fallback for docmap PM detection --- README.md | 2 +- packages/docmap/src/cli.ts | 18 +++++- packages/docmap/src/formatter.ts | 56 ++++++++----------- skills/alignfirst/references/aad-protocol.md | 2 + skills/alignfirst/references/spec-protocol.md | 2 + 5 files changed, 44 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 41517a9..52b7123 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Collaborative spec/plan/AAD/review protocols. See [alignfirst-skills.md](alignfi ## Docmap - Agent-discoverable documentation -`@paleo/docmap` — a lightweight table of contents that lets agents navigate documentation files. This way we have one set of docs, shared by humans and AI agents. Run `docmap --guide` for authoring conventions; the `alignfirst-setup-guide` skill installs it in a new repo. See [packages/docmap/README.md](packages/docmap/README.md). +`@paleo/docmap` — a lightweight table of contents that lets agents navigate documentation files. This way we have one set of docs, shared by humans and AI agents. See [packages/docmap/README.md](packages/docmap/README.md). ## Workspaces - Local environments with worktrees diff --git a/packages/docmap/src/cli.ts b/packages/docmap/src/cli.ts index 4db6b4b..04284e0 100644 --- a/packages/docmap/src/cli.ts +++ b/packages/docmap/src/cli.ts @@ -64,7 +64,9 @@ export function main(options?: MainOptions): number { } // A bare invocation over a small doc set lists recursively and is the only case that - // prefixes the listing with short help. + // prefixes the listing with short help. The threshold check walks the tree (capped at + // SMALL_SET_THRESHOLD), and renderListing below walks it again to format — an intentional + // double traversal, negligible at this threshold and not worth threading a shared pass through. const bare = paths.length === 0 && !recursive; const smallSet = bare && countFilesUpTo(baseDir, SMALL_SET_THRESHOLD) < SMALL_SET_THRESHOLD; if (smallSet) stdout.write(`${renderHelp(pm, { full: false })}\n`); @@ -312,11 +314,23 @@ function detectPackageManager(cwd: string): PackageManagerCommands { if (existsSync(join(dir, "bun.lockb")) || existsSync(join(dir, "bun.lock"))) return sameCommand("bun run docmap"); const parent = dirname(dir); - if (parent === dir) return sameCommand("docmap"); + if (parent === dir) return fallbackCommand(); dir = parent; } } +// No lockfile found: docmap is likely not wired as a project script, so a bare `docmap` would +// assume a global install. Suggest the package-runner form instead, picking the runner from the +// manager that launched this process (npm_config_user_agent) and defaulting to npx, which ships +// with every Node install. +function fallbackCommand(): PackageManagerCommands { + const agent = process.env.npm_config_user_agent ?? ""; + if (agent.startsWith("pnpm")) return sameCommand("pnpm dlx @paleo/docmap"); + if (agent.startsWith("yarn")) return sameCommand("yarn dlx @paleo/docmap"); + if (agent.startsWith("bun")) return sameCommand("bunx @paleo/docmap"); + return sameCommand("npx @paleo/docmap"); +} + // Only npm needs a `--` separator before forwarded args; every other manager passes them verbatim. function sameCommand(command: string): PackageManagerCommands { return { base: command, withArgs: command }; diff --git a/packages/docmap/src/formatter.ts b/packages/docmap/src/formatter.ts index ec73b35..934ff33 100644 --- a/packages/docmap/src/formatter.ts +++ b/packages/docmap/src/formatter.ts @@ -30,6 +30,21 @@ export interface CheckIssue { message: string; } +// Reads a `.md` file and turns its frontmatter (plus name validation) into a FileEntry. The single +// source of the extractMetadata -> fallback-title -> validateName sequence used across the module. +function buildFileEntry(dirPath: string, name: string): FileEntry { + const content = readFileSync(join(dirPath, name), "utf-8"); + const meta = extractMetadata(content); + return { + name, + title: meta.title ?? extractFallbackTitle(content), + summary: meta.summary, + readWhen: meta.readWhen, + error: meta.error, + nameError: validateName(name), + }; +} + export function checkAll(dirPath: string, relDir: string, prefix: string): CheckIssue[] { const issues: CheckIssue[] = []; let entries: Dirent[]; @@ -41,19 +56,17 @@ export function checkAll(dirPath: string, relDir: string, prefix: string): Check for (const entry of entries) { const rel = displayPath(prefix, relDir, entry.name); - const nameWarning = validateName(entry.name); if (entry.isDirectory()) { + const nameWarning = validateName(entry.name); if (nameWarning) issues.push({ path: rel, message: nameWarning }); const subRel = relDir ? `${relDir}/${entry.name}` : entry.name; issues.push(...checkAll(join(dirPath, entry.name), subRel, prefix)); } else if (entry.name.endsWith(".md") && !shouldSkipFile(entry.name)) { - if (nameWarning) issues.push({ path: rel, message: nameWarning }); - const content = readFileSync(join(dirPath, entry.name), "utf-8"); - const meta = extractMetadata(content); - if (meta.error) issues.push({ path: rel, message: meta.error }); - const title = meta.title ?? extractFallbackTitle(content); - if (!title) issues.push({ path: rel, message: "Missing title" }); + const file = buildFileEntry(dirPath, entry.name); + if (file.nameError) issues.push({ path: rel, message: file.nameError }); + if (file.error) issues.push({ path: rel, message: file.error }); + if (!file.title) issues.push({ path: rel, message: "Missing title" }); } } @@ -88,20 +101,7 @@ export function listDirectory(dirPath: string): DirectoryListing { if (warning) subdirWarnings.set(sub, warning); } - const files: FileEntry[] = mdFiles.map((name) => { - const content = readFileSync(join(dirPath, name), "utf-8"); - const meta = extractMetadata(content); - const title = meta.title ?? extractFallbackTitle(content); - const nameError = validateName(name); - return { - name, - title, - summary: meta.summary, - readWhen: meta.readWhen, - error: meta.error, - nameError, - }; - }); + const files: FileEntry[] = mdFiles.map((name) => buildFileEntry(dirPath, name)); return { subdirs, files, subdirWarnings }; } @@ -246,22 +246,12 @@ export function searchDocs(baseDir: string, terms: string[], prefix: string): st const slash = rel.lastIndexOf("/"); const relDir = slash === -1 ? "" : rel.slice(0, slash); const name = slash === -1 ? rel : rel.slice(slash + 1); - const content = readFileSync(join(baseDir, rel), "utf-8"); - const meta = extractMetadata(content); - const title = meta.title ?? extractFallbackTitle(content); - const haystack = [title, meta.summary, ...meta.readWhen] + const entry = buildFileEntry(join(baseDir, relDir), name); + const haystack = [entry.title, entry.summary, ...entry.readWhen] .filter((part): part is string => part !== undefined) .join(" ") .toLowerCase(); if (!needles.every((needle) => haystack.includes(needle))) continue; - const entry: FileEntry = { - name, - title, - summary: meta.summary, - readWhen: meta.readWhen, - error: meta.error, - nameError: validateName(name), - }; lines.push(...formatFileBullets([entry], relDir, prefix)); } return lines; diff --git a/skills/alignfirst/references/aad-protocol.md b/skills/alignfirst/references/aad-protocol.md index 8700db5..5df0eb7 100644 --- a/skills/alignfirst/references/aad-protocol.md +++ b/skills/alignfirst/references/aad-protocol.md @@ -36,6 +36,8 @@ Engage in a thorough collaborative discussion covering: You're new to this project, the user can guide you. +Do not use your question tool. Always ask in plain text. Your questions must be the opportunity for a real discussion. + **This phase is mandatory.** If there is nothing to discuss, ask the user for an explicit validation. ## 3. Act diff --git a/skills/alignfirst/references/spec-protocol.md b/skills/alignfirst/references/spec-protocol.md index cfeb2b3..bdc7a50 100644 --- a/skills/alignfirst/references/spec-protocol.md +++ b/skills/alignfirst/references/spec-protocol.md @@ -45,6 +45,8 @@ You should ask questions freely to ensure you fully understand: - User preferences for implementation approaches - Any constraints or considerations you might have missed +Do not use your question tool. Always ask in plain text. Your questions must be the opportunity for a real discussion. + ## Phase 3. Specification Phase After the user approves your proposal, write the specification in a markdown file in TASK_DIR. Compose the filename with the current CYCLE_LETTER and the next FILE_NUMBER, e.g. `A1-spec.md`. Do not overwrite an existing file. From b18159a5d45dfcea2dad6342c6fcd9b506d7ebbb Mon Sep 17 00:00:00 2001 From: Paleo Date: Mon, 22 Jun 2026 07:48:36 +0200 Subject: [PATCH 3/7] fix: correct wording for clarity in discussion phase instructions --- skills/alignfirst/SKILL.md | 2 +- skills/alignfirst/references/aad-protocol.md | 2 +- skills/alignfirst/references/spec-protocol.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/skills/alignfirst/SKILL.md b/skills/alignfirst/SKILL.md index dfd16ea..dc6cde8 100644 --- a/skills/alignfirst/SKILL.md +++ b/skills/alignfirst/SKILL.md @@ -4,7 +4,7 @@ description: "Collaborative problem-solving protocols. Write technical specifica license: CC0 1.0 metadata: author: Paleo - version: "3.5.3" + version: "3.5.4" repository: https://github.com/paleo/alignfirst --- diff --git a/skills/alignfirst/references/aad-protocol.md b/skills/alignfirst/references/aad-protocol.md index 5df0eb7..1b6e9b1 100644 --- a/skills/alignfirst/references/aad-protocol.md +++ b/skills/alignfirst/references/aad-protocol.md @@ -36,7 +36,7 @@ Engage in a thorough collaborative discussion covering: You're new to this project, the user can guide you. -Do not use your question tool. Always ask in plain text. Your questions must be the opportunity for a real discussion. +Do not use your question tool. Always ask in plain text. Your questions will be the opportunity for a real discussion. **This phase is mandatory.** If there is nothing to discuss, ask the user for an explicit validation. diff --git a/skills/alignfirst/references/spec-protocol.md b/skills/alignfirst/references/spec-protocol.md index bdc7a50..e47184c 100644 --- a/skills/alignfirst/references/spec-protocol.md +++ b/skills/alignfirst/references/spec-protocol.md @@ -45,7 +45,7 @@ You should ask questions freely to ensure you fully understand: - User preferences for implementation approaches - Any constraints or considerations you might have missed -Do not use your question tool. Always ask in plain text. Your questions must be the opportunity for a real discussion. +Do not use your question tool. Always ask in plain text. Your questions will be the opportunity for a real discussion. ## Phase 3. Specification Phase From 4396c7829c20a2cce34d9b2434868e4f3da79d7f Mon Sep 17 00:00:00 2001 From: Paleo Date: Mon, 22 Jun 2026 08:21:02 +0200 Subject: [PATCH 4/7] feat: align docmap help/guide commands, share renderer --- .changeset/self-documenting-docmap-cli.md | 5 ++ .changeset/workspace-setup-guide-pointer.md | 5 ++ AGENTS.md | 2 +- docs/docmap-architecture.md | 4 +- docs/{ => openclaw-coder}/openclaw-coder.md | 0 .../openclaw-context-engineering.md | 0 .../openclaw-test-architecture.md | 0 .../writing-instructions-for-openclaw.md | 0 packages/docmap/src/cli.ts | 81 ++++++++++++++----- packages/docmap/templates/guide.md | 8 +- packages/docmap/test/docmap.test.ts | 19 +++++ 11 files changed, 96 insertions(+), 28 deletions(-) create mode 100644 .changeset/self-documenting-docmap-cli.md create mode 100644 .changeset/workspace-setup-guide-pointer.md rename docs/{ => openclaw-coder}/openclaw-coder.md (100%) rename docs/{ => openclaw-coder}/openclaw-context-engineering.md (100%) rename docs/{ => openclaw-coder}/openclaw-test-architecture.md (100%) rename docs/{ => openclaw-coder}/writing-instructions-for-openclaw.md (100%) diff --git a/.changeset/self-documenting-docmap-cli.md b/.changeset/self-documenting-docmap-cli.md new file mode 100644 index 0000000..b2ce759 --- /dev/null +++ b/.changeset/self-documenting-docmap-cli.md @@ -0,0 +1,5 @@ +--- +"@paleo/docmap": minor +--- + +Self-documenting CLI: add `--help`, `--guide`, and `--search`. Bare run lists recursively for small doc sets. No-lockfile fallback now suggests the package-runner form. diff --git a/.changeset/workspace-setup-guide-pointer.md b/.changeset/workspace-setup-guide-pointer.md new file mode 100644 index 0000000..e504042 --- /dev/null +++ b/.changeset/workspace-setup-guide-pointer.md @@ -0,0 +1,5 @@ +--- +"@paleo/workspace": patch +--- + +Point README Setup at the `alignfirst-setup-guide` skill. diff --git a/AGENTS.md b/AGENTS.md index 841c666..5f39ff1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,7 +35,7 @@ _Ticket ID_: Format is numeric. Use the ticket ID if explicitly provided. Otherw Commit message convention: we use conventional commit, e.g., `feat: [#123] add new feature`. Always prefix the ticket ID with a `#` sign. Do not add a "Co-Authored-By:" line. -Branch naming convention: `/` (with type from conventional commit, e.g., `feat/123`, `fix/123`, `refactor/123`, `chore/123`). +Branch naming convention: `/<1-3-words>`. Add `paleo-typescript-style` skill to every plan. diff --git a/docs/docmap-architecture.md b/docs/docmap-architecture.md index 4cd4388..e3fa621 100644 --- a/docs/docmap-architecture.md +++ b/docs/docmap-architecture.md @@ -24,7 +24,9 @@ Three TypeScript modules in `packages/docmap/src/`, compiled to `packages/docmap Entry point: `packages/docmap/bin/docmap.mjs` → imports `packages/docmap/dist/cli.js` → calls `main()` → returns an exit code. -The `--guide` text lives as Markdown in `packages/docmap/templates/guide.md` with two placeholders: `{{PM}}` for a bare invocation and `{{PM_ARGS}}` for one that forwards arguments. They differ only for npm, whose `run` script needs a `--` separator before args (`npm run docmap` vs `npm run docmap --`); every other manager forwards args verbatim, so both placeholders resolve to the same command. `renderGuide()` reads the file via `new URL("../templates/guide.md", import.meta.url)` — the path resolves from both `src/cli.ts` (dev/test) and `dist/cli.js` (published), each one level under the package root — and substitutes both tokens. `templates/` is listed in `package.json` `files` so it ships with the package. +The `--guide` text lives as Markdown in `packages/docmap/templates/guide.md` with three placeholders: `{{PM}}` for a bare invocation, `{{PM_ARGS}}` for one that forwards arguments, and `{{COMMANDS}}` for the rendered "Browsing with CLI" command block. `{{PM}}` and `{{PM_ARGS}}` differ only for npm, whose `run` script needs a `--` separator before args (`npm run docmap` vs `npm run docmap --`); every other manager forwards args verbatim, so both resolve to the same command. `renderGuide()` reads the file via `new URL("../templates/guide.md", import.meta.url)` — the path resolves from both `src/cli.ts` (dev/test) and `dist/cli.js` (published), each one level under the package root — and substitutes all three tokens. `templates/` is listed in `package.json` `files` so it ships with the package. + +Help and guide command lists are built from shared `CommandRow[]` builders (`browseCommands`, `moreCommands`, `guideCommands`) and rendered through one `renderCommands()` helper that pads each command to the group's longest, so the `#` comments stay vertically aligned for any package-manager prefix. Short help, full help (`Commands:` + `More:`), and the guide's `{{COMMANDS}}` block all flow through it; each group aligns independently. ## CLI Flow diff --git a/docs/openclaw-coder.md b/docs/openclaw-coder/openclaw-coder.md similarity index 100% rename from docs/openclaw-coder.md rename to docs/openclaw-coder/openclaw-coder.md diff --git a/docs/openclaw-context-engineering.md b/docs/openclaw-coder/openclaw-context-engineering.md similarity index 100% rename from docs/openclaw-context-engineering.md rename to docs/openclaw-coder/openclaw-context-engineering.md diff --git a/docs/openclaw-test-architecture.md b/docs/openclaw-coder/openclaw-test-architecture.md similarity index 100% rename from docs/openclaw-test-architecture.md rename to docs/openclaw-coder/openclaw-test-architecture.md diff --git a/docs/writing-instructions-for-openclaw.md b/docs/openclaw-coder/writing-instructions-for-openclaw.md similarity index 100% rename from docs/writing-instructions-for-openclaw.md rename to docs/openclaw-coder/writing-instructions-for-openclaw.md diff --git a/packages/docmap/src/cli.ts b/packages/docmap/src/cli.ts index 04284e0..fc18780 100644 --- a/packages/docmap/src/cli.ts +++ b/packages/docmap/src/cli.ts @@ -1,16 +1,16 @@ import { existsSync, readFileSync, statSync } from "node:fs"; import { dirname, join, relative, resolve } from "node:path"; import { - checkAll, - collectAllFiles, - countFilesUpTo, - formatDirectory, - formatRecursive, - isUnder, - listDirectory, - readDocFile, - searchDocs, - type FormatResult, + checkAll, + collectAllFiles, + countFilesUpTo, + formatDirectory, + formatRecursive, + isUnder, + listDirectory, + readDocFile, + searchDocs, + type FormatResult, } from "./formatter.js"; // A bare invocation over fewer .md files than this lists recursively by default. @@ -147,15 +147,57 @@ interface PackageManagerCommands { withArgs: string; } +interface CommandRow { + command: string; + comment: string; +} + +// Pad every command to the longest one so the `#` comments line up, whatever the +// package-manager prefix length (npm's `run … --` vs a bare `pnpm docmap`). This +// is why the command lists for short help, full help, and the guide all render +// through one helper instead of carrying hand-counted padding. +function renderCommands(rows: CommandRow[], indent = ""): string { + const width = Math.max(...rows.map((row) => row.command.length)); + return rows.map((row) => `${indent}${row.command.padEnd(width)} # ${row.comment}`).join("\n"); +} + +// Everyday browse/search commands, shared by short help, full help, and the guide. +// Examples carry the `docs/` prefix to nudge agents toward valid, openable paths. +function browseCommands(pm: PackageManagerCommands): CommandRow[] { + return [ + { command: pm.base, comment: "list root documents" }, + { command: `${pm.withArgs} docs/topic-a`, comment: "list a sub-directory" }, + { command: `${pm.withArgs} docs/intro.md`, comment: "read a document" }, + { command: `${pm.withArgs} docs/intro.md docs/setup.md`, comment: "read several at once" }, + { command: `${pm.withArgs} --recursive`, comment: "list every document" }, + { + command: `${pm.withArgs} --search "term1 term2"`, + comment: "search frontmatter (title, summary, read_when)", + }, + ]; +} + +function moreCommands(pm: PackageManagerCommands): CommandRow[] { + return [ + { command: `${pm.withArgs} --check`, comment: "validate names and frontmatter" }, + { command: `${pm.withArgs} --root `, comment: "use a custom docs root" }, + ]; +} + +// The guide shows the browse/search set plus validation. +function guideCommands(pm: PackageManagerCommands): CommandRow[] { + return [ + ...browseCommands(pm), + { command: `${pm.withArgs} --check`, comment: "validate all files" }, + ]; +} + function renderHelp(pm: PackageManagerCommands, { full }: HelpOptions): string { const lines = [ "docmap — browse and read a project's docs/ tree of Markdown files.", "", - "Browse:", - ` ${pm.base} # list root documents`, - ` ${pm.withArgs} topic-a # list a subdirectory`, - ` ${pm.withArgs} topic-a/doc.md # read a document`, - ` ${pm.withArgs} --recursive # list every document`, + "Commands:", + renderCommands(browseCommands(pm), " "), "", `To write documentation, run \`${pm.withArgs} --guide\` first.`, ]; @@ -163,9 +205,7 @@ function renderHelp(pm: PackageManagerCommands, { full }: HelpOptions): string { lines.push( "", "More:", - ` ${pm.withArgs} --search "term1 term2" # match frontmatter (title, summary, read_when)`, - ` ${pm.withArgs} --check # validate names and frontmatter`, - ` ${pm.withArgs} --root # use a custom docs root`, + renderCommands(moreCommands(pm), " "), "", "Positional paths are classified by the filesystem: a directory is listed, a file is read,", "an unmatched name falls back to a fuzzy basename search. The docs/ prefix is optional on input.", @@ -176,7 +216,10 @@ function renderHelp(pm: PackageManagerCommands, { full }: HelpOptions): string { function renderGuide(pm: PackageManagerCommands): string { const template = readFileSync(new URL("../templates/guide.md", import.meta.url), "utf-8"); - return template.replaceAll("{{PM_ARGS}}", pm.withArgs).replaceAll("{{PM}}", pm.base); + return template + .replaceAll("{{COMMANDS}}", renderCommands(guideCommands(pm))) + .replaceAll("{{PM_ARGS}}", pm.withArgs) + .replaceAll("{{PM}}", pm.base); } interface ClassifiedTargets { diff --git a/packages/docmap/templates/guide.md b/packages/docmap/templates/guide.md index fe6d8a8..f81b898 100644 --- a/packages/docmap/templates/guide.md +++ b/packages/docmap/templates/guide.md @@ -7,13 +7,7 @@ All project documentation lives in the `docs/` directory. The `docmap` CLI lets Targets are positional paths; the CLI classifies each by inspecting the filesystem (directory → list, file → read). Pass several at once. The `docs/` prefix shown in listings is optional on input. ```bash -{{PM}} # list root docs -{{PM_ARGS}} topic-a topic-b/sub-topic-c # list subdirectories -{{PM_ARGS}} --recursive # list everything -{{PM_ARGS}} doc-1.md topic-a/doc-2.md # read documents (frontmatter stripped) -{{PM_ARGS}} topic-a topic-a/doc-1.md # mix listing and reading -{{PM_ARGS}} --search "term1 term2" # search frontmatter -{{PM_ARGS}} --check # validate all files +{{COMMANDS}} ``` ## Workflow diff --git a/packages/docmap/test/docmap.test.ts b/packages/docmap/test/docmap.test.ts index 966a1fe..0aea250 100644 --- a/packages/docmap/test/docmap.test.ts +++ b/packages/docmap/test/docmap.test.ts @@ -56,6 +56,7 @@ describe("recursive-by-default for small sets (basic fixture)", () => { const { code, stdout } = run([], fixtures.basic); expect(code).toBe(0); expect(stdout).toContain("--guide"); + expect(stdout).toContain("--search"); expect(stdout).toContain(dp(fixtures.basic, "code-style.md")); expect(stdout).toContain("Code Style"); expect(stdout).toContain("Conventions and formatting rules for the codebase."); @@ -417,6 +418,24 @@ describe("--help", () => { expect(stdout).not.toContain(dp(fixtures.basic, "code-style.md")); expect(stdout).not.toContain(" { + const { stdout } = run(["--help"], fixtures.basic); + expect(stdout.indexOf("--search")).toBeLessThan(stdout.indexOf("More:")); + }); + + it("aligns the inline comments within a command group", () => { + const { stdout } = run(["--help"], fixtures.basic); + const lines = stdout.split("\n"); + const start = lines.indexOf("Commands:") + 1; + const columns: number[] = []; + for (let i = start; i < lines.length && lines[i].startsWith(" "); ++i) { + columns.push(lines[i].indexOf(" # ")); + } + // Every command line in the group shares one comment column. + expect(columns.length).toBeGreaterThan(1); + expect(new Set(columns).size).toBe(1); + }); }); describe("--guide", () => { From 6eba4d64c0f7cc92538f91e1500b7ffe0721e53b Mon Sep 17 00:00:00 2001 From: Paleo Date: Mon, 22 Jun 2026 08:35:32 +0200 Subject: [PATCH 5/7] feat: enhance search functionality to match file basenames and directory segments --- docs/docmap-architecture.md | 4 ++-- packages/docmap/README.md | 6 +++--- packages/docmap/src/cli.ts | 28 ++++++++++++++-------------- packages/docmap/src/formatter.ts | 2 +- packages/docmap/test/docmap.test.ts | 15 +++++++++++++++ 5 files changed, 35 insertions(+), 20 deletions(-) diff --git a/docs/docmap-architecture.md b/docs/docmap-architecture.md index e3fa621..f8db3ca 100644 --- a/docs/docmap-architecture.md +++ b/docs/docmap-architecture.md @@ -34,8 +34,8 @@ Help and guide command lists are built from shared `CommandRow[]` builders (`bro 1. **Parse args** — `parseArgs()` extracts the mode flags `--help`, `--guide`, `--search `, `--recursive`, `--root `, `--check`, collects positional **paths**, and collects unknown `--flags`. `--search` consumes the next token as its value (same shape as `--root`); without a following token it is recorded as unknown. Tokens starting with `--` are never paths; an unknown one is recorded and later warned about on stderr (so a stale `docmap --dir topic-a` still works — `--dir` is skipped, `topic-a` is a positional). `parseArgs` is pure (no writes). 2. **Resolve base directory** — `--root` or default `docs/` relative to `cwd`. The **display prefix** is `relative(cwd, baseDir)` — `docs` by default, or the root's path for a custom `--root` (e.g. `config/docs`, or `../shared` outside cwd). All displayed paths carry this prefix, so they stay real and openable; the empty prefix (root === cwd) yields bare paths. -3. **Mode dispatch** — modes are tried in precedence and each returns early after printing only its own output: `--help` (full help) → `--guide` (authoring guide) → `--search` (frontmatter match) → `--check` (validation) → listing/read. `--help`/`--guide`/`--search` always return `0`; only `--check` can return `1`. Help and guide text is rendered from the auto-detected package manager so every shown command is copy-pasteable. -4. **`--search`** → `searchDocs()` walks the tree (`collectAllFiles`), reads each file, and keeps the ones whose joined `title + summary + read_when` text contains every whitespace-split term (case-insensitive; body text is not searched). Matches render as file bullets, or a single `No documents match: ` line. +3. **Mode dispatch** — modes are tried in precedence and each returns early after printing only its own output: `--help` (full help) → `--guide` (authoring guide) → `--search` (path + frontmatter match) → `--check` (validation) → listing/read. `--help`/`--guide`/`--search` always return `0`; only `--check` can return `1`. Help and guide text is rendered from the auto-detected package manager so every shown command is copy-pasteable. +4. **`--search`** → `searchDocs()` walks the tree (`collectAllFiles`), reads each file, and keeps the ones whose joined `relative-path + title + summary + read_when` text contains every whitespace-split term (case-insensitive; body text is not searched). Including the relative path means a basename or directory segment matches even when absent from frontmatter. Matches render as file bullets, or a single `No documents match: ` line. 5. **Classify positionals** — each path is normalized (trailing slashes stripped, leading display prefix removed; the bare prefix → root), resolved under the base dir, then `statSync`-tested. Existing directory → listing bucket; everything else (existing file, or missing) → read bucket. `statSync` throwing on a missing path is caught and treated as "not a directory". The filesystem is the source of truth — no extension heuristic. 6. **Listings** — produced for each directory-classified path via `listDirectory()`/`formatDirectory()` or `formatRecursive()`. With no positionals at all, or with `--recursive` and no directory positionals, the root is listed. A **bare** invocation (no positionals, none of `--recursive`/`--check`/`--help`/`--guide`/`--search`) lists recursively when the tree holds fewer than 20 `.md` files (`collectAllFiles` count), and is the only case prefixed with short help; at ≥20 it keeps the top-level listing. An explicit `--recursive` is unaffected. Files-only with `--recursive` off produces no listing. 7. **Reads** — each read-bucket path goes through `readDocFile()` (direct path, then fuzzy basename search). A resolved result is wrapped in `` tags; an unresolved one becomes a single generic `⚠ Not found: ` line (same wording for a mistyped file or directory — classification can't read intent, and a not-found read keeps exit code 0). Reads follow listings, separated by a blank line. diff --git a/packages/docmap/README.md b/packages/docmap/README.md index 2b2e5ba..08aa03b 100644 --- a/packages/docmap/README.md +++ b/packages/docmap/README.md @@ -82,7 +82,7 @@ npx @paleo/docmap docs/topic-a/doc-1.md docs/topic-b/doc-2.md # Mix directories and files in one call npx @paleo/docmap topic-a docs/topic-b/doc-2.md -# Search frontmatter (title, summary, read_when); every term must match +# Search path and frontmatter (title, summary, read_when); every term must match npx @paleo/docmap --search "api endpoint" # Validate all files (names, frontmatter) @@ -99,7 +99,7 @@ A bare invocation lists recursively when the tree holds fewer than 20 documents, Each positional path is resolved against the docs root: - **Existing directory** → listed (honoring `--recursive`). -- **Existing file** → read, frontmatter stripped. Any extension is accepted; an extensionless file works too. +- **Existing file** → read, frontmatter stripped. - **Neither** → a fuzzy basename search over `.md` files (so `database.md` resolves from anywhere in the tree). No match → a single `⚠ Not found: ` line. Listings display each path prefixed with the docs root **relative to your working directory** — `docs/…` by default, or whatever `--root` points to (e.g. `--root config/docs` shows `config/docs/…`). That prefix is optional on input and trailing slashes are tolerated: `docs/topic-a/`, `docs/topic-a`, and `topic-a` resolve identically — so listing output can be pasted straight back as arguments. @@ -110,7 +110,7 @@ Listings display each path prefixed with the docs root **relative to your workin | --- | --- | | `--help` | Print full help and exit. | | `--guide` | Print the authoring guide (conventions for writing documents) and exit. | -| `--search ` | List documents whose frontmatter (title, summary, read_when) matches every whitespace-separated term. | +| `--search ` | List documents whose path or frontmatter (title, summary, read_when) matches every whitespace-separated term. | | `--recursive` | Walk the entire tree. Applies to directory listings (root or positional). | | `--check` | Validate all files and directories. Reports name and frontmatter issues. | | `--root ` | Use a custom directory as the docs root instead of `docs/`. | diff --git a/packages/docmap/src/cli.ts b/packages/docmap/src/cli.ts index fc18780..350fdd2 100644 --- a/packages/docmap/src/cli.ts +++ b/packages/docmap/src/cli.ts @@ -1,16 +1,16 @@ import { existsSync, readFileSync, statSync } from "node:fs"; import { dirname, join, relative, resolve } from "node:path"; import { - checkAll, - collectAllFiles, - countFilesUpTo, - formatDirectory, - formatRecursive, - isUnder, - listDirectory, - readDocFile, - searchDocs, - type FormatResult, + checkAll, + collectAllFiles, + countFilesUpTo, + formatDirectory, + formatRecursive, + isUnder, + listDirectory, + readDocFile, + searchDocs, + type FormatResult, } from "./formatter.js"; // A bare invocation over fewer .md files than this lists recursively by default. @@ -166,13 +166,13 @@ function renderCommands(rows: CommandRow[], indent = ""): string { function browseCommands(pm: PackageManagerCommands): CommandRow[] { return [ { command: pm.base, comment: "list root documents" }, - { command: `${pm.withArgs} docs/topic-a`, comment: "list a sub-directory" }, - { command: `${pm.withArgs} docs/intro.md`, comment: "read a document" }, - { command: `${pm.withArgs} docs/intro.md docs/setup.md`, comment: "read several at once" }, + { command: `${pm.withArgs} topic-a`, comment: "list a sub-directory" }, + { command: `${pm.withArgs} docs/doc-1.md`, comment: "read a document" }, + { command: `${pm.withArgs} docs/doc-1.md docs/doc-2.md topic-b`, comment: "several at once" }, { command: `${pm.withArgs} --recursive`, comment: "list every document" }, { command: `${pm.withArgs} --search "term1 term2"`, - comment: "search frontmatter (title, summary, read_when)", + comment: "search path and frontmatter (title, summary, read_when)", }, ]; } diff --git a/packages/docmap/src/formatter.ts b/packages/docmap/src/formatter.ts index 934ff33..3806be5 100644 --- a/packages/docmap/src/formatter.ts +++ b/packages/docmap/src/formatter.ts @@ -247,7 +247,7 @@ export function searchDocs(baseDir: string, terms: string[], prefix: string): st const relDir = slash === -1 ? "" : rel.slice(0, slash); const name = slash === -1 ? rel : rel.slice(slash + 1); const entry = buildFileEntry(join(baseDir, relDir), name); - const haystack = [entry.title, entry.summary, ...entry.readWhen] + const haystack = [rel, entry.title, entry.summary, ...entry.readWhen] .filter((part): part is string => part !== undefined) .join(" ") .toLowerCase(); diff --git a/packages/docmap/test/docmap.test.ts b/packages/docmap/test/docmap.test.ts index 0aea250..a454777 100644 --- a/packages/docmap/test/docmap.test.ts +++ b/packages/docmap/test/docmap.test.ts @@ -466,6 +466,21 @@ describe("--search", () => { expect(stdout).not.toContain(dp(fixtures.basic, "backend/database.md")); }); + it("matches the file basename even when absent from frontmatter", () => { + const { code, stdout } = run(["--search", "code-style"], fixtures.basic); + expect(code).toBe(0); + expect(stdout).toContain(dp(fixtures.basic, "code-style.md")); + expect(stdout).not.toContain(dp(fixtures.basic, "getting-started.md")); + }); + + it("matches a directory segment of the path", () => { + const { code, stdout } = run(["--search", "backend"], fixtures.basic); + expect(code).toBe(0); + expect(stdout).toContain(dp(fixtures.basic, "backend/database.md")); + expect(stdout).toContain(dp(fixtures.basic, "backend/api-guide.md")); + expect(stdout).not.toContain(dp(fixtures.basic, "code-style.md")); + }); + it("reports when nothing matches", () => { const { code, stdout } = run(["--search", "zzznomatch"], fixtures.basic); expect(code).toBe(0); From 07f4b5b3d6ce24d1001cf3253384a2e4fd021098 Mon Sep 17 00:00:00 2001 From: Paleo Date: Mon, 22 Jun 2026 08:37:47 +0200 Subject: [PATCH 6/7] feat: update browseCommands to reflect new document structure in CLI --- packages/docmap/src/cli.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/docmap/src/cli.ts b/packages/docmap/src/cli.ts index 350fdd2..762bfa2 100644 --- a/packages/docmap/src/cli.ts +++ b/packages/docmap/src/cli.ts @@ -167,8 +167,11 @@ function browseCommands(pm: PackageManagerCommands): CommandRow[] { return [ { command: pm.base, comment: "list root documents" }, { command: `${pm.withArgs} topic-a`, comment: "list a sub-directory" }, - { command: `${pm.withArgs} docs/doc-1.md`, comment: "read a document" }, - { command: `${pm.withArgs} docs/doc-1.md docs/doc-2.md topic-b`, comment: "several at once" }, + { command: `${pm.withArgs} docs/topic-a/doc-1.md`, comment: "read a document" }, + { + command: `${pm.withArgs} docs/topic-a/doc-1.md docs/doc-2.md topic-b`, + comment: "several at once", + }, { command: `${pm.withArgs} --recursive`, comment: "list every document" }, { command: `${pm.withArgs} --search "term1 term2"`, From f7e076b1513d4918b27a13c741b256b3293d3998 Mon Sep 17 00:00:00 2001 From: Paleo Date: Mon, 22 Jun 2026 08:38:25 +0200 Subject: [PATCH 7/7] chore: changesets --- .changeset/self-documenting-docmap-cli.md | 5 ----- .changeset/workspace-setup-guide-pointer.md | 5 ----- packages/docmap/CHANGELOG.md | 6 ++++++ packages/docmap/package.json | 2 +- packages/workspace/CHANGELOG.md | 6 ++++++ packages/workspace/package.json | 2 +- 6 files changed, 14 insertions(+), 12 deletions(-) delete mode 100644 .changeset/self-documenting-docmap-cli.md delete mode 100644 .changeset/workspace-setup-guide-pointer.md diff --git a/.changeset/self-documenting-docmap-cli.md b/.changeset/self-documenting-docmap-cli.md deleted file mode 100644 index b2ce759..0000000 --- a/.changeset/self-documenting-docmap-cli.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@paleo/docmap": minor ---- - -Self-documenting CLI: add `--help`, `--guide`, and `--search`. Bare run lists recursively for small doc sets. No-lockfile fallback now suggests the package-runner form. diff --git a/.changeset/workspace-setup-guide-pointer.md b/.changeset/workspace-setup-guide-pointer.md deleted file mode 100644 index e504042..0000000 --- a/.changeset/workspace-setup-guide-pointer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@paleo/workspace": patch ---- - -Point README Setup at the `alignfirst-setup-guide` skill. diff --git a/packages/docmap/CHANGELOG.md b/packages/docmap/CHANGELOG.md index a587666..252bdf5 100644 --- a/packages/docmap/CHANGELOG.md +++ b/packages/docmap/CHANGELOG.md @@ -1,5 +1,11 @@ # @paleo/docmap +## 0.6.0 + +### Minor Changes + +- 4396c78: Self-documenting CLI: add `--help`, `--guide`, and `--search`. Bare run lists recursively for small doc sets. No-lockfile fallback now suggests the package-runner form. + ## 0.5.1 ### Patch Changes diff --git a/packages/docmap/package.json b/packages/docmap/package.json index 2b5bef7..f311565 100644 --- a/packages/docmap/package.json +++ b/packages/docmap/package.json @@ -1,6 +1,6 @@ { "name": "@paleo/docmap", - "version": "0.5.1", + "version": "0.6.0", "license": "CC0-1.0", "author": "Thomas MUR", "description": "A lightweight documentation system for AI agents and humans.", diff --git a/packages/workspace/CHANGELOG.md b/packages/workspace/CHANGELOG.md index 35f8621..7dba823 100644 --- a/packages/workspace/CHANGELOG.md +++ b/packages/workspace/CHANGELOG.md @@ -1,5 +1,11 @@ # @paleo/workspace +## 0.19.1 + +### Patch Changes + +- 4396c78: Point README Setup at the `alignfirst-setup-guide` skill. + ## 0.19.0 ### Minor Changes diff --git a/packages/workspace/package.json b/packages/workspace/package.json index 723e023..1b3af54 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -1,6 +1,6 @@ { "name": "@paleo/workspace", - "version": "0.19.0", + "version": "0.19.1", "description": "Run multiple git-worktree dev environments side by side.", "keywords": [ "workspace",