diff --git a/AGENTS.md b/AGENTS.md index 6ee723e..a1e16ba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code), Codex CLI, and othe ## Project Overview -`vault-onboarding` is an Agent Skills-standard onboarding skill that bootstraps an agent collaboration system into an Obsidian vault. It runs a branching conversational interview and scaffolds memory files, session protocols, task boards, generated skills, and per-client instruction files — all tailored to the user's setup and needs. +`vault-onboarding` is an onboarding skill in the Agent Skills format (see Skill Architecture) that bootstraps an agent collaboration system into an Obsidian vault. It runs a branching conversational interview and scaffolds memory files, a session protocol (the steps an agent runs at the start and end of each work session), task boards, generated skills, and instruction files for each AI client the user works in (Claude Code, Cursor, claude.ai, and others; Claude Code, Cowork and Perplexity each get a global file plus a per-project one) — all tailored to the user's setup and needs. Distribution: `npx skills add aliasunder/vault-onboarding`. @@ -16,7 +16,7 @@ vault-onboarding/ skills/vault-onboarding/ # The installable skill payload — everything under # here (and only this) ships via `npx skills add` SKILL.md # Interview flow + generation orchestration - references/ # 7 best-practice reference files (loaded on demand) + references/ # Best-practice reference files (loaded on demand) vault-organization.md # Folder taxonomy, tags, linking, schemas memory-system.md # Three-layer model, entry policies, growth protocol-guide.md # Session protocol, board conventions @@ -24,13 +24,13 @@ vault-onboarding/ generated-skills-guide.md # Skill parameterization progressive-adoption.md # Adoption tiers, evolution paths setup-verification.md # Per-client verification - assets/ # 20 asset files (16 templates + 4 skills) + assets/ # Templates the agent fills in and writes to the vault templates/memory/ # 5 About Me/ templates templates/instructions/ # 8 client-specific instruction templates templates/protocol.md # Genericized session protocol templates/tasks-board.md # 5-lane Kanban templates/onboarding-progress.md # Checkpoint file - skills/ # 4 generated skill templates + skills/ # Generated skill templates session-start.md session-end.md remember.md @@ -38,7 +38,11 @@ vault-onboarding/ scripts/ validate-structure.ts # Structural coherence checks (run by CI) render-social-preview.ts # Renders social-preview.svg → .png via Puppeteer - .github/workflows/validate.yml # Runs the structural checks on PR + push + .github/workflows/ + validate.yml # Runs the structural checks on PRs + pushes to main + umm_review.yml # Runs the umm-actually PR review bot + manual_release.yml # Releases: bump, tag, zip, GitHub release (see CONTRIBUTING.md) + auto_release.yml # Releases from a v* tag push (see CONTRIBUTING.md) .github/social-preview.svg + .png # Repo social card (SVG source + committed render) .github/fonts/ # Fonts embedded by the render script ``` @@ -49,14 +53,15 @@ The skill follows the Agent Skills standard (`agentskills.io`) and lives entirel Reference files (`references/*.md`) are loaded on demand — only when the agent enters a phase that needs them. This keeps the core SKILL.md lean while having deep guidance available. -Asset templates (`assets/`) are literal markdown files with `{{VARIABLE}}` markers. The agent reads, substitutes, writes. +Asset templates (`assets/`) are literal markdown files with `{{VARIABLE}}` markers. The agent reads a template, keeps or drops each `{{#IF_X}}` conditional block, replaces each remaining marker with the user's interview answer or a computed value (SKILL.md's "Variable Reference" table names each variable's source), and writes the result into the user's vault. ## Development Guidelines -- No build step — the skill itself is pure markdown and installs with no dependencies; `devDependencies` cover repo tooling only (bun-managed, `bun.lock`), and `scripts/` is runtime-agnostic TypeScript run directly (`bun run validate` locally; CI runs the same file with Node 24's type stripping) +- No build step — the skill itself is pure markdown and installs with no dependencies; `devDependencies` cover repo tooling only (bun-managed, `bun.lock`) +- `scripts/` holds TypeScript that runs directly under Bun or Node: `bun run validate` runs `scripts/validate-structure.ts` under Bun locally, while CI runs the same file with `node` on Node 24, which strips the types, and the `render:social-preview` package script runs `scripts/render-social-preview.ts` with `node` - All template files use `{{VARIABLE}}` markers for substitution -- Generated skills are triggers, not copies — they say "follow the protocol in your instruction file" -- The SKILL.md itself uses only native file tools (Read, Write, Bash) — vault-cortex MCP tools only appear in GENERATED artifacts +- Generated skills are triggers, not copies — session-start and session-end each tell the agent to follow the session protocol in the client's instruction file (`CLAUDE.md`, Global Instructions, or the client's equivalent), so the protocol text lives in one place +- The SKILL.md itself uses only the client's built-in file tools (Read, Write, Bash). Tools from the optional vault-cortex MCP server (`vault_*`, for remote vault access and search) appear only in GENERATED artifacts, and only when the user has vault-cortex connected - Interview questions use plain language accessible to non-technical users ## Social preview card @@ -69,67 +74,111 @@ skill payload and must stay free of repo branding. Update the SVG when feature categories change (the feature line is rendered in the image), then regenerate the PNG. -**Regenerating `social-preview.png`:** Run `bun run render:social-preview`. -The script uses Puppeteer's pinned Chrome for Testing build with embedded -JetBrains Mono SemiBold (wordmark) and DejaVu Sans (body text) `@font-face` -for deterministic rendering regardless of host system fonts. No browser -downloads at install time — `bun install` skips postinstall scripts and the -`puppeteer.skipDownload` key covers npm installs; the script installs the -pinned browser on demand. Design family: matches vault-cortex's card (dark -tile, glow nodes) with an inverted violet-primary palette. +**To regenerate `social-preview.png`,** run `bun run render:social-preview`. +That `package.json` script installs the Chrome for Testing build that the +`puppeteer` version pinned in `package.json` expects, then runs +`scripts/render-social-preview.ts` with `node`. The render embeds JetBrains +Mono SemiBold (wordmark) and DejaVu Sans (body text) through `@font-face` for +deterministic rendering regardless of host system fonts. No browser downloads +at install time: `bun install` and npm both run puppeteer's postinstall +script (puppeteer is on Bun's default list of trusted dependencies), and the +`puppeteer.skipDownload` key in `package.json` makes that script skip the +download. + +**Design family:** the card follows the social card of the sibling repo +[aliasunder/vault-cortex](https://github.com/aliasunder/vault-cortex) (a dark +tile with glowing nodes) and swaps its colors, with violet as the primary +color and cyan as the accent. ## Adding a reference file 1. Create `skills/vault-onboarding/references/my-reference.md` -2. Add a pointer in SKILL.md's "How This Skill Works" section with path, description, and bold trigger condition -3. Reference from the appropriate interview phase +2. Add a bullet to the **Reference files** list in SKILL.md's "How This Skill Works" section: the path, a one-line description, and a bold sentence saying when to read it (for example **Read before Phase 4 (Memory).**) +3. In each SKILL.md interview phase whose questions or generation steps use the file, add a bold line at the top of the phase: **Read `references/my-reference.md` before this phase.** When only one step of the phase uses the file, name that step instead, as Phase 6 does: **Read `references/generated-skills-guide.md` during skill generation.** +4. Add the file to the `references/` list in this file's Repository Structure tree, and update the reference-file count and list in README.md's Status section ## Code standards - + -This repo is primarily markdown (SKILL.md, references, templates) with TypeScript in `scripts/`. Docs standards apply everywhere; TS standards apply to `scripts/`. +This repo is primarily markdown (SKILL.md, references, templates, README) with TypeScript in `scripts/`. Docs standards apply everywhere; TS and comment standards apply to `scripts/`. There is no test suite; CI runs `scripts/validate-structure.ts`. + +These rules are authoring guidance: apply them to the prose and code you write. An existing file that predates a rule is brought into line when a change already edits that passage, never in a sweep of its own. ### Writing (all files) **Format decision at write time** — before committing any prose: -1. Information (a setting's behavior, a list of options) → structured format (table, bullets, numbered steps). Narrative → PR description, not committed files. -2. More than 3 sentences of prose? → wrong format. Pick what the reader absorbs quickest — table for lookups, bullets for parallel items, numbered list for steps. Multi-paragraph "breakdowns" are still prose. -3. A section twice the length of its siblings is a blob even when each sentence is fine. -4. One idea per sentence; one action per step. - -**Language:** -- Plain-first — jargon with real signal stays with a one-line gloss; session shorthand never ships. Test: would the reader know what it means without looking anything up? -- Factual claims match the implementation — capability lists verified against the code; conditional capabilities stated conditionally. -- Mechanism language is earned — "caches", "batches" only when the code does that, not merely the outcome. -- Describe what the feature does, not why someone would use it. -- Concrete referents at the point of use — state the specific name where the reader is, not "the file" paragraphs away. -- No blobs. No filler lead-ins. +1. Information (a setting's behavior, a list of options) → structured format (table, bullets, numbered steps). Narrative (why a design was chosen, what a change replaced) → PR description, not committed files. +2. More than 3 sentences of prose? → probably the wrong format: past that length the reader cannot find their answer without reading the whole block. Pick what the reader absorbs quickest — table for lookups, bullets for parallel items, numbered list for steps. Splitting the prose into several short paragraphs does not change its format. +3. A section far longer than its siblings is a blob even when each sentence is fine. Read the neighbors first. +4. One idea per sentence; one action per step. A new requirement bolted onto an existing step becomes its own step. -**Multi-path docs** — when offering multiple install/setup methods: every operational section serves every path or scopes itself explicitly. Never reference a path the doc doesn't offer. Resource identifiers compatible across methods. +**The authoring test** — line-level fact-checking cannot validate a document. For each reader the doc serves (a non-technical vault owner, an agent following SKILL.md), walk the whole artifact: sentence, placement, format, sibling contradictions, purpose. After editing one section, re-read the whole document, not the diff. -**Lockstep** — docs update in the same change that alters behavior. Sweep every surface that enumerates siblings when adding a concept. - -**Restructuring** — structural self-references ("shown below", "the section above") must resolve against the current document. Sibling docs are authored as a set. +**Language:** +- Plain-first — jargon with real signal stays with a one-line gloss; plain words get no gloss. Terms an author coins while working (shorthand, an invented name for a group of items) never ship; name what the items do instead. Test: would the reader know what it means without looking anything up? +- Factual claims match the implementation — capability lists verified against the templates and scripts; conditional capabilities stated conditionally. +- Mechanism language is earned — "caches", "batches", "switches automatically" only when the code does that, not merely the outcome. +- Feature descriptions state what the feature does, not why someone would use it. +- Procedure copy (steps the reader follows) opens with the reader's motive ("To add a client, …"), not the operation's name. +- Concrete referents at the point of use — state the specific name where the reader is (`onboarding-progress.md`, not "the checkpoint file" paragraphs away). A definite noun must have been introduced first. +- No implicit contrasts or empty intensifiers — "not just developers" reads as a defense against a doubt nobody raised, and "**real** task management" implies a fake one. State the capability affirmatively. +- Troubleshooting entries end in numbered steps the reader can take, with a safe fallback, never in a diagnosis alone. +- A destructive-outcome claim pairs with its recovery path in the same sentence or table cell, on every surface that makes the claim. +- Setting references (a README option table, an env-file comment) state the knob, its effect, and its limits, with `Valid values:` and `Default:` on their own labelled lines. SKILL.md's Variable Reference table is not one: its rows are slots the agent fills, most with no default. +- Defensive copy ("use at your own risk") enters a public doc only after the problem it guards against has occurred. +- Reviewed copy usually has a job — before cutting a "redundant" line, check why it exists; replace only the clause a change made stale. +- Prerequisites follow the reader — lead with the one install the audience needs, with a link; demote developer conveniences to an "already have it?" aside. +- No blobs. No filler lead-ins. Configuration guidance is a lookup table (situation → setting → outcome). + +**Multi-path docs** — when a doc offers more than one path to the same goal (setup steps that differ per client, or with and without vault-cortex): +- Every operational section serves every path, or scopes itself explicitly. +- Never reference a path the doc doesn't offer. +- Names that several paths create or read (vault folders, generated file paths) stay the same on every path, or the doc says the paths are not interchangeable. +- A note that applies to every path sits outside all of them. Placed inside one path's steps, it stays invisible to readers of the other paths. + +**Lockstep** — docs update in the same change that alters behavior: +- Adding a reference, template, or client means sweeping every surface that enumerates its siblings, counts included. The known surfaces follow; grep the repo for an existing sibling's name to catch any others: + - A reference file: the surfaces the "Adding a reference file" steps above name. + - A template: the structure tree above, SKILL.md's **Asset templates** list, README.md, and the asset-inventory check in `scripts/validate-structure.ts`. + - A client: its instruction template's surfaces (the template line above), SKILL.md's Phase 0 client options and Phase 7 template table, the client list at the top of README.md, and every per-client section or table in `references/`. +- A derived artifact (the social-preview PNG) is regenerated in the same change. +- Instructions that tell an agent how to generate files (SKILL.md's generation steps) hold structure only. Capability claims and explanation text live in the `references/` file the agent reads during that phase, so each claim is written once. + +**Restructuring:** +- A structural self-reference ("shown below", "the section above") must resolve against the current document. +- Sibling docs (the per-client instruction templates) are authored as a set, with the same section order and the same recommended approach listed first, diverging only where the client genuinely differs. +- A feature spanning several docs gets one conceptual home; every other mention points to it. ### Comments (TypeScript in scripts/) **Write-time decision:** 1. Can a reader understand the function from name + params + return type? → **No comment.** Most functions. 2. Something non-obvious? → One-line JSDoc stating the constraint the signature doesn't convey. -3. Does the JSDoc restate the function name? → **Delete it.** +3. Does the JSDoc restate the function name? → **Delete it.** Explains *what* instead of *why*? → rewrite as the constraint. 4. More than 2 lines? → Pick the format the reader absorbs quickest (bullets, numbered steps) — never multi-paragraph prose. **Inline comments at the relevant line, not everything in the docstring.** The docstring states the outward contract; line-level concerns go above the line they explain. +- A guard or branch comment states the scenario, the mechanism, and what breaks without it. +- A chosen number (a timeout, a pinned browser build, a cap) gets its why directly above it. A number an outside spec sets (the 1280×640 card size GitHub recommends) needs no comment. +- Regex constants get a comment. -Durable rationale only — never transition history or operator internals. OSS boundary: no issue/PR numbers, incident dates, deployment names, or task-board IDs in any public artifact. +**Comment content:** +- Short complete sentences. A comment never opens with a label and a colon (`// Fallback: cached copy`) and never uses verbless fragments. +- Durable rationale only, never transition history ("renamed from X"), decision narrative, or details of the maintainer's own machine or setup. A comment that stops a future editor from reintroducing an approach that already failed counts as durable rationale. +- OSS boundary: no issue/PR numbers, incident dates, deployment names, or task-board IDs in any public artifact. ### TypeScript (scripts/ only) -- No semicolons. Arrow `const`s over `function` declarations. -- `const` by default; no unjustified `let`. Readability gates all refactors. -- Early return from the simpler branch; block bodies for multiline responses. -- `type` over `interface`. Truthy/falsy checks over explicit comparisons unless 0/empty/false are valid. -- `as` and `!` are the same sin — runtime guards instead. -- Values named for what they ARE; functions named for their point. -- Standalone scripts are `.ts`, erasable syntax (no enums/namespaces). +- No semicolons. Arrow `const`s over `function` declarations; leaf helpers before their callers. +- `const` by default; no unjustified `let`. Never disguise mutation as a fold (`reduce` with `acc.push()`); a helper never mutates its inputs and types collection params as readonly views. Readability gates all refactors. +- Early return from the simpler branch; block bodies for multiline responses. Never a chained ternary or a `let` assigned through if/else branches — one `if … return` per branch, or one named const per step. +- Named params when a positional call hides roles (an object above two args, adjacent same-typed args). A boolean mode param means the function does two things — split it. +- Named constants over bare magic literals (modes, sizes, dimensions), with the one-line why at the definition, scoped to where they are used. +- `type` over `interface`. Truthy/falsy checks over explicit comparisons unless 0/empty/false are valid. `Boolean(x)` over `!!x`. +- `as` and `!` are the same sin — runtime guards or type-guard functions instead. Model states in the type system: a result with modes is a discriminated union, branched with an exhaustive `switch`, never an `if` plus a ternary whose fallback catches the rest unnamed. +- Values named for what they ARE (no single letters or abbreviations); functions named for their point; booleans named for the affirmative state. +- Platform built-ins before hand-rolled parsing (`URL.parse`, `node:path`); exact-value comparison before regex; a doc-commented regex with named groups only when values genuinely vary. +- Per-operation try/catch — each catch encloses one operation with one failure meaning; broad catch-alls are banned. Every catch logs or re-throws; a swallowed error is worse than an uncaught one. Throw on an unreachable null, never a `?? ""` sentinel. +- File and process work uses Node built-ins (`node:fs`, `node:path`, `node:child_process`). The synchronous `node:fs` calls are the default; use `fs/promises` where file modes or an atomic rename matter. Never Bun-only APIs (`Bun.Glob`, `Bun.spawn`, `Bun.$`), because Node runs every script in `scripts/` (see Development Guidelines). +- Scripts are `.ts` files with erasable syntax only (no enums or namespaces), because Node's type stripping deletes type annotations but cannot compile other TypeScript syntax. diff --git a/README.md b/README.md index ccb9c83..f6a900d 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Works without [vault-cortex](https://github.com/aliasunder/vault-cortex) using n ## Status -vault-onboarding is in active development and not yet released. The full implementation — interview flow, reference files, asset templates, generated skills — is merged ([PR #1](https://github.com/aliasunder/vault-onboarding/pull/1)) and now in validation. The install command works, but treat it as pre-release until testing passes and a first GitHub release is cut. +vault-onboarding is in active development and not yet released. The install command works, but treat it as pre-release until testing passes and a first GitHub release is cut. **Built** diff --git a/scripts/render-social-preview.ts b/scripts/render-social-preview.ts index df1ca14..d72073c 100644 --- a/scripts/render-social-preview.ts +++ b/scripts/render-social-preview.ts @@ -1,11 +1,7 @@ -// Renders .github/social-preview.svg to .github/social-preview.png using -// Puppeteer's pinned Chrome for Testing build (installed on demand by the npm -// script — `npm ci` skips the download via the `puppeteer.skipDownload` key in -// package.json). Embeds JetBrains Mono SemiBold (wordmark) and DejaVu Sans -// (body text) via @font-face for deterministic rendering regardless of host -// system fonts. Ported from vault-cortex's scripts/render-social-preview.ts. -// -// Usage: npm run render:social-preview +// This script renders .github/social-preview.svg to .github/social-preview.png +// with Puppeteer's pinned Chrome for Testing build. Run it with +// `bun run render:social-preview`; the "Social preview card" section of +// AGENTS.md covers setup and the browser install. import { execFileSync } from "node:child_process" import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs" diff --git a/scripts/validate-structure.ts b/scripts/validate-structure.ts index d5aa77f..b87478d 100644 --- a/scripts/validate-structure.ts +++ b/scripts/validate-structure.ts @@ -10,224 +10,250 @@ * stripping), bun, or tsx with no dependencies and no build step. */ -import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; -import { dirname, join, relative, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { existsSync, readFileSync, readdirSync, statSync } from "node:fs" +import { dirname, join, relative, resolve } from "node:path" +import { fileURLToPath } from "node:url" -const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); -const SKILL_DIR = join(ROOT, "skills", "vault-onboarding"); -const errors: string[] = []; +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..") +const SKILL_DIR = join(ROOT, "skills", "vault-onboarding") +const errors: string[] = [] -function check(label: string, ok: boolean, detail = ""): void { +const check = (label: string, ok: boolean, detail = ""): void => { if (ok) { - console.log(`PASS ${label}`); + console.log(`PASS ${label}`) } else { - errors.push(`${label}: ${detail}`); - console.log(`FAIL ${label}: ${detail}`); + errors.push(`${label}: ${detail}`) + console.log(`FAIL ${label}: ${detail}`) } } -function mdFilesUnder(dir: string): string[] { - const found: string[] = []; +const mdFilesUnder = (dir: string): string[] => { + const found: string[] = [] for (const entry of readdirSync(dir, { withFileTypes: true })) { - const full = join(dir, entry.name); - if (entry.isDirectory()) found.push(...mdFilesUnder(full)); - else if (entry.name.endsWith(".md")) found.push(full); + const full = join(dir, entry.name) + + if (entry.isDirectory()) found.push(...mdFilesUnder(full)) + else if (entry.name.endsWith(".md")) found.push(full) } - return found.sort(); + return found.toSorted() } -const read = (path: string): string => readFileSync(path, "utf8"); -const rel = (path: string): string => relative(ROOT, path); -const isFile = (path: string): boolean => existsSync(path) && statSync(path).isFile(); +const read = (path: string): string => readFileSync(path, "utf8") +const rel = (path: string): string => relative(ROOT, path) +const isFile = (path: string): boolean => existsSync(path) && statSync(path).isFile() -const assetFiles = mdFilesUnder(join(SKILL_DIR, "assets")); +const assetFiles = mdFilesUnder(join(SKILL_DIR, "assets")) // 1. SKILL.md frontmatter carries the Agent Skills required keys. -const skillText = read(join(SKILL_DIR, "SKILL.md")); -const frontmatter = skillText.match(/^---\n([\s\S]*?)\n---\n/); +// Matches the leading pair of --- fences; group 1 captures the YAML between them. +const FRONTMATTER_BLOCK = /^---\n([\s\S]*?)\n---\n/ +// Each matches its key at the start of a frontmatter line. +const NAME_KEY = /^name:/m +const DESCRIPTION_KEY = /^description:/m +const skillText = read(join(SKILL_DIR, "SKILL.md")) +const frontmatter = skillText.match(FRONTMATTER_BLOCK) check( "SKILL.md frontmatter has name + description", - frontmatter !== null && /^name:/m.test(frontmatter[1]) && /^description:/m.test(frontmatter[1]), + frontmatter !== null && NAME_KEY.test(frontmatter[1]) && DESCRIPTION_KEY.test(frontmatter[1]), "missing frontmatter block or required keys", -); +) -// 2. Marker well-formedness: every line's {{ and }} counts agree. -const occurrences = (text: string, token: string): number => text.split(token).length - 1; +// 2. Markers are well formed when every line's {{ and }} counts agree. +const occurrences = (text: string, token: string): number => text.split(token).length - 1 for (const path of [...assetFiles, join(SKILL_DIR, "SKILL.md")]) { const badLines = read(path) .split("\n") - .flatMap((line, index) => - occurrences(line, "{{") !== occurrences(line, "}}") ? [String(index + 1)] : [], - ); + .flatMap((line, index) => { + return occurrences(line, "{{") !== occurrences(line, "}}") ? [String(index + 1)] : [] + }) check( `balanced marker braces: ${rel(path)}`, badLines.length === 0, `unbalanced {{ }} on line(s) ${badLines.join(", ")}`, - ); + ) } -// 3. Conditional nesting: {{#IF_X}} / {{^IF_X}} open, {{/IF_X}} closes the -// innermost open block of the same name (block and inline forms alike). -const CONDITIONAL = /\{\{([#^/])(IF_[A-Z0-9_]+)\}\}/g; +// 3. Conditionals nest correctly when {{#IF_X}} or {{^IF_X}} opens a block and +// {{/IF_X}} closes the innermost open block of the same name (block and +// inline forms alike). +const CONDITIONAL = /\{\{([#^/])(IF_[A-Z0-9_]+)\}\}/g for (const path of assetFiles) { - const stack: Array<{ name: string; line: number }> = []; - let problem: string | null = null; - const lines = read(path).split("\n"); - outer: for (let i = 0; i < lines.length; i++) { - for (const match of lines[i].matchAll(CONDITIONAL)) { - const kind = match[1]; - const name = match[2]; + const stack: Array<{ name: string; line: number }> = [] + // The first mismatch stops the scan, so the loop threads it through this variable. + let problem: string | null = null + const lines = read(path).split("\n") + outer: for (const [index, line] of lines.entries()) { + for (const match of line.matchAll(CONDITIONAL)) { + const kind = match[1] + const name = match[2] + if (kind === "#" || kind === "^") { - stack.push({ name, line: i + 1 }); + stack.push({ name, line: index + 1 }) } else if (stack.pop()?.name !== name) { - problem = `line ${i + 1}: {{/${name}}} has no matching open`; - break outer; + problem = `line ${index + 1}: {{/${name}}} has no matching open` + break outer } } } - if (problem === null && stack.length > 0) { - const unclosed = stack[stack.length - 1]; - problem = `unclosed ${unclosed.name} opened on line ${unclosed.line}`; + const unclosed = stack.at(-1) + + if (problem === null && unclosed) { + problem = `unclosed ${unclosed.name} opened on line ${unclosed.line}` + } + if (problem === null) { + check(`conditional nesting: ${rel(path)}`, true) + } else { + check(`conditional nesting: ${rel(path)}`, false, problem) } - check(`conditional nesting: ${rel(path)}`, problem === null, problem ?? ""); } // 4. Internal path references in SKILL.md and references/ resolve on disk. -const PATH_REF = /`((?:references|assets)\/[A-Za-z0-9_\-./]+\.md)`/g; +const PATH_REF = /`((?:references|assets)\/[A-Za-z0-9_\-./]+\.md)`/g for (const path of [join(SKILL_DIR, "SKILL.md"), ...mdFilesUnder(join(SKILL_DIR, "references"))]) { - const refs = [...read(path).matchAll(PATH_REF)].map((match) => match[1]); - const missing = [...new Set(refs.filter((ref) => !isFile(join(SKILL_DIR, ref))))].sort(); + const refs = [...read(path).matchAll(PATH_REF)].map((match) => match[1]) + const missing = [...new Set(refs.filter((ref) => !isFile(join(SKILL_DIR, ref))))].toSorted() check( `internal paths resolve: ${rel(path)}`, missing.length === 0, `dead reference(s): ${missing.join(", ")}`, - ); + ) } // 5. Variable Reference table matches actual {{VAR}} usage in assets, both ways. +// Matches a Variable Reference table row; group 1 captures the documented name. +const DOCUMENTED_VARIABLE_ROW = /^\| `\{\{([A-Z][A-Z0-9_]*)\}\}`/gm +// Matches a {{VARIABLE}} marker in an asset; group 1 captures the name, conditionals included. +const VARIABLE_MARKER = /\{\{([A-Z][A-Z0-9_]*)\}\}/g const documented = new Set( - [...skillText.matchAll(/^\| `\{\{([A-Z][A-Z0-9_]*)\}\}`/gm)].map((match) => match[1]), -); -const used = new Set(); + [...skillText.matchAll(DOCUMENTED_VARIABLE_ROW)].map((match) => match[1]), +) +const used = new Set() for (const path of assetFiles) { - for (const match of read(path).matchAll(/\{\{([A-Z][A-Z0-9_]*)\}\}/g)) { - if (!match[1].startsWith("IF_")) used.add(match[1]); + for (const match of read(path).matchAll(VARIABLE_MARKER)) { + if (!match[1].startsWith("IF_")) used.add(match[1]) } } -const usedNotDocumented = [...used].filter((name) => !documented.has(name)).sort(); -const documentedNotUsed = [...documented].filter((name) => !used.has(name)).sort(); +const usedNotDocumented = [...used].filter((name) => !documented.has(name)).toSorted() +const documentedNotUsed = [...documented].filter((name) => !used.has(name)).toSorted() check( "variable table covers all used variables", usedNotDocumented.length === 0, `used but undocumented: ${usedNotDocumented.join(", ")}`, -); +) check( "variable table lists no unused variables", documentedNotUsed.length === 0, `documented but unused: ${documentedNotUsed.join(", ")}`, -); +) // 6. Asset inventory matches the structure AGENTS.md and SKILL.md advertise. const EXPECTED_COUNTS: Record = { "assets/templates/memory": 5, "assets/templates/instructions": 8, "assets/skills": 4, -}; +} for (const [folder, expected] of Object.entries(EXPECTED_COUNTS)) { - const actual = readdirSync(join(SKILL_DIR, folder)).filter((name) => name.endsWith(".md")).length; - check(`${folder} holds ${expected} files`, actual === expected, `found ${actual}`); + const actual = readdirSync(join(SKILL_DIR, folder)).filter((name) => name.endsWith(".md")).length + check(`${folder} holds ${expected} files`, actual === expected, `found ${actual}`) } for (const single of [ "assets/templates/protocol.md", "assets/templates/tasks-board.md", "assets/templates/onboarding-progress.md", ]) { - check(`${single} exists`, isFile(join(SKILL_DIR, single)), "missing"); + check(`${single} exists`, isFile(join(SKILL_DIR, single)), "missing") } -// 7. Heading cross-references: quoted heading names paired with a backtick-quoted -// reference-file path must resolve to an actual heading in the target file. -// Catches heading renames that silently break navigation aids for agents. -const HEADING_XREF_A = /"([^"]+)"\s+(?:section\s+of|in)\s+`(references\/[A-Za-z0-9_\-./]+\.md)`/g; -const HEADING_XREF_B = /`(references\/[A-Za-z0-9_\-./]+\.md)`\s*→\s*"([^"]+)"/g; -// Unquoted Title-Case heading named as a table/section/walkthrough of the file, -// e.g. "per the Skill Placement table in `references/generated-skills-guide.md`". +// 7. Heading cross-references resolve when a quoted heading name paired with a +// backtick-quoted reference-file path names an actual heading in the target +// file. This catches heading renames that silently break navigation aids. +const HEADING_XREF_A = /"([^"]+)"\s+(?:section\s+of|in)\s+`(references\/[A-Za-z0-9_\-./]+\.md)`/g +const HEADING_XREF_B = /`(references\/[A-Za-z0-9_\-./]+\.md)`\s*→\s*"([^"]+)"/g +// Matches an unquoted Title-Case heading named as a table, section, walkthrough or +// list of the file, e.g. "per the Skill Placement table in `references/generated-skills-guide.md`". const HEADING_XREF_C = - /\b([A-Z][A-Za-z0-9'&-]*(?:\s+[A-Z][A-Za-z0-9'&-]*)*)\s+(?:table|section|walkthrough|list)\s+(?:in|of)\s+`(references\/[A-Za-z0-9_\-./]+\.md)`/g; -const xrefFiles = [join(SKILL_DIR, "SKILL.md"), ...mdFilesUnder(join(SKILL_DIR, "references"))]; + /\b([A-Z][A-Za-z0-9'&-]*(?:\s+[A-Z][A-Za-z0-9'&-]*)*)\s+(?:table|section|walkthrough|list)\s+(?:in|of)\s+`(references\/[A-Za-z0-9_\-./]+\.md)`/g +// Matches a markdown heading's leading hashes and the whitespace after them. +const HEADING_PREFIX = /^#{1,6}\s+/ +const xrefFiles = [join(SKILL_DIR, "SKILL.md"), ...mdFilesUnder(join(SKILL_DIR, "references"))] for (const path of xrefFiles) { // Collapse line continuations so multi-line references match. - const text = read(path).replace(/\s+/g, " "); - const mismatches: string[] = []; + const text = read(path).replace(/\s+/g, " ") + const mismatches: string[] = [] const checkXref = (rawHeading: string, refFile: string): void => { - const heading = rawHeading.replace(/\s+/g, " ").trim(); - const targetPath = join(SKILL_DIR, refFile); - if (!isFile(targetPath)) return; // already caught by check 4 + const heading = rawHeading.replace(/\s+/g, " ").trim() + const targetPath = join(SKILL_DIR, refFile) + + if (!isFile(targetPath)) return // already caught by check 4 const headings = read(targetPath) .split("\n") - .filter((line) => /^#{1,6}\s/.test(line)) - .map((line) => line.replace(/^#{1,6}\s+/, "").trim()); + .filter((line) => HEADING_PREFIX.test(line)) + .map((line) => line.replace(HEADING_PREFIX, "").trim()) if (!headings.includes(heading)) { - mismatches.push(`"${heading}" not found in ${refFile}`); + mismatches.push(`"${heading}" not found in ${refFile}`) } - }; + } // Pattern A: "Heading" section of / in `references/file.md` for (const match of text.matchAll(new RegExp(HEADING_XREF_A))) { - checkXref(match[1], match[2]); + checkXref(match[1], match[2]) } // Pattern B: `references/file.md` → "Heading" for (const match of text.matchAll(new RegExp(HEADING_XREF_B))) { - checkXref(match[2], match[1]); + checkXref(match[2], match[1]) } // Pattern C: Skill Placement table in `references/file.md` (unquoted). // A sentence-leading "The" is part of the prose, not the heading. for (const match of text.matchAll(new RegExp(HEADING_XREF_C))) { - checkXref(match[1].replace(/^The\s+/, ""), match[2]); + checkXref(match[1].replace(/^The\s+/, ""), match[2]) } check( `heading cross-refs resolve: ${rel(path)}`, mismatches.length === 0, mismatches.join("; "), - ); + ) } // 8. Template filenames in SKILL.md's client-mapping table resolve to actual // files in assets/templates/instructions/, and every instruction template on // disk is referenced by the table. Catches renames that silently break the // Phase 7 mapping without changing the overall folder count. -const INSTRUCTION_DIR = join(SKILL_DIR, "assets", "templates", "instructions"); -const onDisk = new Set(readdirSync(INSTRUCTION_DIR).filter((name) => name.endsWith(".md"))); -const TEMPLATE_NAME = /`([A-Za-z0-9_-]+\.md)`/g; -const tableSection = skillText.match( - /Read the appropriate template from `assets\/templates\/instructions\/`:\s*\n\n\|[^\n]+\n\|[^\n]+\n([\s\S]*?)(?:\n\n|\n(?=[A-Z*#]))/, -); -const inTable = new Set(); +const INSTRUCTION_DIR = join(SKILL_DIR, "assets", "templates", "instructions") +const onDisk = new Set(readdirSync(INSTRUCTION_DIR).filter((name) => name.endsWith(".md"))) +const TEMPLATE_NAME = /`([A-Za-z0-9_-]+\.md)`/g +// Matches the Phase 7 client-mapping table from its intro sentence through the +// header and separator rows; group 1 captures the body rows up to the next blank +// line or prose line. +const CLIENT_TEMPLATE_TABLE = + /Read the appropriate template from `assets\/templates\/instructions\/`:\s*\n\n\|[^\n]+\n\|[^\n]+\n([\s\S]*?)(?:\n\n|\n(?=[A-Z*#]))/ +const tableSection = skillText.match(CLIENT_TEMPLATE_TABLE) +const inTable = new Set() + if (tableSection) { for (const row of tableSection[1].split("\n").filter((line) => line.startsWith("|"))) { // Split on | and take the Template column (index 2: empty, Client, Template, ...) - const columns = row.split("|"); - const templateCol = columns[2] ?? ""; + const templateCol = row.split("|")[2] + + if (!templateCol) continue for (const match of templateCol.matchAll(TEMPLATE_NAME)) { - inTable.add(match[1]); + inTable.add(match[1]) } } } -const onDiskNotInTable = [...onDisk].filter((name) => !inTable.has(name)).sort(); -const inTableNotOnDisk = [...inTable].filter((name) => !onDisk.has(name)).sort(); +const onDiskNotInTable = [...onDisk].filter((name) => !inTable.has(name)).toSorted() +const inTableNotOnDisk = [...inTable].filter((name) => !onDisk.has(name)).toSorted() check( "instruction template table lists all files on disk", onDiskNotInTable.length === 0, `on disk but not in table: ${onDiskNotInTable.join(", ")}`, -); +) check( "instruction template table lists no missing files", inTableNotOnDisk.length === 0, `in table but not on disk: ${inTableNotOnDisk.join(", ")}`, -); +) if (errors.length > 0) { - console.log(`\n${errors.length} check(s) failed`); - process.exit(1); + console.log(`\n${errors.length} check(s) failed`) + process.exit(1) } -console.log("\nAll structural checks passed"); +console.log("\nAll structural checks passed") diff --git a/skills/vault-onboarding/references/setup-verification.md b/skills/vault-onboarding/references/setup-verification.md index f260467..ed532e5 100644 --- a/skills/vault-onboarding/references/setup-verification.md +++ b/skills/vault-onboarding/references/setup-verification.md @@ -185,37 +185,57 @@ installation. ### Agent doesn't know my name -- Verify the instruction file exists at the expected path -- Check that it's not empty or malformed -- For paste surfaces: re-paste, checking for truncation -- For file surfaces: check file permissions and encoding (UTF-8) +1. Check that the instruction file exists at the path the client reads (see + the client's checklist above). +2. Open it and confirm it is not empty and its frontmatter or paste block is + intact. +3. On a paste surface, re-paste the block and compare its last line with the + vault note; a truncated paste drops the end. +4. On a file surface, confirm the file is readable and saved as UTF-8. +5. If the agent still does not know your name, regenerate the file from its + template through the onboarding skill and re-run the client's checklist. ### Session-start skill doesn't activate -- Verify the skill file exists at the expected path -- Check the skill's `description` frontmatter — it controls trigger matching -- Try explicit invocation: "/session-start" or ask "run the session-start skill" -- Check that the skill directory is one Claude Code scans (`~/.claude/skills/` - or `~/.agents/skills/`) +1. Check that the skill file exists in a directory the client scans (Claude + Code: `~/.claude/skills/` or `~/.agents/skills/`). +2. Read its `description` frontmatter; it controls trigger matching, so it must + name the phrases you use. +3. Invoke it explicitly: type `/session-start`, or ask "run the session-start + skill". +4. If explicit invocation also fails, run the session-start steps from the + protocol in your instruction file by hand, then reinstall the skill from its + template. ### Memory entries going to wrong file -- Read the scope callout at the top of each About Me/ file -- The callout defines routing rules — "Contains" and "Does NOT contain" -- If the remember skill is installed, it checks these rules before proposing +1. Read the scope callout at the top of each About Me/ file; its "Contains" and + "Does NOT contain" lines are the routing rules. +2. Move the misplaced entry to the file whose callout claims it, keeping its + date. +3. If the remember skill is installed, confirm it is active; it checks these + callouts before proposing an entry. +4. If entries keep landing in the wrong file, add the case that went astray to + that file's "Does NOT contain" line. ### Task board not rendering as Kanban -- Check that the Obsidian Kanban plugin is installed and enabled — see - "Obsidian App Setup" above (new vaults never have it preinstalled) -- Verify TASKS.md has `kanban-plugin: board` in its frontmatter — generated - boards keep it even before the plugin is installed; if it's missing, add it -- The board renders as a visual Kanban only in Obsidian with the plugin +1. Check that the Obsidian Kanban plugin is installed and enabled (see + "Obsidian App Setup" above; new vaults never have it preinstalled). +2. Confirm TASKS.md has `kanban-plugin: board` in its frontmatter. Generated + boards keep it even before the plugin is installed, so add it if missing. +3. Reopen the note; the board renders as a visual Kanban only in Obsidian with + the plugin enabled. +4. If it still renders as plain markdown, keep working: the file is still a + valid task list that agents and the Tasks plugin read unchanged. ### Protocol seems incomplete -- Check whether full or lightweight was selected during onboarding -- Lightweight omits: extension points, board reconciliation at session end, - memory review at session end -- To upgrade: see `references/progressive-adoption.md` → "Upgrading lightweight - to full protocol" +1. Check which protocol was selected during onboarding. Lightweight omits the + extension points, board reconciliation at session end, and the memory review + at session end. +2. To upgrade, follow "Upgrading lightweight to full protocol" in + `references/progressive-adoption.md`. +3. If the full protocol was selected and steps are still missing, compare the + instruction file with `assets/templates/protocol.md` and re-paste the + missing sections.