@cratis/pi 7.2.1 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +4 -2
  2. package/package/corpus/harnesses/pi/extensions/cratis-path-guidance/SkillTrigger.ts +1 -1
  3. package/package/corpus/harnesses/pi/extensions/cratis-path-guidance/index.ts +1 -1
  4. package/package/corpus/harnesses/pi/extensions/cratis-path-guidance/skills.ts +3 -4
  5. package/package/corpus/harnesses/pi/extensions/shared/frontmatter.ts +100 -4
  6. package/package/corpus/harnesses/pi/extensions/shared/skillFrontmatter.ts +27 -5
  7. package/package/corpus/rules/profiles.md +23 -0
  8. package/package/corpus/skills/cratis-application-react-specifications/SKILL.md +2 -3
  9. package/package/corpus/skills/cratis-application-slice-specifications/SKILL.md +2 -3
  10. package/package/corpus/skills/cratis-arc-command-typescript/LICENSE +2 -0
  11. package/package/corpus/skills/cratis-arc-command-typescript/SKILL.md +495 -0
  12. package/package/corpus/skills/cratis-arc-command-typescript/references/chronicle-events.md +141 -0
  13. package/package/corpus/skills/cratis-arc-command-typescript/references/hosting-and-storage.md +183 -0
  14. package/package/corpus/skills/cratis-arc-command-typescript/verification.json +15 -0
  15. package/package/corpus/skills/cratis-arc-query-typescript/LICENSE +2 -0
  16. package/package/corpus/skills/cratis-arc-query-typescript/SKILL.md +350 -0
  17. package/package/corpus/skills/cratis-arc-query-typescript/verification.json +14 -0
  18. package/package/corpus/skills/cratis-arc-validation-typescript/LICENSE +2 -0
  19. package/package/corpus/skills/cratis-arc-validation-typescript/SKILL.md +320 -0
  20. package/package/corpus/skills/cratis-arc-validation-typescript/verification.json +14 -0
  21. package/package/corpus/skills/cratis-documentation-writing/SKILL.md +2 -2
  22. package/package/corpus/skills/cratis-engineering-docs-authoring/SKILL.md +2 -2
  23. package/package/corpus/skills/cratis-specifications-csharp/SKILL.md +2 -2
  24. package/package/corpus/skills/cratis-specifications-typescript/SKILL.md +2 -3
  25. package/package/corpus/skills/cratis-technical-examples/SKILL.md +2 -2
  26. package/package/profile-catalog.json +24 -0
  27. package/package.json +1 -1
package/README.md CHANGED
@@ -34,8 +34,10 @@ managed corpus.
34
34
  The packaged `cratis-path-guidance` extension attaches a path-scoped rule to the
35
35
  tool result the first time a matching file is touched, and after a successful
36
36
  `write` or `edit` adds one advisory line naming every skill whose `SKILL.md`
37
- `cratis-hint-paths` frontmatter matches the file. (The key is Cratis-specific
38
- because Claude Code gives a plain `paths` key its own meaning.) A skill is hinted at
37
+ `metadata.cratis-hint-paths` frontmatter (one string of whitespace-separated globs)
38
+ matches the file. (It sits under `metadata` because Agent Skills allows only a fixed set
39
+ of top-level keys, and the key is Cratis-specific because Claude Code gives a plain
40
+ `paths` key its own meaning.) A skill is hinted at
39
41
  most once per session and not when it is already in context: read (a shell command
40
42
  counts only when `cat`, `sed`, `head`, `tail`, `less`, `bat`, `rg` or `grep` opens
41
43
  its `SKILL.md` or a file under `references/`), preloaded by pi-subagents, or
@@ -8,6 +8,6 @@ export interface SkillTrigger {
8
8
  filePath: string;
9
9
  /** Directory holding `SKILL.md` and the skill's references. */
10
10
  baseDir: string;
11
- /** The `cratis-hint-paths` frontmatter of the skill. Empty when the skill declares no trigger. */
11
+ /** The `metadata.cratis-hint-paths` frontmatter of the skill. Empty when the skill declares no trigger. */
12
12
  globs: string[];
13
13
  }
@@ -66,7 +66,7 @@ function expandedSkillNames(prompt: unknown): string[] {
66
66
  *
67
67
  * Path-scoped rules are attached to the tool result the first time a matching file is touched in the session,
68
68
  * exactly as they were when `cratis-rules` delivered them. On a successful `write` or `edit`, one advisory line
69
- * names every skill whose `cratis-hint-paths` frontmatter matches the file and where its `SKILL.md` is. A skill
69
+ * names every skill whose `metadata.cratis-hint-paths` frontmatter matches the file and where its `SKILL.md` is. A skill
70
70
  * is not hinted when it is already in context: read in the session, preloaded by pi-subagents
71
71
  * (`# Preloaded Skill: <name>` followed by its text in the system prompt), or expanded by `/skill:<name>`. Hints are advisory only:
72
72
  * nothing is blocked and the system prompt is never touched.
@@ -4,9 +4,8 @@
4
4
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
5
5
  import { dirname, join } from 'node:path';
6
6
  import { corpusRoot } from '../shared/corpusRoot.ts';
7
- import { frontmatter } from '../shared/frontmatter.ts';
8
7
  import { globToRegExp } from '../shared/globs.ts';
9
- import { skillTriggerKey } from '../shared/skillFrontmatter.ts';
8
+ import { skillTriggerGlobs } from '../shared/skillFrontmatter.ts';
10
9
  import { selectedSkillNames } from '../shared/skillSelection.ts';
11
10
  import type { LoadedSkill } from './LoadedSkill.ts';
12
11
  import type { SkillMatch } from './SkillMatch.ts';
@@ -35,7 +34,7 @@ function repositorySkills(cwd: string): LoadedSkill[] {
35
34
 
36
35
  function triggerGlobs(filePath: string): string[] {
37
36
  try {
38
- return frontmatter(readFileSync(filePath, 'utf8')).get(skillTriggerKey) ?? [];
37
+ return skillTriggerGlobs(readFileSync(filePath, 'utf8'));
39
38
  } catch {
40
39
  return [];
41
40
  }
@@ -48,7 +47,7 @@ function triggerGlobs(filePath: string): string[] {
48
47
  * it can be empty (a pi-subagents agent with `skills: false`, the usual setup for cheap workers), or non-empty
49
48
  * with only personal skills (an agent with `skills: true` whose `extensions:` allowlist leaves `@cratis/pi`, and
50
49
  * so its skill paths, out). The selected skills' `SKILL.md` can still be read by path, which is what the hint asks
51
- * for; a corpus skill the repository did not select is never added. Skills without a `cratis-hint-paths` trigger
50
+ * for; a corpus skill the repository did not select is never added. Skills without a `metadata.cratis-hint-paths` trigger
52
51
  * are left out.
53
52
  */
54
53
  export function skillTriggers(loaded: LoadedSkill[] | undefined, cwd: string): SkillTrigger[] {
@@ -5,17 +5,23 @@ export function unquote(value: string): string {
5
5
  return value.trim().replace(/^["']|["']$/g, '');
6
6
  }
7
7
 
8
+ /** The text between the opening and closing `---` markers, or `undefined` when there is no terminated frontmatter. */
9
+ export function frontmatterText(content: string): string | undefined {
10
+ if (!content.startsWith('---\n')) return undefined;
11
+ const end = content.indexOf('\n---\n', 4);
12
+ return end < 0 ? undefined : content.slice(4, end);
13
+ }
14
+
8
15
  /**
9
16
  * Reads the YAML-ish frontmatter the corpus uses: scalar `key: value` lines and `key:` followed by
10
17
  * ` - item` lines. Anything richer is not used by the rules and skills and is deliberately not supported.
11
18
  */
12
19
  export function frontmatter(content: string): Map<string, string[]> {
13
20
  const fields = new Map<string, string[]>();
14
- if (!content.startsWith('---\n')) return fields;
15
- const end = content.indexOf('\n---\n', 4);
16
- if (end < 0) return fields;
21
+ const text = frontmatterText(content);
22
+ if (text === undefined) return fields;
17
23
  let current: string | undefined;
18
- for (const line of content.slice(4, end).split('\n')) {
24
+ for (const line of text.split('\n')) {
19
25
  const item = /^\s+-\s+(.*)$/.exec(line);
20
26
  if (item && current) {
21
27
  fields.get(current)!.push(unquote(item[1]));
@@ -29,3 +35,93 @@ export function frontmatter(content: string): Map<string, string[]> {
29
35
  }
30
36
  return fields;
31
37
  }
38
+
39
+ /** One `key: value` line under a top-level block map such as `metadata`, as written, not interpreted. */
40
+ export interface FrontmatterEntry {
41
+ /** The number of spaces the key is indented by, or -1 when the indentation contains a tab. */
42
+ indent: number;
43
+ /** The text after the colon, trimmed and otherwise untouched: quotes, a trailing comment and block scalar indicators included. */
44
+ raw: string;
45
+ /** True when more-indented lines follow the entry, so its value is a block scalar, a multi-line scalar or a nested map. */
46
+ continues: boolean;
47
+ }
48
+
49
+ /** The result of reading the block map under a top-level key. */
50
+ export interface FrontmatterBlock {
51
+ /** The text after `name:` on the key's own line, trimmed. Non-empty for an inline value such as a flow map. */
52
+ inline: string;
53
+ /** The entries the block holds, in order. A more-indented line belongs to the entry above it and is not an entry itself. */
54
+ entries: Map<string, FrontmatterEntry>;
55
+ }
56
+
57
+ /**
58
+ * Reads the block under a top-level `name:` key without interpreting the values. Blank and comment lines are skipped.
59
+ * Returns `undefined` when the key is absent or the frontmatter is unterminated.
60
+ */
61
+ export function frontmatterBlock(content: string, name: string): FrontmatterBlock | undefined {
62
+ const text = frontmatterText(content);
63
+ if (text === undefined) return undefined;
64
+ let block: FrontmatterBlock | undefined;
65
+ let inside = false;
66
+ let last: FrontmatterEntry | undefined;
67
+ let skipAbove: number | undefined;
68
+ for (const line of text.split('\n')) {
69
+ // Any line that starts in column 0 with something other than a comment ends the current block, whatever its
70
+ // spelling (a quoted key, a space before the colon). Only a real `name:` key starts this block.
71
+ if (/^[^\s#]/.test(line)) {
72
+ const top = /^([A-Za-z][\w-]*):(.*)$/.exec(line);
73
+ inside = top?.[1] === name;
74
+ last = undefined;
75
+ skipAbove = undefined;
76
+ // A comment after the key (`metadata: # note`) is not a value, so the block below it is still the block form.
77
+ if (inside) block ??= { inline: /^\s+#/.test(top![2]) ? '' : top![2].trim(), entries: new Map() };
78
+ continue;
79
+ }
80
+ if (!inside || !block || line.trim() === '' || /^\s*#/.test(line)) continue;
81
+ const indent = /^ *\t/.test(line) ? -1 : /^( *)/.exec(line)![1].length;
82
+ // Lines under a sibling key the entry pattern does not read (a quoted key, `? complex`) are not entries.
83
+ if (skipAbove !== undefined && indent > skipAbove) continue;
84
+ skipAbove = undefined;
85
+ const entry = /^\s+([A-Za-z][\w-]*):(.*)$/.exec(line);
86
+ if (entry && (!last || indent <= last.indent)) {
87
+ last = { indent, raw: entry[2].trim(), continues: false };
88
+ block.entries.set(entry[1], last);
89
+ } else if (last && (indent < 0 || indent > last.indent)) {
90
+ last.continues = true;
91
+ } else {
92
+ // At the entry's indentation or less, so a sibling key ends the entry even when its spelling is not read here.
93
+ last = undefined;
94
+ skipAbove = indent;
95
+ }
96
+ }
97
+ return block;
98
+ }
99
+
100
+ /**
101
+ * The string a raw scalar spells when it is written as one complete double-quoted or single-quoted string on one
102
+ * line, with nothing after the closing quote. Anything else, including a string with an escape or an embedded
103
+ * quote, a trailing `# comment`, an unquoted value and a block scalar, yields `undefined`: it is not guessed at.
104
+ */
105
+ export function quotedScalar(raw: string): string | undefined {
106
+ return /^"([^"\\]*)"$/.exec(raw)?.[1] ?? /^'([^']*)'$/.exec(raw)?.[1];
107
+ }
108
+
109
+ /**
110
+ * Reads the nested string map under a top-level `name:` key, as Agent Skills `metadata` uses. Only the one form the
111
+ * corpus supports is read: an entry indented two spaces whose value is a complete one-line quoted string (see
112
+ * `quotedScalar`). Values stay whole strings, without comma splitting. An entry in any other form is left out of the
113
+ * map, so a caller never sees a guess such as `>-`; `frontmatterBlock` gives the raw entries for reporting them.
114
+ * Returns `undefined` when the key is absent or the frontmatter is unterminated.
115
+ */
116
+ export function frontmatterMap(content: string, name: string): Map<string, string> | undefined {
117
+ const block = frontmatterBlock(content, name);
118
+ if (!block) return undefined;
119
+ const map = new Map<string, string>();
120
+ // A value on the key's own line (a flow map, even one that continues below) is not the supported block form.
121
+ if (block.inline !== '') return map;
122
+ for (const [key, entry] of block.entries) {
123
+ const value = entry.indent === 2 && !entry.continues ? quotedScalar(entry.raw) : undefined;
124
+ if (value !== undefined) map.set(key, value);
125
+ }
126
+ return map;
127
+ }
@@ -1,9 +1,31 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
- /**
5
- * The `SKILL.md` frontmatter key that lists the globs a skill is hinted for. It is deliberately Cratis-specific:
6
- * Claude Code gives a plain `paths` key in `SKILL.md` a meaning of its own (a "conditional skill" that stays out
7
- * of the skill list until a matching file is touched), and `.claude/skills` exposes this corpus to it.
8
- */
4
+ import { frontmatterMap } from './frontmatter.ts';
5
+
6
+ // The key, under the `metadata` map of `SKILL.md` frontmatter, whose value lists the globs a skill is hinted for.
7
+ // Agent Skills allows only `name`, `description`, `license`, `compatibility`, `metadata` and `allowed-tools` at the
8
+ // top level, and `metadata` maps strings to strings, so the value is one string of whitespace-separated globs:
9
+ //
10
+ // metadata:
11
+ // cratis-hint-paths: "**/for_*/**/*.cs **/Documentation/**/*.{md,mdx}"
12
+ //
13
+ // It is deliberately Cratis-specific: Claude Code gives a plain `paths` key in `SKILL.md` a meaning of its own (a
14
+ // "conditional skill" that stays out of the skill list until a matching file is touched), and `.claude/skills`
15
+ // exposes this corpus to it.
9
16
  export const skillTriggerKey = 'cratis-hint-paths';
17
+
18
+ /** The raw `metadata.cratis-hint-paths` string of a `SKILL.md`, or `undefined` when the skill declares none. */
19
+ export function skillTriggerValue(content: string): string | undefined {
20
+ return frontmatterMap(content, 'metadata')?.get(skillTriggerKey);
21
+ }
22
+
23
+ /** Splits a trigger value into its globs. Globs contain no whitespace, and brace sets keep their commas. */
24
+ export function splitSkillTriggerGlobs(value: string): string[] {
25
+ return value.split(/\s+/).filter(Boolean);
26
+ }
27
+
28
+ /** The trigger globs a `SKILL.md` declares under `metadata.cratis-hint-paths`; empty when it declares none. */
29
+ export function skillTriggerGlobs(content: string): string[] {
30
+ return splitSkillTriggerGlobs(skillTriggerValue(content) ?? '');
31
+ }
@@ -110,6 +110,29 @@ Standalone Chronicle client usage (no Arc) for Kotlin and Java is
110
110
  `cratis-chronicle-client-kotlin`, reused by `cratis/chronicle/client-java`.
111
111
  JVM language conventions are `kotlin.md` and `java.md`.
112
112
 
113
+ ### Arc for TypeScript on Node.js
114
+
115
+ Arc for TypeScript (`@cratis/arc.core` with the Express, Fastify, Hono,
116
+ MongoDB, Drizzle, testing and experimental Chronicle packages) brings the same
117
+ model-bound command/query shape to a Node.js server. Its packages are a source
118
+ preview and are not published to npm. It is separate from `@cratis/arc`, the
119
+ published client runtime that generated proxies import in a frontend.
120
+ `cratis/arc/server-typescript` carries the skills; `cratis/arc/typescript` adds
121
+ the TypeScript language conventions, and `cratis/full/typescript` composes it.
122
+ The server skills are opt-in: `cratis/arc`, `cratis/application` and
123
+ `cratis/application/typescript` do not load them, because they target a
124
+ Node.js server and a C# or React-frontend project has no use for them. Select
125
+ `cratis/arc/typescript` (or `cratis/full/typescript`) for a Node.js Arc server.
126
+
127
+ | Skill | Covers |
128
+ | --- | --- |
129
+ | `cratis-arc-command-typescript` | Project setup from the source preview, `ArcApplication.createBuilder()`, `@command()` with `handle()`/`provide()`, outcomes, authorization, returned Chronicle events, generated metadata and proxies, hosts, `CommandScenario` |
130
+ | `cratis-arc-query-typescript` | `@readModel()` with static `@query()` methods, argument binding, GET and HTTP `QUERY`, paging, RxJS observable queries, MongoDB/Drizzle/Chronicle read models, `QueryScenario`/`ObservableQueryScenario` |
131
+ | `cratis-arc-validation-typescript` | `CommandValidator`/`QueryValidator`/`ConceptValidator`/`ModelValidator`, `readModelForValidation`, `validation()` with `rejected`/`denied`, severity filtering |
132
+
133
+ Standalone Chronicle client usage (no Arc) in TypeScript is
134
+ `cratis-chronicle-client-typescript`.
135
+
113
136
  ### Specialized Profiles
114
137
 
115
138
  | Profile ID | Description |
@@ -2,9 +2,8 @@
2
2
  name: cratis-application-react-specifications
3
3
  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.
4
4
  license: MIT
5
- cratis-hint-paths:
6
- - "**/for_*/**/*.ts"
7
- - "**/for_*/**/*.tsx"
5
+ metadata:
6
+ cratis-hint-paths: "**/for_*/**/*.ts **/for_*/**/*.tsx"
8
7
  ---
9
8
 
10
9
  # Cratis application React specifications
@@ -2,9 +2,8 @@
2
2
  name: cratis-application-slice-specifications
3
3
  description: Specify the backend behavior of an event-sourced Cratis application slice with the in-process scenario family — CommandScenario, EventScenario, ReadModelScenario and ReactorScenario — including what to cover for each slice type and where the specification files live. Use when adding or changing backend behavior in an application built on Cratis. Do not use for framework library specifications and do not use for frontend behavior.
4
4
  license: MIT
5
- cratis-hint-paths:
6
- - "**/when_*/**/*.cs"
7
- - "**/for_*/**/*.cs"
5
+ metadata:
6
+ cratis-hint-paths: "**/when_*/**/*.cs **/for_*/**/*.cs"
8
7
  ---
9
8
 
10
9
  # Cratis application slice specifications
@@ -0,0 +1,2 @@
1
+ Copyright (c) Cratis. All rights reserved.
2
+ Licensed under the MIT license. See LICENSE file in the project root for full license information.