From 1b12d4a84e1b809140e704ee338a32e7b0aaf7be Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:16:48 -0500 Subject: [PATCH 01/18] docs(agents): refresh code standards from the vault notes Co-Authored-By: Claude Fable 5.1 --- AGENTS.md | 78 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6ee723e..67fdb04 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -86,50 +86,80 @@ tile, glow nodes) with an inverted violet-primary palette. ## 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`. ### 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. +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. -**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. - -**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; session shorthand and coined group names never ship. 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. +- Describe what the feature does, not why someone would use it. Procedure copy 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 — 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 and variable references (a `{{VARIABLE}}` table, a README option) state the knob, its effect, and its limits, with `Valid values:` and `Default:` on their own labelled lines. +- 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 install or client path: +- Every operational section serves every path, or scopes itself explicitly. +- Never reference a path the doc doesn't offer. +- Resource identifiers stay compatible across methods, or the doc says the methods are not interchangeable. +- A note that applies to all paths attaches to none of them. + +**Lockstep** — docs update in the same change that alters behavior: +- Adding a reference, template, or client means sweeping every surface that enumerates its siblings: the structure tree above, SKILL.md's pointer list, the README. +- A derived artifact (the social-preview PNG) is regenerated in the same change. +- Generator guidance holds structure only; the claims live in the canonical doc the generator reads. + +**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 lead method and section skeleton, 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 fixed fact needs none. +- 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, with no colon-hinged labels and no verbless fragments. +- Durable rationale only, never transition history, decision narrative, or operator internals. A genuine risk of retracing an abandoned error counts as 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. +- 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. +- Bun builtins where they fit (`Bun.Glob`, `Bun.spawn`); `fs/promises` where file modes or an atomic rename matter; never `Bun.$`. Boundary: committed scripts stay Node-runnable (CI runs them under Node 24's type stripping). - Standalone scripts are `.ts`, erasable syntax (no enums/namespaces). From 96d464e6b5628ac4cc114deb8bf8209c0968e35f Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:18:36 -0500 Subject: [PATCH 02/18] fix(review): scope script runtime rule to Node built-ins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The distilled TypeScript rule recommended Bun.Glob and Bun.spawn for scripts/, but CI and the render:social-preview npm script run every script under Node, and Development Guidelines call scripts/ runtime-agnostic. The vault source limits Bun builtins to local scripts. Ship-Check: pr-review · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 67fdb04..5a7f614 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -161,5 +161,5 @@ This repo is primarily markdown (SKILL.md, references, templates, README) with T - 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. -- Bun builtins where they fit (`Bun.Glob`, `Bun.spawn`); `fs/promises` where file modes or an atomic rename matter; never `Bun.$`. Boundary: committed scripts stay Node-runnable (CI runs them under Node 24's type stripping). +- File and process work uses Node built-ins (`node:fs`, `node:path`, `node:child_process`), with `fs/promises` where file modes or an atomic rename matter. Never Bun-only APIs (`Bun.Glob`, `Bun.spawn`, `Bun.$`), because CI and the `render:social-preview` npm script run these scripts under Node 24's type stripping. - Standalone scripts are `.ts`, erasable syntax (no enums/namespaces). From c7de5052e27154d5bc51ca561f2edecff746472e Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:24:29 -0500 Subject: [PATCH 03/18] style: resolve fresh-eyes pauses in AGENTS.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Name where substituted values, the Chrome pin and the skipDownload key come from; state one script runtime story (Bun locally, node in CI and the render script); complete the add-a-reference procedure with its lockstep step; and gloss the standards terms a cold reader stalled on. Ship-Check: code-quality · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 75 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5a7f614..7655698 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 one instruction file for each AI client the user works in (Claude Code, Cursor, claude.ai, and others) — 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 @@ -49,14 +49,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, replaces each 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, and CI runs the same file with `node` on Node 24, which strips the types - 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 — each one tells 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 built-in tools every client has (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 @@ -70,19 +71,26 @@ 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. +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` skips postinstall scripts, and the +`puppeteer.skipDownload` key in `package.json` stops the download for package +managers that run them, such as npm. + +**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.** +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 @@ -93,20 +101,21 @@ This repo is primarily markdown (SKILL.md, references, templates, README) with T ### 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. +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. **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. **Language:** -- Plain-first — jargon with real signal stays with a one-line gloss; plain words get no gloss; session shorthand and coined group names never ship. Test: would the reader know what it means without looking anything up? +- 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. -- Describe what the feature does, not why someone would use it. Procedure copy opens with the reader's motive ("To add a client, …"), not the operation's name. +- 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 — state the capability affirmatively. +- 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 and variable references (a `{{VARIABLE}}` table, a README option) state the knob, its effect, and its limits, with `Valid values:` and `Default:` on their own labelled lines. @@ -115,20 +124,20 @@ This repo is primarily markdown (SKILL.md, references, templates, README) with T - 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 install or client path: +**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. -- Resource identifiers stay compatible across methods, or the doc says the methods are not interchangeable. -- A note that applies to all paths attaches to none of them. +- 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: the structure tree above, SKILL.md's pointer list, the README. +- Adding a reference, template, or client means sweeping every surface that enumerates its siblings: the structure tree above, SKILL.md's **Reference files** list, the README. - A derived artifact (the social-preview PNG) is regenerated in the same change. -- Generator guidance holds structure only; the claims live in the canonical doc the generator reads. +- 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 lead method and section skeleton, diverging only where the client genuinely differs. +- 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/) @@ -141,12 +150,12 @@ This repo is primarily markdown (SKILL.md, references, templates, README) with T **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 fixed fact needs none. +- 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. **Comment content:** -- Short complete sentences, with no colon-hinged labels and no verbless fragments. -- Durable rationale only, never transition history, decision narrative, or operator internals. A genuine risk of retracing an abandoned error counts as rationale. +- 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) @@ -161,5 +170,5 @@ This repo is primarily markdown (SKILL.md, references, templates, README) with T - 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`), with `fs/promises` where file modes or an atomic rename matter. Never Bun-only APIs (`Bun.Glob`, `Bun.spawn`, `Bun.$`), because CI and the `render:social-preview` npm script run these scripts under Node 24's type stripping. -- Standalone scripts are `.ts`, erasable syntax (no enums/namespaces). +- 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 both scripts: CI runs `validate-structure.ts` with `node`, and the `render:social-preview` package script runs `render-social-preview.ts` with `node`. +- 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. From b998600245f5276fca2205d1e4511249dcb95ae2 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:36:18 -0500 Subject: [PATCH 04/18] fix: correct AGENTS.md claims about skills, installs and lockstep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Generated skills: only session-start and session-end defer to the session protocol; remember and project-role carry their own workflows. - SKILL.md tools: "every client has" Read/Write/Bash contradicted SKILL.md's own client table (claude.ai chat has no file access). - Install-time download: Bun runs puppeteer's postinstall (puppeteer is on Bun's default trusted-dependency list); the skipDownload key is what stops the download under every package manager. - Lockstep: name the real surfaces per kind (Asset templates list, asset-inventory check, Phase 0/7, per-client references) instead of the Reference files list for templates and clients. - Reference-file step 3: cover the "during " form SKILL.md uses. - Template description: mention conditional blocks; validate.yml runs on pushes to main only. Ship-Check: bug-check · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7655698..ae3f34c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,7 +38,7 @@ 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 .github/social-preview.svg + .png # Repo social card (SVG source + committed render) .github/fonts/ # Fonts embedded by the render script ``` @@ -49,15 +49,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 a template, replaces each 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. +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`) - `scripts/` holds TypeScript that runs directly under Bun or Node: `bun run validate` runs `scripts/validate-structure.ts` under Bun locally, and CI runs the same file with `node` on Node 24, which strips the types - All template files use `{{VARIABLE}}` markers for substitution -- Generated skills are triggers, not copies — each one tells 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 built-in tools every client has (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 +- 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 @@ -76,9 +76,10 @@ That `package.json` script installs the Chrome for Testing build that the `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` skips postinstall scripts, and the -`puppeteer.skipDownload` key in `package.json` stops the download for package -managers that run them, such as npm. +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 @@ -89,7 +90,7 @@ color and cyan as the accent. 1. Create `skills/vault-onboarding/references/my-reference.md` 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.** +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 @@ -131,7 +132,10 @@ This repo is primarily markdown (SKILL.md, references, templates, README) with T - 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: the structure tree above, SKILL.md's **Reference files** list, the README. +- 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. From b96957a86e74e35bb8175416f27eccbbbc787f59 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:38:00 -0500 Subject: [PATCH 05/18] fix: scope the setting-reference rule and correct the render script header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship-Check: triage · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 --- AGENTS.md | 2 +- scripts/render-social-preview.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ae3f34c..200f13a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -119,7 +119,7 @@ This repo is primarily markdown (SKILL.md, references, templates, README) with T - 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 and variable references (a `{{VARIABLE}}` table, a README option) state the knob, its effect, and its limits, with `Valid values:` and `Default:` on their own labelled lines. +- 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. diff --git a/scripts/render-social-preview.ts b/scripts/render-social-preview.ts index df1ca14..3b3efd9 100644 --- a/scripts/render-social-preview.ts +++ b/scripts/render-social-preview.ts @@ -1,11 +1,11 @@ // 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. +// Puppeteer's pinned Chrome for Testing build. The package script installs the +// browser on demand; the `puppeteer.skipDownload` key in package.json stops +// the download at install time. Embeds JetBrains Mono SemiBold (wordmark) and +// DejaVu Sans (body text) via @font-face for deterministic rendering +// regardless of host system fonts. // -// Usage: npm run render:social-preview +// Usage: bun run render:social-preview import { execFileSync } from "node:child_process" import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs" From e632ab7c6d9d44493ab6e2fe42c872121806df73 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:41:01 -0500 Subject: [PATCH 06/18] refactor(docs): state the script runtime once and trim the render header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship-Check: triage · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 --- AGENTS.md | 4 ++-- scripts/render-social-preview.ts | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 200f13a..36c907d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,7 +54,7 @@ Asset templates (`assets/`) are literal markdown files with `{{VARIABLE}}` marke ## 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`) -- `scripts/` holds TypeScript that runs directly under Bun or Node: `bun run validate` runs `scripts/validate-structure.ts` under Bun locally, and CI runs the same file with `node` on Node 24, which strips the types +- `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 — 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 @@ -174,5 +174,5 @@ This repo is primarily markdown (SKILL.md, references, templates, README) with T - 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 both scripts: CI runs `validate-structure.ts` with `node`, and the `render:social-preview` package script runs `render-social-preview.ts` with `node`. +- 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/scripts/render-social-preview.ts b/scripts/render-social-preview.ts index 3b3efd9..5876eb3 100644 --- a/scripts/render-social-preview.ts +++ b/scripts/render-social-preview.ts @@ -1,9 +1,6 @@ -// Renders .github/social-preview.svg to .github/social-preview.png using -// Puppeteer's pinned Chrome for Testing build. The package script installs the -// browser on demand; the `puppeteer.skipDownload` key in package.json stops -// the download at install time. Embeds JetBrains Mono SemiBold (wordmark) and -// DejaVu Sans (body text) via @font-face for deterministic rendering -// regardless of host system fonts. +// Renders .github/social-preview.svg to .github/social-preview.png with +// Puppeteer's pinned Chrome for Testing build. Setup and the install story: +// AGENTS.md, "Social preview card". // // Usage: bun run render:social-preview From 8e4134ccdd3416b1f2cd3401c4ac8fab447cae7c Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:41:18 -0500 Subject: [PATCH 07/18] style: write the render header pointer as a sentence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship-Check: triage · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 --- scripts/render-social-preview.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/render-social-preview.ts b/scripts/render-social-preview.ts index 5876eb3..8487936 100644 --- a/scripts/render-social-preview.ts +++ b/scripts/render-social-preview.ts @@ -1,6 +1,6 @@ // Renders .github/social-preview.svg to .github/social-preview.png with -// Puppeteer's pinned Chrome for Testing build. Setup and the install story: -// AGENTS.md, "Social preview card". +// Puppeteer's pinned Chrome for Testing build. The "Social preview card" +// section of AGENTS.md covers setup and the browser install. // // Usage: bun run render:social-preview From 25435882f09384eea5b3709c9ada8a808af11c87 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:42:47 -0500 Subject: [PATCH 08/18] docs(agents): scope the TypeScript rules to new code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship-Check: pr-monitor · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 36c907d..a0e97ba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -164,6 +164,8 @@ This repo is primarily markdown (SKILL.md, references, templates, README) with T ### TypeScript (scripts/ only) +These rules govern new code. `scripts/validate-structure.ts` predates them (semicolons, `function` declarations); bring a file into line when a change already edits it, never in a sweep of its own. + - 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. From 5ed11e3139ab6ea1a8bb5921c28288bfc4c0e5e4 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:47:22 -0500 Subject: [PATCH 09/18] docs(agents): count the per-client instruction files and list every workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship-Check: pr-monitor · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 --- AGENTS.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a0e97ba..f401105 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 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 one instruction file for each AI client the user works in (Claude Code, Cursor, claude.ai, and others) — 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`. @@ -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 PRs + pushes to main + .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 ``` From 032d6098fa3f184244c924ec23d13d13b050679b Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 17:10:26 -0500 Subject: [PATCH 10/18] docs(agents): open the regenerate procedure with the reader's motive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship-Check: pr-monitor · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index f401105..0b734ab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,7 +74,7 @@ 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`. +**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 From 38b8ee9a7c66c5bd9701466d5fa2531ec97b7e11 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 17:13:56 -0500 Subject: [PATCH 11/18] docs(agents): state that the standards govern what you write MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship-Check: pr-monitor · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 --- AGENTS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 0b734ab..04fe1e2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -103,6 +103,8 @@ color and cyan as the accent. 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: @@ -168,7 +170,7 @@ This repo is primarily markdown (SKILL.md, references, templates, README) with T ### TypeScript (scripts/ only) -These rules govern new code. `scripts/validate-structure.ts` predates them (semicolons, `function` declarations); bring a file into line when a change already edits it, never in a sweep of its own. +`scripts/validate-structure.ts` predates these rules (semicolons, `function` declarations); the authoring-guidance boundary above applies. - 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. From 5711bf636088a3f4f9d343eec01c6257dcc67174 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 17:17:14 -0500 Subject: [PATCH 12/18] style: write the render script header as complete sentences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship-Check: pr-monitor · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 --- scripts/render-social-preview.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/render-social-preview.ts b/scripts/render-social-preview.ts index 8487936..d72073c 100644 --- a/scripts/render-social-preview.ts +++ b/scripts/render-social-preview.ts @@ -1,8 +1,7 @@ -// Renders .github/social-preview.svg to .github/social-preview.png with -// Puppeteer's pinned Chrome for Testing build. The "Social preview card" -// section of AGENTS.md covers setup and the browser install. -// -// Usage: bun 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" From 3886be9fd8058b2b8218572280661b98cf49b1b7 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 17:26:39 -0500 Subject: [PATCH 13/18] docs(readme): drop the stale pull-request reference from the Status section Co-Authored-By: Claude Fable 5.1 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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** From 7550bfdd1af2f35878366353d1ad88024a98f12a Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 17:26:39 -0500 Subject: [PATCH 14/18] docs(references): write troubleshooting entries as numbered steps with a fallback Co-Authored-By: Claude Fable 5.1 --- .../references/setup-verification.md | 64 ++++++++++++------- 1 file changed, 42 insertions(+), 22 deletions(-) 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. From 1371a7b4de3d7e940673a7f8f0434c97ae790246 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 17:26:39 -0500 Subject: [PATCH 15/18] style(scripts): match the repo's TypeScript rules No semicolons, arrow constants for the two helpers, and the conditional scan iterates with entries() instead of an index loop. Output under Node 24 and Bun is byte-identical to the previous version. The AGENTS.md carve-out that named this file as predating the rules is removed. Co-Authored-By: Claude Fable 5.1 --- AGENTS.md | 2 - scripts/validate-structure.ts | 173 +++++++++++++++++----------------- 2 files changed, 87 insertions(+), 88 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 04fe1e2..a1e16ba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -170,8 +170,6 @@ These rules are authoring guidance: apply them to the prose and code you write. ### TypeScript (scripts/ only) -`scripts/validate-structure.ts` predates these rules (semicolons, `function` declarations); the authoring-guidance boundary above applies. - - 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. diff --git a/scripts/validate-structure.ts b/scripts/validate-structure.ts index d5aa77f..d27a559 100644 --- a/scripts/validate-structure.ts +++ b/scripts/validate-structure.ts @@ -10,224 +10,225 @@ * 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.sort() } -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/); +const skillText = read(join(SKILL_DIR, "SKILL.md")) +const frontmatter = skillText.match(/^---\n([\s\S]*?)\n---\n/) check( "SKILL.md frontmatter has name + description", frontmatter !== null && /^name:/m.test(frontmatter[1]) && /^description:/m.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; +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)] : [], - ); + ) 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; +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[stack.length - 1] + problem = `unclosed ${unclosed.name} opened on line ${unclosed.line}` } - check(`conditional nesting: ${rel(path)}`, problem === null, 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))))].sort() 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. const documented = new Set( [...skillText.matchAll(/^\| `\{\{([A-Z][A-Z0-9_]*)\}\}`/gm)].map((match) => match[1]), -); -const used = new Set(); +) +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]); + 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)).sort() +const documentedNotUsed = [...documented].filter((name) => !used.has(name)).sort() 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; +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`". 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 +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()); + .map((line) => line.replace(/^#{1,6}\s+/, "").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 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 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 columns = row.split("|") + const templateCol = columns[2] ?? "" 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)).sort() +const inTableNotOnDisk = [...inTable].filter((name) => !onDisk.has(name)).sort() 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") From 5efb19b7a4c591d83ab950d0d4e25c6e20f12a50 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 17:38:52 -0500 Subject: [PATCH 16/18] style(scripts): clear the remaining lint flags in validate-structure.ts toSorted() for the fresh-array sorts, stack.at(-1) for the last open block, the two `?? ""` sentinels replaced by handling the absent value at the use site, and the inline regexes lifted into doc-commented constants. Output under Node 24 and Bun is byte-identical to the previous version. Co-Authored-By: Claude Fable 5.1 --- scripts/validate-structure.ts | 69 +++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/scripts/validate-structure.ts b/scripts/validate-structure.ts index d27a559..4cedd62 100644 --- a/scripts/validate-structure.ts +++ b/scripts/validate-structure.ts @@ -31,10 +31,11 @@ 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) } - return found.sort() + return found.toSorted() } const read = (path: string): string => readFileSync(path, "utf8") @@ -44,11 +45,16 @@ const isFile = (path: string): boolean => existsSync(path) && statSync(path).isF const assetFiles = mdFilesUnder(join(SKILL_DIR, "assets")) // 1. SKILL.md frontmatter carries the Agent Skills required keys. +// A leading pair of --- fences; group 1 is the YAML between them. +const FRONTMATTER_BLOCK = /^---\n([\s\S]*?)\n---\n/ +// A `name:` or `description:` 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(/^---\n([\s\S]*?)\n---\n/) +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", ) @@ -57,9 +63,9 @@ const occurrences = (text: string, token: string): number => text.split(token).l 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, @@ -79,6 +85,7 @@ for (const path of assetFiles) { for (const match of line.matchAll(CONDITIONAL)) { const kind = match[1] const name = match[2] + if (kind === "#" || kind === "^") { stack.push({ name, line: index + 1 }) } else if (stack.pop()?.name !== name) { @@ -87,18 +94,23 @@ for (const path of assetFiles) { } } } - if (problem === null && stack.length > 0) { - const unclosed = stack[stack.length - 1] + const unclosed = stack.at(-1) + + if (problem === null && unclosed) { problem = `unclosed ${unclosed.name} opened on line ${unclosed.line}` } - check(`conditional nesting: ${rel(path)}`, problem === null, problem ?? "") + if (problem === null) { + check(`conditional nesting: ${rel(path)}`, true) + } else { + check(`conditional nesting: ${rel(path)}`, false, problem) + } } // 4. Internal path references in SKILL.md and references/ resolve on disk. 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 missing = [...new Set(refs.filter((ref) => !isFile(join(SKILL_DIR, ref))))].toSorted() check( `internal paths resolve: ${rel(path)}`, missing.length === 0, @@ -107,17 +119,21 @@ for (const path of [join(SKILL_DIR, "SKILL.md"), ...mdFilesUnder(join(SKILL_DIR, } // 5. Variable Reference table matches actual {{VAR}} usage in assets, both ways. +// A Variable Reference table row; group 1 is the documented variable name. +const DOCUMENTED_VARIABLE_ROW = /^\| `\{\{([A-Z][A-Z0-9_]*)\}\}`/gm +// A {{VARIABLE}} marker in an asset; group 1 is 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]), + [...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)) { + 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, @@ -156,6 +172,8 @@ const HEADING_XREF_B = /`(references\/[A-Za-z0-9_\-./]+\.md)`\s*→\s*"([^"]+)"/ // 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 +// 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. @@ -164,11 +182,12 @@ for (const path of xrefFiles) { 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 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}`) } @@ -200,22 +219,26 @@ for (const path of xrefFiles) { 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*#]))/, -) +// The Phase 7 client-mapping table: its intro sentence, the header and separator +// rows, then (group 1) 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]) } } } -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, From a745926298b956f322277a62dde92f18c3691edc Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 17:46:35 -0500 Subject: [PATCH 17/18] style(scripts): write the regex constant comments as sentences Co-Authored-By: Claude Fable 5.1 --- scripts/validate-structure.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/scripts/validate-structure.ts b/scripts/validate-structure.ts index 4cedd62..6030a83 100644 --- a/scripts/validate-structure.ts +++ b/scripts/validate-structure.ts @@ -45,9 +45,9 @@ const isFile = (path: string): boolean => existsSync(path) && statSync(path).isF const assetFiles = mdFilesUnder(join(SKILL_DIR, "assets")) // 1. SKILL.md frontmatter carries the Agent Skills required keys. -// A leading pair of --- fences; group 1 is the YAML between them. +// Matches the leading pair of --- fences; group 1 captures the YAML between them. const FRONTMATTER_BLOCK = /^---\n([\s\S]*?)\n---\n/ -// A `name:` or `description:` key at the start of a frontmatter line. +// 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")) @@ -119,9 +119,9 @@ for (const path of [join(SKILL_DIR, "SKILL.md"), ...mdFilesUnder(join(SKILL_DIR, } // 5. Variable Reference table matches actual {{VAR}} usage in assets, both ways. -// A Variable Reference table row; group 1 is the documented variable name. +// Matches a Variable Reference table row; group 1 captures the documented name. const DOCUMENTED_VARIABLE_ROW = /^\| `\{\{([A-Z][A-Z0-9_]*)\}\}`/gm -// A {{VARIABLE}} marker in an asset; group 1 is the name (conditionals included). +// 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(DOCUMENTED_VARIABLE_ROW)].map((match) => match[1]), @@ -168,11 +168,11 @@ for (const single of [ // 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`". +// 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 -// A markdown heading's leading hashes and the whitespace after them. +// 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) { @@ -219,8 +219,9 @@ for (const path of xrefFiles) { 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 -// The Phase 7 client-mapping table: its intro sentence, the header and separator -// rows, then (group 1) the body rows up to the next blank line or prose line. +// 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) From fe3eec06ddaa5e2b3346cf8ac41275db91f81212 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 18:00:23 -0500 Subject: [PATCH 18/18] style(scripts): write the numbered check comments as sentences Co-Authored-By: Claude Fable 5.1 --- scripts/validate-structure.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/validate-structure.ts b/scripts/validate-structure.ts index 6030a83..b87478d 100644 --- a/scripts/validate-structure.ts +++ b/scripts/validate-structure.ts @@ -58,7 +58,7 @@ check( "missing frontmatter block or required keys", ) -// 2. Marker well-formedness: every line's {{ and }} counts agree. +// 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) @@ -73,8 +73,9 @@ for (const path of [...assetFiles, join(SKILL_DIR, "SKILL.md")]) { ) } -// 3. Conditional nesting: {{#IF_X}} / {{^IF_X}} open, {{/IF_X}} closes the -// innermost open block of the same name (block and inline forms alike). +// 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 }> = [] @@ -163,9 +164,9 @@ for (const single of [ 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. +// 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