Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .cratis/ai.manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"SourceRevision": "e7dc6fea47fe743e620d015076855ea2087b0a4e",
"SourceRevision": "cc847418c99ad8ba62d6372cace683d8d4a47db9",
"Files": [
{
"Source": "agents/backend-developer.md",
Expand Down Expand Up @@ -189,7 +189,7 @@
{
"Source": "harnesses/pi/extensions/cratis-path-guidance/SkillTrigger.ts",
"Destination": "harnesses/pi/extensions/cratis-path-guidance/SkillTrigger.ts",
"Hash": "F5F1D27B86BBEA315E107633B64C217E070C6448671C40B9450C4FC72E2D13C1"
"Hash": "9DD331F5C989F8B150D56883D35236CE946082D149700E9C7D34FE2894EF41EA"
},
{
"Source": "harnesses/pi/extensions/cratis-path-guidance/ToolName.ts",
Expand All @@ -199,7 +199,7 @@
{
"Source": "harnesses/pi/extensions/cratis-path-guidance/index.ts",
"Destination": "harnesses/pi/extensions/cratis-path-guidance/index.ts",
"Hash": "0465709FABDC841B4C0B01ABEB652F29DD3ECDA3F9C6266A324B7148321ACFBB"
"Hash": "84E0FAD5F229BDE941DADC430F26937C60655D1742B79380631BBC8C04D0342B"
},
{
"Source": "harnesses/pi/extensions/cratis-path-guidance/skillReads.ts",
Expand All @@ -209,7 +209,7 @@
{
"Source": "harnesses/pi/extensions/cratis-path-guidance/skills.ts",
"Destination": "harnesses/pi/extensions/cratis-path-guidance/skills.ts",
"Hash": "3B436920AEA9BFE57ACF9B17361ED45F61D75447367B7BF5B1F561E0A74D7A67"
"Hash": "51EA0F9EB2EA2215C1244714BF17AC584299A1B425498668839583A9833F4013"
},
{
"Source": "harnesses/pi/extensions/cratis-path-guidance/standDown.ts",
Expand Down Expand Up @@ -249,7 +249,7 @@
{
"Source": "harnesses/pi/extensions/shared/frontmatter.ts",
"Destination": "harnesses/pi/extensions/shared/frontmatter.ts",
"Hash": "D1B96A98A8100A6CF8690F0140F5838C89D9EF990F1DD110CC05389EF203EBE8"
"Hash": "DF8063CC4EBA0F6D29181361AD0AB06A905DD9D308BBEC29C352292050296AE3"
},
{
"Source": "harnesses/pi/extensions/shared/globs.ts",
Expand All @@ -264,7 +264,7 @@
{
"Source": "harnesses/pi/extensions/shared/skillFrontmatter.ts",
"Destination": "harnesses/pi/extensions/shared/skillFrontmatter.ts",
"Hash": "31913D4368A0DA79E212CD896E12D712662D8F733141F155F88A69B04A5F90E8"
"Hash": "7DE5E9B9BEF0F3B64E23F016E87DE67232711FFD629D9FFA574A723AABCEAE60"
},
{
"Source": "harnesses/pi/extensions/shared/skillSelection.ts",
Expand Down Expand Up @@ -644,7 +644,7 @@
{
"Source": "skills/cratis-application-react-specifications/SKILL.md",
"Destination": "skills/cratis-application-react-specifications/SKILL.md",
"Hash": "98601657B9BF860ECA85896DD373824832B86E268C8553F3466DCDDED2B564C5"
"Hash": "506D463AA3AE19506BCEE57BF8F58CEDC3153C829E6B2FEEA9A69E65256946AB"
},
{
"Source": "skills/cratis-arc-authentication-authorization-and-identity/LICENSE",
Expand Down Expand Up @@ -914,7 +914,7 @@
{
"Source": "skills/cratis-documentation-writing/SKILL.md",
"Destination": "skills/cratis-documentation-writing/SKILL.md",
"Hash": "665A62930F7251C724EC71788AB48091478FA9C0AC2F15CF5EC7E398D0CEB8B1"
"Hash": "E22BF1E4450C497D7A9B630ADA18B9932866A9E1577F904D909398032A1C4BE0"
},
{
"Source": "skills/cratis-documentation-writing/references/cratis-site.md",
Expand Down Expand Up @@ -944,7 +944,7 @@
{
"Source": "skills/cratis-engineering-docs-authoring/SKILL.md",
"Destination": "skills/cratis-engineering-docs-authoring/SKILL.md",
"Hash": "4E49F04C9695BACC082E9D3B2C1CB81826D80A0FD0CB2C5DD02B071C36E7EFE2"
"Hash": "BCF09E3312A713D53317975CD39BA44E4B4A2FD8659C8C2953BA3907D8AB7B4E"
},
{
"Source": "skills/cratis-engineering-docs-authoring/references/site-format.md",
Expand Down Expand Up @@ -1054,7 +1054,7 @@
{
"Source": "skills/cratis-specifications-typescript/SKILL.md",
"Destination": "skills/cratis-specifications-typescript/SKILL.md",
"Hash": "A8D99CDE8171BE5D5B4FB319189637F127E3F75CFE7FBA1B55825C6E7B97688A"
"Hash": "0CFF3CBA9FD0FA7B6CB347BCA99F912D375F1D7E34F61FA8C4B70EB6B59637E3"
},
{
"Source": "skills/cratis-specifications-typescript/references/typescript-patterns.md",
Expand All @@ -1069,7 +1069,7 @@
{
"Source": "skills/cratis-technical-examples/SKILL.md",
"Destination": "skills/cratis-technical-examples/SKILL.md",
"Hash": "D8E9720F9D9E867A9341B6A04D35AF6C90D587BBD14C971F674FEC1BAD392732"
"Hash": "CCBF45F5C7FBE89A18FBA1B7CF94F1EF287F31D35EE9D4802FED587667585A22"
},
{
"Source": "skills/cratis-writing-voice-and-cadence/LICENSE",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ export interface SkillTrigger {
filePath: string;
/** Directory holding `SKILL.md` and the skill's references. */
baseDir: string;
/** The `cratis-hint-paths` frontmatter of the skill. Empty when the skill declares no trigger. */
/** The `metadata.cratis-hint-paths` frontmatter of the skill. Empty when the skill declares no trigger. */
globs: string[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function expandedSkillNames(prompt: unknown): string[] {
*
* Path-scoped rules are attached to the tool result the first time a matching file is touched in the session,
* exactly as they were when `cratis-rules` delivered them. On a successful `write` or `edit`, one advisory line
* names every skill whose `cratis-hint-paths` frontmatter matches the file and where its `SKILL.md` is. A skill
* names every skill whose `metadata.cratis-hint-paths` frontmatter matches the file and where its `SKILL.md` is. A skill
* is not hinted when it is already in context: read in the session, preloaded by pi-subagents
* (`# Preloaded Skill: <name>` followed by its text in the system prompt), or expanded by `/skill:<name>`. Hints are advisory only:
* nothing is blocked and the system prompt is never touched.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { corpusRoot } from '../shared/corpusRoot.ts';
import { frontmatter } from '../shared/frontmatter.ts';
import { globToRegExp } from '../shared/globs.ts';
import { skillTriggerKey } from '../shared/skillFrontmatter.ts';
import { skillTriggerGlobs } from '../shared/skillFrontmatter.ts';
import { selectedSkillNames } from '../shared/skillSelection.ts';
import type { LoadedSkill } from './LoadedSkill.ts';
import type { SkillMatch } from './SkillMatch.ts';
Expand Down Expand Up @@ -36,7 +35,7 @@ function repositorySkills(cwd: string): LoadedSkill[] {

function triggerGlobs(filePath: string): string[] {
try {
return frontmatter(readFileSync(filePath, 'utf8')).get(skillTriggerKey) ?? [];
return skillTriggerGlobs(readFileSync(filePath, 'utf8'));
} catch {
return [];
}
Expand All @@ -49,7 +48,7 @@ function triggerGlobs(filePath: string): string[] {
* it can be empty (a pi-subagents agent with `skills: false`, the usual setup for cheap workers), or non-empty
* with only personal skills (an agent with `skills: true` whose `extensions:` allowlist leaves `@cratis/pi`, and
* so its skill paths, out). The selected skills' `SKILL.md` can still be read by path, which is what the hint asks
* for; a corpus skill the repository did not select is never added. Skills without a `cratis-hint-paths` trigger
* for; a corpus skill the repository did not select is never added. Skills without a `metadata.cratis-hint-paths` trigger
* are left out.
*/
export function skillTriggers(loaded: LoadedSkill[] | undefined, cwd: string): SkillTrigger[] {
Expand Down
104 changes: 100 additions & 4 deletions .cratis/ai/harnesses/pi/extensions/shared/frontmatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,23 @@ export function unquote(value: string): string {
return value.trim().replace(/^["']|["']$/g, '');
}

/** The text between the opening and closing `---` markers, or `undefined` when there is no terminated frontmatter. */
export function frontmatterText(content: string): string | undefined {
if (!content.startsWith('---\n')) return undefined;
const end = content.indexOf('\n---\n', 4);
return end < 0 ? undefined : content.slice(4, end);
}

/**
* Reads the YAML-ish frontmatter the corpus uses: scalar `key: value` lines and `key:` followed by
* ` - item` lines. Anything richer is not used by the rules and skills and is deliberately not supported.
*/
export function frontmatter(content: string): Map<string, string[]> {
const fields = new Map<string, string[]>();
if (!content.startsWith('---\n')) return fields;
const end = content.indexOf('\n---\n', 4);
if (end < 0) return fields;
const text = frontmatterText(content);
if (text === undefined) return fields;
let current: string | undefined;
for (const line of content.slice(4, end).split('\n')) {
for (const line of text.split('\n')) {
const item = /^\s+-\s+(.*)$/.exec(line);
if (item && current) {
fields.get(current)!.push(unquote(item[1]));
Expand All @@ -30,3 +36,93 @@ export function frontmatter(content: string): Map<string, string[]> {
}
return fields;
}

/** One `key: value` line under a top-level block map such as `metadata`, as written, not interpreted. */
export interface FrontmatterEntry {
/** The number of spaces the key is indented by, or -1 when the indentation contains a tab. */
indent: number;
/** The text after the colon, trimmed and otherwise untouched: quotes, a trailing comment and block scalar indicators included. */
raw: string;
/** True when more-indented lines follow the entry, so its value is a block scalar, a multi-line scalar or a nested map. */
continues: boolean;
}

/** The result of reading the block map under a top-level key. */
export interface FrontmatterBlock {
/** The text after `name:` on the key's own line, trimmed. Non-empty for an inline value such as a flow map. */
inline: string;
/** The entries the block holds, in order. A more-indented line belongs to the entry above it and is not an entry itself. */
entries: Map<string, FrontmatterEntry>;
}

/**
* Reads the block under a top-level `name:` key without interpreting the values. Blank and comment lines are skipped.
* Returns `undefined` when the key is absent or the frontmatter is unterminated.
*/
export function frontmatterBlock(content: string, name: string): FrontmatterBlock | undefined {
const text = frontmatterText(content);
if (text === undefined) return undefined;
let block: FrontmatterBlock | undefined;
let inside = false;
let last: FrontmatterEntry | undefined;
let skipAbove: number | undefined;
for (const line of text.split('\n')) {
// Any line that starts in column 0 with something other than a comment ends the current block, whatever its
// spelling (a quoted key, a space before the colon). Only a real `name:` key starts this block.
if (/^[^\s#]/.test(line)) {
const top = /^([A-Za-z][\w-]*):(.*)$/.exec(line);
inside = top?.[1] === name;
last = undefined;
skipAbove = undefined;
// A comment after the key (`metadata: # note`) is not a value, so the block below it is still the block form.
if (inside) block ??= { inline: /^\s+#/.test(top![2]) ? '' : top![2].trim(), entries: new Map() };
continue;
}
if (!inside || !block || line.trim() === '' || /^\s*#/.test(line)) continue;
const indent = /^ *\t/.test(line) ? -1 : /^( *)/.exec(line)![1].length;
// Lines under a sibling key the entry pattern does not read (a quoted key, `? complex`) are not entries.
if (skipAbove !== undefined && indent > skipAbove) continue;
skipAbove = undefined;
const entry = /^\s+([A-Za-z][\w-]*):(.*)$/.exec(line);
if (entry && (!last || indent <= last.indent)) {
last = { indent, raw: entry[2].trim(), continues: false };
block.entries.set(entry[1], last);
} else if (last && (indent < 0 || indent > last.indent)) {
last.continues = true;
} else {
// At the entry's indentation or less, so a sibling key ends the entry even when its spelling is not read here.
last = undefined;
skipAbove = indent;
}
}
return block;
}

/**
* The string a raw scalar spells when it is written as one complete double-quoted or single-quoted string on one
* line, with nothing after the closing quote. Anything else, including a string with an escape or an embedded
* quote, a trailing `# comment`, an unquoted value and a block scalar, yields `undefined`: it is not guessed at.
*/
export function quotedScalar(raw: string): string | undefined {
return /^"([^"\\]*)"$/.exec(raw)?.[1] ?? /^'([^']*)'$/.exec(raw)?.[1];
}

/**
* Reads the nested string map under a top-level `name:` key, as Agent Skills `metadata` uses. Only the one form the
* corpus supports is read: an entry indented two spaces whose value is a complete one-line quoted string (see
* `quotedScalar`). Values stay whole strings, without comma splitting. An entry in any other form is left out of the
* map, so a caller never sees a guess such as `>-`; `frontmatterBlock` gives the raw entries for reporting them.
* Returns `undefined` when the key is absent or the frontmatter is unterminated.
*/
export function frontmatterMap(content: string, name: string): Map<string, string> | undefined {
const block = frontmatterBlock(content, name);
if (!block) return undefined;
const map = new Map<string, string>();
// A value on the key's own line (a flow map, even one that continues below) is not the supported block form.
if (block.inline !== '') return map;
for (const [key, entry] of block.entries) {
const value = entry.indent === 2 && !entry.continues ? quotedScalar(entry.raw) : undefined;
if (value !== undefined) map.set(key, value);
}
return map;
}
32 changes: 27 additions & 5 deletions .cratis/ai/harnesses/pi/extensions/shared/skillFrontmatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,31 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

/**
* The `SKILL.md` frontmatter key that lists the globs a skill is hinted for. It is deliberately Cratis-specific:
* Claude Code gives a plain `paths` key in `SKILL.md` a meaning of its own (a "conditional skill" that stays out
* of the skill list until a matching file is touched), and `.claude/skills` exposes this corpus to it.
*/
import { frontmatterMap } from './frontmatter.ts';

// The key, under the `metadata` map of `SKILL.md` frontmatter, whose value lists the globs a skill is hinted for.
// Agent Skills allows only `name`, `description`, `license`, `compatibility`, `metadata` and `allowed-tools` at the
// top level, and `metadata` maps strings to strings, so the value is one string of whitespace-separated globs:
//
// metadata:
// cratis-hint-paths: "**/for_*/**/*.cs **/Documentation/**/*.{md,mdx}"
//
// It is deliberately Cratis-specific: Claude Code gives a plain `paths` key in `SKILL.md` a meaning of its own (a
// "conditional skill" that stays out of the skill list until a matching file is touched), and `.claude/skills`
// exposes this corpus to it.
export const skillTriggerKey = 'cratis-hint-paths';

/** The raw `metadata.cratis-hint-paths` string of a `SKILL.md`, or `undefined` when the skill declares none. */
export function skillTriggerValue(content: string): string | undefined {
return frontmatterMap(content, 'metadata')?.get(skillTriggerKey);
}

/** Splits a trigger value into its globs. Globs contain no whitespace, and brace sets keep their commas. */
export function splitSkillTriggerGlobs(value: string): string[] {
return value.split(/\s+/).filter(Boolean);
}

/** The trigger globs a `SKILL.md` declares under `metadata.cratis-hint-paths`; empty when it declares none. */
export function skillTriggerGlobs(content: string): string[] {
return splitSkillTriggerGlobs(skillTriggerValue(content) ?? '');
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
name: cratis-application-react-specifications
description: Write specifications for the React and TypeScript surface of a Cratis application slice — view models, helpers, command orchestration, and narrow component behavior — using Vitest with Mocha-style describe/it, Sinon, and the Chai should interface. Use when adding or changing frontend behavior in an application that consumes Cratis. Do not use for backend scenarios or for specifications inside a Cratis framework package.
license: MIT
cratis-hint-paths:
- "**/for_*/**/*.ts"
- "**/for_*/**/*.tsx"
metadata:
cratis-hint-paths: "**/for_*/**/*.ts **/for_*/**/*.tsx"
---
<!-- cratis-ai-managed: skills/cratis-application-react-specifications/SKILL.md -->

Expand Down
4 changes: 2 additions & 2 deletions .cratis/ai/skills/cratis-documentation-writing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
name: cratis-documentation-writing
description: Plan, write, and improve user-centered Cratis documentation with a clear reader journey and one primary Diátaxis purpose per page. Use for product docs, tutorials, how-to guides, reference, explanations, and documentation reviews. For executable examples use cratis-technical-examples; for release notes use cratis-release-notes. Do not invent APIs or publish content.
license: MIT
cratis-hint-paths:
- "**/Documentation/**/*.{md,mdx}"
metadata:
cratis-hint-paths: "**/Documentation/**/*.{md,mdx}"
---
<!-- cratis-ai-managed: skills/cratis-documentation-writing/SKILL.md -->

Expand Down
4 changes: 2 additions & 2 deletions .cratis/ai/skills/cratis-engineering-docs-authoring/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
name: cratis-engineering-docs-authoring
description: Draft accurate Cratis product or engineering documentation once the reader's goal, owning source, and product evidence are known. Use for tutorials, how-to guides, explanations, and references; use the owning repository's navigation and visual QA workflows for placement and rendering.
license: LICENSE
cratis-hint-paths:
- "**/Documentation/**/*.{md,mdx}"
metadata:
cratis-hint-paths: "**/Documentation/**/*.{md,mdx}"
---
<!-- cratis-ai-managed: skills/cratis-engineering-docs-authoring/SKILL.md -->

Expand Down
5 changes: 2 additions & 3 deletions .cratis/ai/skills/cratis-specifications-typescript/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
name: cratis-specifications-typescript
description: Write TypeScript specifications in the Cratis BDD style using the given() helper, reusable context classes, Sinon stubbing, and the Chai .should fluent interface. Use when adding or restructuring TypeScript specifications, building a given/ context class, or laying out a for_/when_ specification folder. Do not use for C# specifications, and do not use it to decide what the code under specification should do.
license: MIT
cratis-hint-paths:
- "**/for_*/**/*.ts"
- "**/for_*/**/*.tsx"
metadata:
cratis-hint-paths: "**/for_*/**/*.ts **/for_*/**/*.tsx"
---
<!-- cratis-ai-managed: skills/cratis-specifications-typescript/SKILL.md -->

Expand Down
4 changes: 2 additions & 2 deletions .cratis/ai/skills/cratis-technical-examples/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
name: cratis-technical-examples
description: Design and verify developer-facing code samples, tutorial projects, and documentation snippets against real Cratis APIs. Use when adding or reviewing a runnable example, multi-client snippet, sample app, command/output pair, or migration before/after code. Do not invent API shapes or treat rendering as a compilation check.
license: MIT
cratis-hint-paths:
- "**/Samples/**/*.{cs,ts,tsx}"
metadata:
cratis-hint-paths: "**/Samples/**/*.{cs,ts,tsx}"
---
<!-- cratis-ai-managed: skills/cratis-technical-examples/SKILL.md -->

Expand Down
Loading