@cratis/pi 2.22.0 → 2.23.1
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.
- package/package/corpus/agents/backend-developer.md +6 -5
- package/package/corpus/agents/code-reviewer.md +7 -7
- package/package/corpus/agents/coordinator.md +5 -4
- package/package/corpus/agents/frontend-developer.md +34 -40
- package/package/corpus/agents/orchestrator.md +5 -4
- package/package/corpus/agents/performance-reviewer.md +5 -4
- package/package/corpus/agents/planner.md +5 -4
- package/package/corpus/agents/repository-investigation-reviewer.md +2 -1
- package/package/corpus/agents/repository-investigator.md +2 -1
- package/package/corpus/agents/security-reviewer.md +5 -4
- package/package/corpus/agents/slice-implementer.md +8 -2
- package/package/corpus/agents/spec-writer.md +6 -4
- package/package/corpus/harnesses/pi/extensions/cratis-rules/index.ts +233 -10
- package/package/corpus/harnesses/pi/extensions/subagent/agents.ts +52 -7
- package/package/corpus/harnesses/pi/extensions/subagent/index.ts +14 -0
- package/package/corpus/hooks/README.md +29 -27
- package/package/corpus/hooks/agent-stop.md +1 -5
- package/package/corpus/hooks/scripts/cratis-patterns.json +1 -1
- package/package/corpus/hooks/scripts/validate-package-subpaths.sh +1 -2
- package/package/corpus/prompts/ship-changes.prompt.md +4 -0
- package/package/corpus/prompts/verify-ai-setup.prompt.md +17 -7
- package/package/corpus/rules/ai-distribution.md +60 -0
- package/package/corpus/rules/application-profile.md +73 -0
- package/package/corpus/rules/components.md +77 -44
- package/package/corpus/rules/dialogs.md +24 -20
- package/package/corpus/rules/efcore.md +35 -187
- package/package/corpus/rules/framework.md +1 -1
- package/package/corpus/rules/frontend-quality.md +1 -1
- package/package/corpus/rules/frontend-testing.md +4 -4
- package/package/corpus/rules/general.md +21 -162
- package/package/corpus/rules/guards-and-fuses.md +24 -10
- package/package/corpus/rules/local-work-artifacts.md +6 -0
- package/package/corpus/rules/new-repository-intake.md +34 -0
- package/package/corpus/rules/orleans.md +13 -27
- package/package/corpus/rules/profiles.md +6 -0
- package/package/corpus/rules/react.md +12 -12
- package/package/corpus/rules/reactors.md +28 -5
- package/package/corpus/rules/rtk.md +13 -3
- package/package/corpus/rules/specs.scenarios.csharp.md +18 -3
- package/package/corpus/rules/specs.typescript.md +1 -1
- package/package/corpus/rules/storybook.md +2 -2
- package/package/corpus/rules/typescript.md +1 -0
- package/package/corpus/rules/verification-discipline.md +7 -1
- package/package/corpus/rules/vertical-slices.md +20 -15
- package/package/corpus/rules/writing-correct-examples.md +1 -0
- package/package/corpus/skills/cratis-application-react-specifications/SKILL.md +4 -4
- package/package/corpus/skills/cratis-application-slice-specifications/SKILL.md +16 -8
- package/package/corpus/skills/cratis-arc-authentication-authorization-and-identity/SKILL.md +6 -4
- package/package/corpus/skills/cratis-arc-authentication-authorization-and-identity/references/authentication.md +1 -1
- package/package/corpus/skills/cratis-arc-authentication-authorization-and-identity/references/authorization.md +1 -1
- package/package/corpus/skills/cratis-arc-authentication-authorization-and-identity/references/frontend.md +1 -1
- package/package/corpus/skills/cratis-arc-authentication-authorization-and-identity/references/local-development.md +1 -1
- package/package/corpus/skills/cratis-arc-authentication-authorization-and-identity/references/tenancy.md +7 -4
- package/package/corpus/skills/cratis-arc-command/SKILL.md +9 -7
- package/package/corpus/skills/cratis-arc-command/references/command-result.md +1 -1
- package/package/corpus/skills/cratis-arc-command/references/handler-shapes.md +2 -2
- package/package/corpus/skills/cratis-arc-command/references/proxy-generation.md +6 -3
- package/package/corpus/skills/cratis-arc-command/references/read-model-injection.md +1 -1
- package/package/corpus/skills/cratis-arc-command-execution/SKILL.md +12 -2
- package/package/corpus/skills/cratis-arc-command-validation/SKILL.md +5 -3
- package/package/corpus/skills/cratis-arc-ef-core-migration/SKILL.md +7 -4
- package/package/corpus/skills/cratis-arc-observable-query-http/SKILL.md +10 -3
- package/package/corpus/skills/cratis-arc-query-paging/SKILL.md +13 -8
- package/package/corpus/skills/cratis-arc-react-page/SKILL.md +27 -24
- package/package/corpus/skills/cratis-arc-react-page/references/data-page.md +15 -11
- package/package/corpus/skills/cratis-arc-react-page/references/data-tables.md +9 -2
- package/package/corpus/skills/cratis-arc-react-page/references/dialogs.md +11 -9
- package/package/corpus/skills/cratis-arc-react-page/references/queries-and-commands.md +26 -2
- package/package/corpus/skills/cratis-chronicle-cli-operations/SKILL.md +19 -13
- package/package/corpus/skills/cratis-chronicle-client-dotnet/SKILL.md +17 -15
- package/package/corpus/skills/cratis-chronicle-client-elixir/SKILL.md +24 -20
- package/package/corpus/skills/cratis-chronicle-client-kotlin/SKILL.md +26 -15
- package/package/corpus/skills/cratis-chronicle-client-typescript/SKILL.md +15 -11
- package/package/corpus/skills/cratis-chronicle-compliance/SKILL.md +14 -3
- package/package/corpus/skills/cratis-chronicle-event-constraints/SKILL.md +13 -6
- package/package/corpus/skills/cratis-chronicle-event-modeling/SKILL.md +2 -2
- package/package/corpus/skills/cratis-chronicle-event-specifications/SKILL.md +11 -5
- package/package/corpus/skills/cratis-chronicle-event-type-migration/SKILL.md +2 -2
- package/package/corpus/skills/cratis-chronicle-multi-tenancy/SKILL.md +2 -2
- package/package/corpus/skills/cratis-chronicle-projection/SKILL.md +23 -4
- package/package/corpus/skills/cratis-chronicle-projection/references/fluent-builder.md +1 -1
- package/package/corpus/skills/cratis-chronicle-projection/references/model-bound-attributes.md +2 -2
- package/package/corpus/skills/cratis-chronicle-reactor/SKILL.md +22 -8
- package/package/corpus/skills/cratis-chronicle-read-model/SKILL.md +1 -1
- package/package/corpus/skills/cratis-chronicle-read-model/references/queries.md +1 -1
- package/package/corpus/skills/cratis-chronicle-read-model-specifications/SKILL.md +3 -1
- package/package/corpus/skills/cratis-chronicle-reducer/SKILL.md +1 -1
- package/package/corpus/skills/cratis-chronicle-web-workbench/SKILL.md +19 -11
- package/package/corpus/skills/cratis-cli-terminal-workbench/SKILL.md +11 -6
- package/package/corpus/skills/cratis-code-review/SKILL.md +8 -6
- package/package/corpus/skills/cratis-components-accessibility/SKILL.md +112 -82
- package/package/corpus/skills/cratis-components-schema-editor/SKILL.md +12 -10
- package/package/corpus/skills/cratis-components-stepper-command-dialog/SKILL.md +11 -12
- package/package/corpus/skills/cratis-components-styling/SKILL.md +204 -170
- package/package/corpus/skills/cratis-components-toolbar/SKILL.md +32 -26
- package/package/corpus/skills/cratis-event-model-diagram/SKILL.md +1 -1
- package/package/corpus/skills/cratis-fundamentals-concept/SKILL.md +2 -2
- package/package/corpus/skills/cratis-fundamentals-type-discovery/SKILL.md +2 -2
- package/package/corpus/skills/cratis-lens-browser-extension/SKILL.md +4 -2
- package/package/corpus/skills/cratis-performance-review/SKILL.md +4 -2
- package/package/corpus/skills/cratis-screenplay-model-authoring/SKILL.md +15 -5
- package/package/corpus/skills/cratis-security-review/SKILL.md +20 -3
- package/package/corpus/skills/cratis-specifications-csharp/SKILL.md +3 -3
- package/package/corpus/skills/cratis-specifications-csharp/references/application-scenarios.md +12 -8
- package/package/corpus/skills/cratis-specifications-csharp/references/csharp-patterns.md +1 -1
- package/package/corpus/skills/cratis-specifications-csharp/references/integration-specs.md +7 -3
- package/package/corpus/skills/cratis-specifications-typescript/SKILL.md +6 -3
- package/package/corpus/skills/cratis-specifications-typescript/references/typescript-patterns.md +21 -4
- package/package/corpus/skills/cratis-stage-rendering-and-sandbox/SKILL.md +29 -17
- package/package.json +1 -1
- package/package/corpus/rules/terminal-commands.md +0 -19
|
@@ -7,11 +7,12 @@ description: >
|
|
|
7
7
|
and reactors — all in strict compliance with the vertical slice architecture.
|
|
8
8
|
model: claude-sonnet-4-5
|
|
9
9
|
tools:
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
10
|
+
- Read
|
|
11
|
+
- Grep
|
|
12
|
+
- Glob
|
|
13
|
+
- Bash
|
|
14
|
+
- Edit
|
|
15
|
+
- Write
|
|
15
16
|
---
|
|
16
17
|
|
|
17
18
|
# Backend Developer
|
|
@@ -6,11 +6,11 @@ description: >
|
|
|
6
6
|
TypeScript conventions, and vertical slice correctness before merge.
|
|
7
7
|
model: claude-sonnet-4-5
|
|
8
8
|
tools:
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
9
|
+
- Read
|
|
10
|
+
- Grep
|
|
11
|
+
- Glob
|
|
12
|
+
- Bash
|
|
13
|
+
readonly: true
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
# Code Reviewer
|
|
@@ -110,7 +110,7 @@ When checking unused code, references, or naming, use semantic navigation if the
|
|
|
110
110
|
|
|
111
111
|
## TypeScript Styling checklist
|
|
112
112
|
|
|
113
|
-
- [ ] No hard-coded hex/rgb values —
|
|
113
|
+
- [ ] No hard-coded hex/rgb values — `--cratis-*` tokens used
|
|
114
114
|
- [ ] CSS co-located with component (`.css` file in same folder)
|
|
115
115
|
- [ ] No `!important` unless absolutely required and justified with a comment
|
|
116
116
|
|
|
@@ -128,7 +128,7 @@ When checking unused code, references, or naming, use semantic navigation if the
|
|
|
128
128
|
- [ ] README.md exists for complex component folders
|
|
129
129
|
- [ ] `CommandDialog` from `@cratis/components/CommandDialog` used for command-based dialogs
|
|
130
130
|
- [ ] `Dialog` from `@cratis/components/Dialogs` used for data-only dialogs
|
|
131
|
-
- [ ] Never
|
|
131
|
+
- [ ] Never uses a vendor or hand-rolled modal — `CommandDialog` / `Dialog` from Cratis Components
|
|
132
132
|
- [ ] No monolithic components — decomposed into smaller, focused sub-components
|
|
133
133
|
|
|
134
134
|
---
|
|
@@ -6,11 +6,12 @@ description: >
|
|
|
6
6
|
proxies, following the project's component and styling conventions.
|
|
7
7
|
model: claude-sonnet-4-5
|
|
8
8
|
tools:
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
9
|
+
- Read
|
|
10
|
+
- Grep
|
|
11
|
+
- Glob
|
|
12
|
+
- Bash
|
|
13
|
+
- Edit
|
|
14
|
+
- Write
|
|
14
15
|
---
|
|
15
16
|
|
|
16
17
|
# Frontend Developer
|
|
@@ -64,7 +65,7 @@ Confirm that the TypeScript proxies exist in the slice folder before writing any
|
|
|
64
65
|
- Place `.tsx` files in the **same folder** as the corresponding `.cs` file.
|
|
65
66
|
- Do NOT prefix the file name with the feature or slice name (folder provides context).
|
|
66
67
|
- Each component has its own `.css` file for static styles.
|
|
67
|
-
- Use
|
|
68
|
+
- Use the `--cratis-*` design tokens for all colors, backgrounds, and borders — never hard-code hex values. The default stack is Cratis Components 4 (Components-owned markup and tokens; no PrimeReact) — not Tailwind.
|
|
68
69
|
- Use `const` over `let`.
|
|
69
70
|
- Use full descriptive names (never abbreviations like `e`, `idx`, `prev`).
|
|
70
71
|
- **Move non-trivial state out of the render function** into a `withViewModel` view model (or a tested state module) — see `react.md`. Extract as soon as a component has 3+ `useState`, a state-syncing `useEffect`, or derived values. A view model is a plain class with no React hooks, constructible in a spec.
|
|
@@ -90,28 +91,22 @@ const handleSubmit = async () => {
|
|
|
90
91
|
## Query usage pattern (with paging)
|
|
91
92
|
|
|
92
93
|
```tsx
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
totalRecords={allProjectsResult.paging.totalItems}
|
|
104
|
-
alwaysShowPaginator={false}
|
|
105
|
-
first={allProjectsResult.paging.page * pageSize}
|
|
106
|
-
onPage={event => setPage(event.page ?? 0)}
|
|
107
|
-
scrollable scrollHeight="flex"
|
|
108
|
-
emptyMessage="No items found.">
|
|
109
|
-
<Column field="name" header="Name" />
|
|
110
|
-
</DataTable>
|
|
111
|
-
);
|
|
112
|
-
};
|
|
94
|
+
import { DataTableForQuery, Column } from '@cratis/components/DataTables';
|
|
95
|
+
import { AllProjects } from './AllProjects';
|
|
96
|
+
|
|
97
|
+
// The table subscribes to the query itself and pages server-side (20 rows a page);
|
|
98
|
+
// do not fetch rows and pass an items array.
|
|
99
|
+
export const Listing = () => (
|
|
100
|
+
<DataTableForQuery query={AllProjects} emptyMessage="No items found.">
|
|
101
|
+
<Column field="name" header="Name" />
|
|
102
|
+
</DataTableForQuery>
|
|
103
|
+
);
|
|
113
104
|
```
|
|
114
105
|
|
|
106
|
+
For caller-controlled paging outside a table, use the proxy hook directly —
|
|
107
|
+
`const [result, , , setPage] = AllProjects.useWithPaging(pageSize)`; `result.data` and
|
|
108
|
+
`result.paging` (`page`, `size`, `totalItems`, `totalPages`) drive your own layout.
|
|
109
|
+
|
|
115
110
|
---
|
|
116
111
|
|
|
117
112
|
## Dialog patterns
|
|
@@ -133,7 +128,7 @@ export const AddProject = ({ closeDialog }: DialogProps) => {
|
|
|
133
128
|
title="Add Project"
|
|
134
129
|
okLabel="Add"
|
|
135
130
|
cancelLabel="Cancel"
|
|
136
|
-
|
|
131
|
+
onSuccess={() => closeDialog(DialogResult.Ok)}
|
|
137
132
|
onCancel={() => closeDialog(DialogResult.Cancelled)}
|
|
138
133
|
>
|
|
139
134
|
<InputTextField<RegisterProject>
|
|
@@ -157,7 +152,7 @@ Use this for dialogs that collect data and return it without executing a command
|
|
|
157
152
|
import { useState } from 'react';
|
|
158
153
|
import { DialogProps, DialogResult } from '@cratis/arc.react/dialogs';
|
|
159
154
|
import { Dialog } from '@cratis/components/Dialogs';
|
|
160
|
-
import {
|
|
155
|
+
import { TextInput } from '@cratis/components/Common';
|
|
161
156
|
|
|
162
157
|
export const AddProject = ({ closeDialog }: DialogProps<{ name: string }>) => {
|
|
163
158
|
const [name, setName] = useState('');
|
|
@@ -173,18 +168,18 @@ export const AddProject = ({ closeDialog }: DialogProps<{ name: string }>) => {
|
|
|
173
168
|
onConfirm={() => closeDialog(DialogResult.Ok, { name })}
|
|
174
169
|
onCancel={() => closeDialog(DialogResult.Cancelled)}
|
|
175
170
|
>
|
|
176
|
-
<
|
|
171
|
+
<TextInput
|
|
177
172
|
value={name}
|
|
178
|
-
onChange={
|
|
173
|
+
onChange={value => setName(value)}
|
|
179
174
|
placeholder="Enter a name"
|
|
180
|
-
|
|
175
|
+
aria-label="Project name"
|
|
181
176
|
/>
|
|
182
177
|
</Dialog>
|
|
183
178
|
);
|
|
184
179
|
};
|
|
185
180
|
```
|
|
186
181
|
|
|
187
|
-
> **Never**
|
|
182
|
+
> **Never** use a vendor or hand-rolled modal — dialogs are Components-owned.
|
|
188
183
|
|
|
189
184
|
---
|
|
190
185
|
|
|
@@ -195,20 +190,19 @@ import { Page } from '@cratis/components/Common';
|
|
|
195
190
|
import { AddProject } from './Registration/AddProject';
|
|
196
191
|
import { Listing } from './Listing/Listing';
|
|
197
192
|
import { DialogResult, useDialog } from '@cratis/arc.react/dialogs';
|
|
198
|
-
import { Button } from '
|
|
199
|
-
import
|
|
193
|
+
import { Button } from '@cratis/components/Common';
|
|
194
|
+
import { MdAdd } from 'react-icons/md';
|
|
200
195
|
|
|
201
196
|
export const Projects = () => {
|
|
202
197
|
const [AddProjectDialog, showAddProjectDialog] = useDialog(AddProject);
|
|
203
198
|
|
|
204
199
|
// For a query-backed list page, prefer `DataPage` with `<DataPage.MenuItems>`
|
|
205
|
-
// (it owns the action bar).
|
|
206
|
-
//
|
|
200
|
+
// (it owns the action bar). For a custom action row, compose Components `Button`s
|
|
201
|
+
// (`variant`: solid | outline | ghost | link; `tone`: neutral | accent | positive | caution | critical).
|
|
207
202
|
return (
|
|
208
203
|
<Page title="Projects">
|
|
209
|
-
<Button variant="
|
|
210
|
-
|
|
211
|
-
<span>Add Project</span>
|
|
204
|
+
<Button variant="ghost" icon={<MdAdd aria-hidden="true" />} onClick={() => showAddProjectDialog()}>
|
|
205
|
+
Add Project
|
|
212
206
|
</Button>
|
|
213
207
|
<Listing />
|
|
214
208
|
<AddProjectDialog />
|
|
@@ -238,7 +232,7 @@ Before handing back:
|
|
|
238
232
|
- [ ] `npx tsc -b` passes with zero errors
|
|
239
233
|
- [ ] Components are in the correct slice folder
|
|
240
234
|
- [ ] If the app has a localization convention, user-visible text is routed through it (product policy — not a Cratis rule)
|
|
241
|
-
- [ ] No hard-coded hex/rgb color values —
|
|
235
|
+
- [ ] No hard-coded hex/rgb color values — `--cratis-*` tokens used throughout
|
|
242
236
|
- [ ] All variable/parameter names are fully descriptive (no abbreviations)
|
|
243
237
|
- [ ] No `any` types — `unknown` with type guards where needed
|
|
244
238
|
- [ ] Composition page updated to include the new component
|
|
@@ -10,10 +10,11 @@ description: >
|
|
|
10
10
|
large refactors, or any goal that spans more than one concern.
|
|
11
11
|
model: claude-sonnet-4-5
|
|
12
12
|
tools:
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
13
|
+
- Read
|
|
14
|
+
- Grep
|
|
15
|
+
- Glob
|
|
16
|
+
- Bash
|
|
17
|
+
- Agent
|
|
17
18
|
---
|
|
18
19
|
|
|
19
20
|
# Orchestrator
|
|
@@ -6,10 +6,11 @@ description: >
|
|
|
6
6
|
React render overhead, and Chronicle anti-patterns before merge.
|
|
7
7
|
model: claude-sonnet-4-5
|
|
8
8
|
tools:
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
9
|
+
- Read
|
|
10
|
+
- Grep
|
|
11
|
+
- Glob
|
|
12
|
+
- Bash
|
|
13
|
+
readonly: true
|
|
13
14
|
---
|
|
14
15
|
|
|
15
16
|
# Performance Reviewer
|
|
@@ -5,7 +5,13 @@ description: >
|
|
|
5
5
|
in when_*/ folders, and the React surface (page and/or command dialog). Use for new slices and for
|
|
6
6
|
non-trivial slice changes spanning backend and frontend.
|
|
7
7
|
model: claude-opus-4-8
|
|
8
|
-
tools:
|
|
8
|
+
tools:
|
|
9
|
+
- Read
|
|
10
|
+
- Grep
|
|
11
|
+
- Glob
|
|
12
|
+
- Bash
|
|
13
|
+
- Edit
|
|
14
|
+
- Write
|
|
9
15
|
---
|
|
10
16
|
|
|
11
17
|
# Slice Implementer
|
|
@@ -52,7 +58,7 @@ Proxies now exist. Build React components from the generated proxies (`react.md`
|
|
|
52
58
|
- Events: no arguments on `[EventType]`, non-nullable, past tense, `<summary>`, never carry the event-source id.
|
|
53
59
|
- `[OnceOnly]` on non-idempotent reactor side effects; reactors return side-effect events or use `ICommandPipeline` (never `IEventLog`).
|
|
54
60
|
- Specs `#if DEBUG`, command aliased, per-test unique values.
|
|
55
|
-
- Frontend via `withViewModel` + Arc proxy hooks + Cratis Components; never edit generated proxies; never
|
|
61
|
+
- Frontend via `withViewModel` + Arc proxy hooks + Cratis Components; never edit generated proxies; never use a vendor or hand-rolled modal (dialogs come from `@cratis/components/CommandDialog` and `/Dialogs`).
|
|
56
62
|
|
|
57
63
|
## Output
|
|
58
64
|
|
|
@@ -6,10 +6,12 @@ description: >
|
|
|
6
6
|
comprehensive behavior coverage following the project's BDD conventions.
|
|
7
7
|
model: claude-sonnet-4-5
|
|
8
8
|
tools:
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
9
|
+
- Read
|
|
10
|
+
- Grep
|
|
11
|
+
- Glob
|
|
12
|
+
- Bash
|
|
13
|
+
- Edit
|
|
14
|
+
- Write
|
|
13
15
|
---
|
|
14
16
|
|
|
15
17
|
# Spec Writer
|
|
@@ -1,27 +1,250 @@
|
|
|
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
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
5
|
-
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
5
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
8
8
|
|
|
9
9
|
const corpusRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
type AiConfiguration = { profiles?: string[] };
|
|
12
|
+
|
|
13
|
+
export interface ManagedRule {
|
|
14
|
+
/** Path relative to the rules root, e.g. `code-quality-csharp.md` or `project/running-the-local-stack.md`. */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Full file content, frontmatter included, exactly as the other harnesses receive it. */
|
|
17
|
+
content: string;
|
|
18
|
+
/** `application`, `framework`, or undefined when the rule is not profile-specific. */
|
|
19
|
+
profile?: string;
|
|
20
|
+
/** Globs from `applyTo` and `paths`. Empty means the rule applies to every file. */
|
|
21
|
+
globs: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const universalGlobs = new Set(['**', '**/*', '*']);
|
|
25
|
+
|
|
26
|
+
function unquote(value: string): string {
|
|
27
|
+
return value.trim().replace(/^["']|["']$/g, '');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Reads the YAML-ish frontmatter the corpus uses: scalar `key: value` lines and `key:` followed by
|
|
32
|
+
* ` - item` lines. Anything richer is not used by the rules and is deliberately not supported.
|
|
33
|
+
*/
|
|
34
|
+
function frontmatter(content: string): Map<string, string[]> {
|
|
35
|
+
const fields = new Map<string, string[]>();
|
|
36
|
+
if (!content.startsWith('---\n')) return fields;
|
|
37
|
+
const end = content.indexOf('\n---\n', 4);
|
|
38
|
+
if (end < 0) return fields;
|
|
39
|
+
let current: string | undefined;
|
|
40
|
+
for (const line of content.slice(4, end).split('\n')) {
|
|
41
|
+
const item = /^\s+-\s+(.*)$/.exec(line);
|
|
42
|
+
if (item && current) {
|
|
43
|
+
fields.get(current)!.push(unquote(item[1]));
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const scalar = /^([A-Za-z][\w-]*):\s*(.*)$/.exec(line);
|
|
47
|
+
if (!scalar) continue;
|
|
48
|
+
current = scalar[1];
|
|
49
|
+
const value = unquote(scalar[2]);
|
|
50
|
+
fields.set(current, value ? value.split(',').map(unquote).filter(Boolean) : []);
|
|
51
|
+
}
|
|
52
|
+
return fields;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function escapeRegExp(text: string): string {
|
|
56
|
+
return text.replace(/[.+^$()|[\]\\]/g, '\\$&');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Converts the glob dialect used by `applyTo` (`**`, `*`, `?`, `{a,b}`) into an anchored RegExp. */
|
|
60
|
+
export function globToRegExp(glob: string): RegExp {
|
|
61
|
+
let pattern = '';
|
|
62
|
+
for (let index = 0; index < glob.length; index++) {
|
|
63
|
+
const character = glob[index];
|
|
64
|
+
if (character === '*') {
|
|
65
|
+
if (glob[index + 1] === '*') {
|
|
66
|
+
if (glob[index + 2] === '/') {
|
|
67
|
+
pattern += '(?:.*/)?';
|
|
68
|
+
index += 2;
|
|
69
|
+
} else {
|
|
70
|
+
pattern += '.*';
|
|
71
|
+
index += 1;
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
pattern += '[^/]*';
|
|
75
|
+
}
|
|
76
|
+
} else if (character === '?') {
|
|
77
|
+
pattern += '[^/]';
|
|
78
|
+
} else if (character === '{') {
|
|
79
|
+
const close = glob.indexOf('}', index);
|
|
80
|
+
if (close > index) {
|
|
81
|
+
pattern += `(?:${glob.slice(index + 1, close).split(',').map(part => escapeRegExp(part.trim())).join('|')})`;
|
|
82
|
+
index = close;
|
|
83
|
+
} else {
|
|
84
|
+
pattern += '\\{';
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
pattern += escapeRegExp(character);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return new RegExp(`^${pattern}$`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function rulesRoot(cwd: string): string {
|
|
13
94
|
const managedRoot = join(cwd, '.cratis', 'ai', 'rules');
|
|
14
|
-
|
|
15
|
-
|
|
95
|
+
return existsSync(managedRoot) ? managedRoot : join(corpusRoot, 'rules');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function configuration(cwd: string): AiConfiguration | undefined {
|
|
99
|
+
const path = join(cwd, '.cratis', 'ai.json');
|
|
100
|
+
if (!existsSync(path)) return undefined;
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(readFileSync(path, 'utf8')) as AiConfiguration;
|
|
103
|
+
} catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A profile-specific rule is kept only when the repository selects that profile. Repositories without
|
|
110
|
+
* a `.cratis/ai.json` keep every rule, matching the `@cratis/pi` package.
|
|
111
|
+
*/
|
|
112
|
+
function matchesProfile(rule: ManagedRule, selected: AiConfiguration | undefined): boolean {
|
|
113
|
+
if (!rule.profile || !selected) return true;
|
|
114
|
+
const profiles = selected.profiles ?? [];
|
|
115
|
+
if (rule.profile === 'application') return profiles.some(profile => profile.startsWith('cratis/application'));
|
|
116
|
+
if (rule.profile === 'framework') return profiles.some(profile => profile.startsWith('cratis/engineering'));
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Loads every managed rule with its frontmatter interpreted, filtered to the repository's profiles. */
|
|
121
|
+
export function managedRules(cwd: string): ManagedRule[] {
|
|
122
|
+
const root = rulesRoot(cwd);
|
|
123
|
+
const selected = configuration(cwd);
|
|
124
|
+
return readdirSync(root, { recursive: true, encoding: 'utf8' })
|
|
16
125
|
.filter((entry): entry is string => entry.endsWith('.md'))
|
|
17
126
|
.sort()
|
|
18
|
-
.map(entry =>
|
|
19
|
-
|
|
127
|
+
.map(entry => {
|
|
128
|
+
const content = readFileSync(join(root, entry), 'utf8');
|
|
129
|
+
const fields = frontmatter(content);
|
|
130
|
+
return {
|
|
131
|
+
name: entry.split(sep).join('/'),
|
|
132
|
+
content,
|
|
133
|
+
profile: fields.get('profile')?.[0],
|
|
134
|
+
globs: [...(fields.get('applyTo') ?? []), ...(fields.get('paths') ?? [])],
|
|
135
|
+
} satisfies ManagedRule;
|
|
136
|
+
})
|
|
137
|
+
.filter(rule => matchesProfile(rule, selected));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Rules that apply to every file. These belong in the system prompt. */
|
|
141
|
+
export function universalRules(cwd: string): ManagedRule[] {
|
|
142
|
+
return managedRules(cwd).filter(rule => rule.globs.length === 0 || rule.globs.some(glob => universalGlobs.has(glob)));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Rules whose `applyTo`/`paths` match a repository-relative path. These are delivered when that file is touched. */
|
|
146
|
+
export function rulesForPath(cwd: string, relativePath: string): ManagedRule[] {
|
|
147
|
+
const normalized = relativePath.split(sep).join('/');
|
|
148
|
+
return managedRules(cwd).filter(rule =>
|
|
149
|
+
rule.globs.length > 0 &&
|
|
150
|
+
!rule.globs.some(glob => universalGlobs.has(glob)) &&
|
|
151
|
+
rule.globs.some(glob => globToRegExp(glob).test(normalized)));
|
|
20
152
|
}
|
|
21
153
|
|
|
22
|
-
/**
|
|
154
|
+
/** Upper bound on files considered from one bash command, so a wide command cannot deliver the corpus. */
|
|
155
|
+
const maxPathsPerCommand = 10;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Extracts file paths a bash command refers to. `rtk.md` tells agents to run `rtk read`, `rtk grep`
|
|
159
|
+
* and `rtk find` from the terminal for bulk reads, so file access frequently arrives as a bash
|
|
160
|
+
* command rather than the read tool; without this, a session that follows that rule would never
|
|
161
|
+
* receive a path-scoped rule. A token counts only when it resolves to a file that exists inside the
|
|
162
|
+
* working directory, which keeps a filename mentioned inside a commit message from matching.
|
|
163
|
+
*/
|
|
164
|
+
function pathsFromCommand(command: string, cwd: string): string[] {
|
|
165
|
+
const found: string[] = [];
|
|
166
|
+
for (const raw of command.split(/[\s;|&()<>]+/)) {
|
|
167
|
+
if (found.length >= maxPathsPerCommand) break;
|
|
168
|
+
const token = raw.replace(/^['"]+|['"]+$/g, '').replace(/[,:]+$/, '');
|
|
169
|
+
if (!token || token.startsWith('-') || !/[./]/.test(token)) continue;
|
|
170
|
+
try {
|
|
171
|
+
if (statSync(resolve(cwd, token)).isFile()) found.push(token);
|
|
172
|
+
} catch {
|
|
173
|
+
/* not a path we can see; ignore */
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return found;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function touchedPaths(toolName: string, input: unknown, cwd: string): string[] {
|
|
180
|
+
if (toolName === 'bash' || toolName === 'powershell') {
|
|
181
|
+
const command = (input as { command?: unknown } | undefined)?.command;
|
|
182
|
+
return typeof command === 'string' ? pathsFromCommand(command, cwd) : [];
|
|
183
|
+
}
|
|
184
|
+
if (toolName !== 'read' && toolName !== 'write' && toolName !== 'edit') return [];
|
|
185
|
+
const candidate = (input as { path?: unknown; file_path?: unknown } | undefined);
|
|
186
|
+
const value = candidate?.path ?? candidate?.file_path;
|
|
187
|
+
return typeof value === 'string' && value.length > 0 ? [value] : [];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Gives Pi the same rule semantics as the other harnesses: universal rules in the system prompt, and
|
|
192
|
+
* path-scoped rules attached the first time a matching file is touched in the session. Without this,
|
|
193
|
+
* every rule was concatenated into every turn regardless of `applyTo`, `paths`, or `profile`.
|
|
194
|
+
*
|
|
195
|
+
* Delivery happens on `tool_result`, so a rule arrives after the call that first touched its file.
|
|
196
|
+
* `ToolCallEventResult` carries only `block`/`reason`/`terminate`, so there is no supported way to add
|
|
197
|
+
* context before a tool runs. In practice a file is read before it is edited, and reads through both
|
|
198
|
+
* the read tool and bash are covered, so the rule is present before the edit; a file created blind by
|
|
199
|
+
* `write` is the residual case, and it receives the rule with that result.
|
|
200
|
+
*
|
|
201
|
+
* A delivered rule lives in the conversation rather than the system prompt, so anything that rewrites
|
|
202
|
+
* or replaces the conversation can remove it. The delivery record is therefore reset whenever that
|
|
203
|
+
* happens, and the rule is delivered again the next time one of its files is touched.
|
|
204
|
+
*/
|
|
23
205
|
export default function (pi: ExtensionAPI): void {
|
|
206
|
+
const delivered = new Set<string>();
|
|
207
|
+
|
|
208
|
+
// Compaction summarizes the conversation, which can drop an injected rule while leaving the
|
|
209
|
+
// delivery record claiming it is present; a switch replaces the conversation outright. Without
|
|
210
|
+
// this reset a long session would silently lose its scoped rules and never see them again.
|
|
211
|
+
const reset = () => {
|
|
212
|
+
delivered.clear();
|
|
213
|
+
};
|
|
214
|
+
pi.on('session_start', reset);
|
|
215
|
+
pi.on('session_compact', reset);
|
|
216
|
+
pi.on('session_before_switch', reset);
|
|
217
|
+
|
|
24
218
|
pi.on('before_agent_start', (event, context) => ({
|
|
25
|
-
systemPrompt: `${event.systemPrompt}\n\n${
|
|
219
|
+
systemPrompt: `${event.systemPrompt}\n\n${universalRules(context.cwd).map(rule => rule.content).join('\n\n')}`,
|
|
26
220
|
}));
|
|
221
|
+
|
|
222
|
+
pi.on('tool_result', (event, context) => {
|
|
223
|
+
if (event.isError) return;
|
|
224
|
+
const paths = touchedPaths(event.toolName, (event as { input?: unknown }).input, context.cwd);
|
|
225
|
+
if (paths.length === 0) return;
|
|
226
|
+
const pending: ManagedRule[] = [];
|
|
227
|
+
const matched: string[] = [];
|
|
228
|
+
for (const path of paths) {
|
|
229
|
+
const relativePath = relative(context.cwd, resolve(context.cwd, path));
|
|
230
|
+
if (!relativePath || relativePath.startsWith('..')) continue;
|
|
231
|
+
for (const rule of rulesForPath(context.cwd, relativePath)) {
|
|
232
|
+
if (delivered.has(rule.name) || pending.some(candidate => candidate.name === rule.name)) continue;
|
|
233
|
+
pending.push(rule);
|
|
234
|
+
if (!matched.includes(relativePath)) matched.push(relativePath);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (pending.length === 0) return;
|
|
238
|
+
pending.forEach(rule => delivered.add(rule.name));
|
|
239
|
+
const existing = Array.isArray(event.content) ? event.content : [];
|
|
240
|
+
return {
|
|
241
|
+
content: [
|
|
242
|
+
...existing,
|
|
243
|
+
{
|
|
244
|
+
type: 'text',
|
|
245
|
+
text: `\n\n[cratis-rules] Rules that apply to ${matched.map(path => path.split(sep).join('/')).join(', ')}:\n\n${pending.map(rule => rule.content).join('\n\n')}`,
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
};
|
|
249
|
+
});
|
|
27
250
|
}
|