@cratis/pi 0.0.1 → 2.0.2
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/LICENSE +21 -0
- package/README.md +18 -37
- package/package/corpus/agents/backend-developer.md +125 -0
- package/package/corpus/agents/code-reviewer.md +165 -0
- package/package/corpus/agents/coordinator.md +163 -0
- package/package/corpus/agents/frontend-developer.md +246 -0
- package/package/corpus/agents/orchestrator.md +196 -0
- package/package/corpus/agents/performance-reviewer.md +109 -0
- package/package/corpus/agents/planner.md +145 -0
- package/package/corpus/agents/repository-investigation-reviewer.md +45 -0
- package/package/corpus/agents/repository-investigator.md +50 -0
- package/package/corpus/agents/security-reviewer.md +118 -0
- package/package/corpus/agents/slice-implementer.md +59 -0
- package/package/corpus/agents/spec-writer.md +149 -0
- package/package/corpus/harnesses/pi/extensions/cratis-hooks/index.ts +213 -0
- package/package/corpus/harnesses/pi/extensions/cratis-rules/index.ts +27 -0
- package/package/corpus/harnesses/pi/extensions/package.json +4 -0
- package/package/corpus/harnesses/pi/extensions/subagent/agents.ts +167 -0
- package/package/corpus/harnesses/pi/extensions/subagent/index.ts +352 -0
- package/package/corpus/hooks/README.md +434 -0
- package/package/corpus/hooks/agent-stop.md +49 -0
- package/package/corpus/hooks/pre-commit.md +47 -0
- package/package/corpus/hooks/scripts/cratis-guard-writes.sh +87 -0
- package/package/corpus/hooks/scripts/cratis-nuget-pins.txt +11 -0
- package/package/corpus/hooks/scripts/cratis-pattern-scan.sh +197 -0
- package/package/corpus/hooks/scripts/cratis-patterns.json +84 -0
- package/package/corpus/hooks/scripts/cratis-quality-gate.sh +219 -0
- package/package/corpus/hooks/scripts/hook-lib.sh +152 -0
- package/package/corpus/hooks/scripts/quality-gates.json +261 -0
- package/package/corpus/hooks/scripts/type-references-allowlist.txt +71 -0
- package/package/corpus/hooks/scripts/validate-package-imports.sh +166 -0
- package/package/corpus/hooks/scripts/validate-package-subpaths.sh +120 -0
- package/package/corpus/hooks/scripts/validate-type-references.sh +308 -0
- package/package/corpus/hooks/settings.template.json +40 -0
- package/package/corpus/prompts/add-business-rule.prompt.md +22 -0
- package/package/corpus/prompts/add-concept.prompt.md +17 -0
- package/package/corpus/prompts/add-ef-migration.prompt.md +24 -0
- package/package/corpus/prompts/add-projection.prompt.md +20 -0
- package/package/corpus/prompts/add-reactor.prompt.md +22 -0
- package/package/corpus/prompts/add-reducer.prompt.md +20 -0
- package/package/corpus/prompts/audit-hooks.prompt.md +15 -0
- package/package/corpus/prompts/check-doc-drift.prompt.md +21 -0
- package/package/corpus/prompts/code-review.prompt.md +9 -0
- package/package/corpus/prompts/new-feature.prompt.md +9 -0
- package/package/corpus/prompts/new-vertical-slice.prompt.md +18 -0
- package/package/corpus/prompts/review-pr.prompt.md +35 -0
- package/package/corpus/prompts/review-skill.prompt.md +16 -0
- package/package/corpus/prompts/scaffold-feature.prompt.md +16 -0
- package/package/corpus/prompts/ship-changes.prompt.md +20 -0
- package/package/corpus/prompts/verify-ai-setup.prompt.md +19 -0
- package/package/corpus/prompts/write-documentation.prompt.md +21 -0
- package/package/corpus/prompts/write-specs.prompt.md +22 -0
- package/package/corpus/rules/capability-is-not-authority.md +31 -0
- package/package/corpus/rules/code-quality.csharp.md +91 -0
- package/package/corpus/rules/code-quality.md +82 -0
- package/package/corpus/rules/code-quality.typescript.md +89 -0
- package/package/corpus/rules/components.md +207 -0
- package/package/corpus/rules/concepts.md +115 -0
- package/package/corpus/rules/csharp.md +269 -0
- package/package/corpus/rules/dialogs.md +264 -0
- package/package/corpus/rules/documentation-structure-and-formatting.md +148 -0
- package/package/corpus/rules/documentation.md +90 -0
- package/package/corpus/rules/editing-cratis-docs.md +69 -0
- package/package/corpus/rules/efcore.md +235 -0
- package/package/corpus/rules/efcore.specs.md +44 -0
- package/package/corpus/rules/exit-codes-and-wrappers.md +33 -0
- package/package/corpus/rules/framework.md +52 -0
- package/package/corpus/rules/frontend-quality.md +59 -0
- package/package/corpus/rules/frontend-testing.md +126 -0
- package/package/corpus/rules/general.md +305 -0
- package/package/corpus/rules/git-commits.md +138 -0
- package/package/corpus/rules/github-actions.md +92 -0
- package/package/corpus/rules/glossary.md +61 -0
- package/package/corpus/rules/guards-and-fuses.md +45 -0
- package/package/corpus/rules/local-work-artifacts.md +33 -0
- package/package/corpus/rules/managing-ai-rules.md +40 -0
- package/package/corpus/rules/orleans.md +50 -0
- package/package/corpus/rules/pull-requests.md +78 -0
- package/package/corpus/rules/react.md +195 -0
- package/package/corpus/rules/reactors.md +238 -0
- package/package/corpus/rules/rtk.md +37 -0
- package/package/corpus/rules/specs.csharp.md +139 -0
- package/package/corpus/rules/specs.md +132 -0
- package/package/corpus/rules/specs.scenarios.csharp.md +172 -0
- package/package/corpus/rules/specs.typescript.md +139 -0
- package/package/corpus/rules/storybook.md +84 -0
- package/package/corpus/rules/terminal-commands.md +19 -0
- package/package/corpus/rules/typescript.md +149 -0
- package/package/corpus/rules/verification-discipline.md +21 -0
- package/package/corpus/rules/vertical-slices.md +338 -0
- package/package/corpus/rules/web-fetching.md +11 -0
- package/package/corpus/rules/writing-correct-examples.md +35 -0
- package/package/corpus/rules/writing-cratis-docs.md +70 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/SKILL.md +131 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/references/code-style.md +187 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/references/domain-philosophy.md +91 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/references/exceptions-logging-and-di.md +223 -0
- package/package/corpus/skills/cratis-engineering-decision-record/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-decision-record/SKILL.md +133 -0
- package/package/corpus/skills/cratis-engineering-decision-record/references/record-format.md +107 -0
- package/package/corpus/skills/cratis-engineering-docs-authoring/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-docs-authoring/SKILL.md +86 -0
- package/package/corpus/skills/cratis-engineering-docs-authoring/references/site-format.md +46 -0
- package/package/corpus/skills/cratis-engineering-effect-boundaries/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-effect-boundaries/SKILL.md +130 -0
- package/package/corpus/skills/cratis-engineering-effect-boundaries/references/failure-archetypes.md +133 -0
- package/package/corpus/skills/cratis-fundamentals-concept/verification.json +8 -0
- package/package/profile-catalog.json +821 -0
- package/package.json +35 -13
- package/src/index.ts +80 -0
- /package/{skills → package/corpus/skills}/cratis-application-react-specifications/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-application-react-specifications/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-application-slice-specifications/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-application-slice-specifications/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/authentication.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/authorization.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/frontend.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/local-development.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/tenancy.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/references/command-result.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/references/handler-shapes.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/references/proxy-generation.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/references/read-model-injection.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command-execution/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command-execution/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command-validation/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command-validation/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-ef-core-migration/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-ef-core-migration/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-observable-query-http/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-observable-query-http/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-query-paging/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-query-paging/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/data-page.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/data-tables.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/dialogs.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/mvvm.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/queries-and-commands.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-cli-operations/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-cli-operations/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-dotnet/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-dotnet/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-elixir/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-elixir/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-kotlin/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-kotlin/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-typescript/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-typescript/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-compliance/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-compliance/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-constraints/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-constraints/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-modeling/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-modeling/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-specifications/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-specifications/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-type-migration/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-type-migration/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/references/blocked-tools.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/references/observational-tools.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-multi-tenancy/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-multi-tenancy/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-projection/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-projection/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-projection/references/fluent-builder.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-projection/references/model-bound-attributes.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-reactor/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-reactor/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model/references/queries.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model-specifications/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model-specifications/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-reducer/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-reducer/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-web-workbench/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-web-workbench/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-cli-terminal-workbench/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-cli-terminal-workbench/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-code-review/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-code-review/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-accessibility/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-accessibility/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-schema-editor/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-schema-editor/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-stepper-command-dialog/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-stepper-command-dialog/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-styling/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-styling/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-toolbar/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-toolbar/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-documentation-writing/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-documentation-writing/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-event-model-diagram/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-event-model-diagram/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-fundamentals-concept/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-fundamentals-concept/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-fundamentals-type-discovery/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-fundamentals-type-discovery/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-governed-release-methodology/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-governed-release-methodology/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-lens-browser-extension/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-lens-browser-extension/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-performance-review/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-performance-review/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-screenplay-model-authoring/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-screenplay-model-authoring/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-security-review/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-security-review/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specification-by-example/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-specification-by-example/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/references/application-scenarios.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/references/csharp-patterns.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/references/integration-specs.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-typescript/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-typescript/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-typescript/references/typescript-patterns.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-stage-rendering-and-sandbox/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-stage-rendering-and-sandbox/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/references/blocked-tools.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/references/observational-tools.md +0 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cratis enforcement hooks for the Pi coding agent.
|
|
3
|
+
*
|
|
4
|
+
* The corpus enforcement scripts under `.cratis/ai/hooks/scripts/` are the single source of truth and
|
|
5
|
+
* are wired into Claude Code via `.claude/settings.json`. Pi has no markdown/JSON hook format —
|
|
6
|
+
* lifecycle enforcement is done in an extension — so this bridge subscribes to the equivalent Pi
|
|
7
|
+
* events and drives the SAME scripts, synthesizing the Claude hook JSON they read on stdin:
|
|
8
|
+
*
|
|
9
|
+
* Claude PreToolUse (Write|Edit) → Pi `tool_call` → cratis-guard-writes.sh (exit 2 = block)
|
|
10
|
+
* Claude PostToolUse (Write|Edit) → Pi `tool_result` → cratis-pattern-scan.sh (advisory context)
|
|
11
|
+
* Claude Stop → Pi `agent_settled` → cratis-quality-gate.sh (exit 2 = keep going)
|
|
12
|
+
*
|
|
13
|
+
* Nothing here duplicates corpus content: it is adapter machinery, the Pi peer of the Claude
|
|
14
|
+
* `hooks` block in `.claude/settings.json`. Every environment escape hatch the scripts honor
|
|
15
|
+
* (CRATIS_HOOKS_ALLOW_PROTECTED_WRITES, CRATIS_HOOKS_SKIP_SCAN, CRATIS_HOOKS_SKIP_GATE, …) still
|
|
16
|
+
* works because the scripts are executed unchanged.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { spawn } from "node:child_process";
|
|
20
|
+
import * as fs from "node:fs";
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
|
|
25
|
+
const extensionPath = fileURLToPath(import.meta.url);
|
|
26
|
+
const bundledCorpusRoot = path.resolve(path.dirname(extensionPath), "..", "..", "..", "..");
|
|
27
|
+
const isPackagedExtension = extensionPath.includes(`${path.sep}package${path.sep}corpus${path.sep}`);
|
|
28
|
+
|
|
29
|
+
interface ScriptRun {
|
|
30
|
+
code: number;
|
|
31
|
+
stdout: string;
|
|
32
|
+
stderr: string;
|
|
33
|
+
/** The script could not be executed at all, as opposed to running and deciding. */
|
|
34
|
+
failed?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Run a corpus hook script, feeding `stdinJson` on stdin. Never throws.
|
|
39
|
+
*
|
|
40
|
+
* `failed` separates "the script ran and returned a verdict" from "the script never ran", which
|
|
41
|
+
* the exit code alone cannot express: bash exits 127 for a missing script, and a script that
|
|
42
|
+
* fails to spawn produces no code at all. Both used to surface as `code: 0` — indistinguishable
|
|
43
|
+
* from a deliberate allow — so a hook that was absent or unrunnable silently permitted the very
|
|
44
|
+
* writes it exists to refuse. Callers decide what to do with `failed`; this function only reports
|
|
45
|
+
* it honestly.
|
|
46
|
+
*/
|
|
47
|
+
function runScript(script: string, stdinJson: string, cwd: string, signal?: AbortSignal): Promise<ScriptRun> {
|
|
48
|
+
return new Promise<ScriptRun>((resolve) => {
|
|
49
|
+
let proc: ReturnType<typeof spawn>;
|
|
50
|
+
try {
|
|
51
|
+
proc = spawn("bash", [script], { cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
resolve({ code: 127, stdout: "", stderr: String(error), failed: true });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let stdout = "";
|
|
57
|
+
let stderr = "";
|
|
58
|
+
proc.stdout?.on("data", (d) => (stdout += d.toString()));
|
|
59
|
+
proc.stderr?.on("data", (d) => (stderr += d.toString()));
|
|
60
|
+
proc.on("error", (error) => resolve({ code: 127, stdout, stderr: stderr || String(error), failed: true }));
|
|
61
|
+
proc.on("close", (code) => resolve({ code: code ?? 0, stdout, stderr, failed: code === 127 }));
|
|
62
|
+
if (signal) {
|
|
63
|
+
const kill = () => proc.kill("SIGTERM");
|
|
64
|
+
if (signal.aborted) kill();
|
|
65
|
+
else signal.addEventListener("abort", kill, { once: true });
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
proc.stdin?.write(stdinJson);
|
|
69
|
+
proc.stdin?.end();
|
|
70
|
+
} catch {
|
|
71
|
+
/* ignore */
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** file_path + written content, extracted from Pi's write/edit tool inputs. */
|
|
77
|
+
function writeTarget(toolName: string, input: any): { filePath?: string; content?: string; newString?: string } {
|
|
78
|
+
const filePath = input?.path ?? input?.file_path;
|
|
79
|
+
if (toolName === "write") return { filePath, content: typeof input?.content === "string" ? input.content : undefined };
|
|
80
|
+
if (toolName === "edit") {
|
|
81
|
+
const edits = Array.isArray(input?.edits) ? input.edits : [];
|
|
82
|
+
const newString = edits.map((e: any) => (typeof e?.newText === "string" ? e.newText : "")).join("\n");
|
|
83
|
+
return { filePath, newString: newString || undefined };
|
|
84
|
+
}
|
|
85
|
+
return { filePath };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether a hook script is installed at all.
|
|
90
|
+
*
|
|
91
|
+
* A repository that ships no hook scripts is a supported configuration — the corpus scripts
|
|
92
|
+
* themselves degrade to a silent no-op when `jq` is missing, on the stated principle that a hook
|
|
93
|
+
* must never break a session. So an absent script is not an error and is not enforced.
|
|
94
|
+
*
|
|
95
|
+
* The dangerous case is the other one: the script is present, so this repository clearly intends
|
|
96
|
+
* the guard to run, but it cannot be executed. That is a broken guard rather than an absent one,
|
|
97
|
+
* and it is the case that must never be mistaken for permission.
|
|
98
|
+
*/
|
|
99
|
+
function isInstalled(script: string): boolean {
|
|
100
|
+
try {
|
|
101
|
+
return fs.statSync(script).isFile();
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default function (pi: ExtensionAPI) {
|
|
108
|
+
if (isPackagedExtension && fs.existsSync(path.join(process.cwd(), ".cratis", "ai.manifest.json"))) return;
|
|
109
|
+
const managedScriptsDir = path.join(process.cwd(), ".cratis", "ai", "hooks", "scripts");
|
|
110
|
+
const scriptsDir = fs.existsSync(managedScriptsDir) ? managedScriptsDir : path.join(bundledCorpusRoot, "hooks", "scripts");
|
|
111
|
+
const guardWrites = path.join(scriptsDir, "cratis-guard-writes.sh");
|
|
112
|
+
const patternScan = path.join(scriptsDir, "cratis-pattern-scan.sh");
|
|
113
|
+
const qualityGate = path.join(scriptsDir, "cratis-quality-gate.sh");
|
|
114
|
+
|
|
115
|
+
// Mirrors Claude's `stop_hook_active`: true only while the model is continuing because the
|
|
116
|
+
// gate already blocked once this user turn, so the gate never blocks twice in a row (no loop).
|
|
117
|
+
let gateActive = false;
|
|
118
|
+
pi.on("input", async (event) => {
|
|
119
|
+
if (event.source !== "extension") gateActive = false; // a fresh user turn resets the guard
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ── PreToolUse → guard writes (blocking) ──
|
|
123
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
124
|
+
if (event.toolName !== "write" && event.toolName !== "edit") return;
|
|
125
|
+
const { filePath, content, newString } = writeTarget(event.toolName, (event as any).input);
|
|
126
|
+
if (!filePath) return;
|
|
127
|
+
if (!isInstalled(guardWrites)) return; // no guard installed in this repository - nothing to enforce
|
|
128
|
+
const payload = JSON.stringify({ cwd: ctx.cwd, tool_input: { file_path: filePath, content, new_string: newString } });
|
|
129
|
+
const run = await runScript(guardWrites, payload, ctx.cwd, ctx.signal);
|
|
130
|
+
|
|
131
|
+
// The guard is installed but could not run. Allowing here would mean the one case the guard
|
|
132
|
+
// exists to catch - a protected write - passes silently precisely because the guard is broken.
|
|
133
|
+
// Block instead, and say why, so a broken guard is loud rather than permissive.
|
|
134
|
+
if (run.failed) {
|
|
135
|
+
return {
|
|
136
|
+
block: true,
|
|
137
|
+
reason:
|
|
138
|
+
`cratis-guard-writes is installed at ${guardWrites} but could not be run, so this write cannot be checked.` +
|
|
139
|
+
`${run.stderr.trim() ? `\n\n${run.stderr.trim()}` : ""}` +
|
|
140
|
+
"\n\nFix the script (or remove it if this repository is not meant to enforce write guards) and retry.",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (run.code === 2) return { block: true, reason: run.stderr.trim() || "Blocked by cratis-guard-writes." };
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ── PostToolUse → deterministic pattern scan (advisory; injects reminders the model sees) ──
|
|
147
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
148
|
+
if (event.toolName !== "write" && event.toolName !== "edit") return;
|
|
149
|
+
if (event.isError) return;
|
|
150
|
+
const { filePath } = writeTarget(event.toolName, (event as any).input);
|
|
151
|
+
if (!filePath) return;
|
|
152
|
+
if (!isInstalled(patternScan)) return;
|
|
153
|
+
const payload = JSON.stringify({
|
|
154
|
+
cwd: ctx.cwd,
|
|
155
|
+
session_id: ctx.sessionManager.getSessionId?.() ?? "nosession",
|
|
156
|
+
tool_input: { file_path: filePath },
|
|
157
|
+
});
|
|
158
|
+
const run = await runScript(patternScan, payload, ctx.cwd, ctx.signal);
|
|
159
|
+
|
|
160
|
+
// Advisory, not a gate: a broken pattern scan must not fail a write that already succeeded.
|
|
161
|
+
// It is still surfaced rather than swallowed, because a scan that silently stops running
|
|
162
|
+
// looks exactly like a scan that finds nothing.
|
|
163
|
+
if (run.failed) {
|
|
164
|
+
const existing = Array.isArray(event.content) ? event.content : [];
|
|
165
|
+
return {
|
|
166
|
+
content: [
|
|
167
|
+
...existing,
|
|
168
|
+
{
|
|
169
|
+
type: "text",
|
|
170
|
+
text: `\n\n[cratis-hooks] cratis-pattern-scan is installed but could not be run, so no pattern checks were applied to this edit.`,
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (run.code !== 0 || !run.stdout.trim()) return;
|
|
176
|
+
let reminder = "";
|
|
177
|
+
try {
|
|
178
|
+
reminder = JSON.parse(run.stdout)?.hookSpecificOutput?.additionalContext ?? "";
|
|
179
|
+
} catch {
|
|
180
|
+
reminder = "";
|
|
181
|
+
}
|
|
182
|
+
if (!reminder.trim()) return;
|
|
183
|
+
const existing = Array.isArray(event.content) ? event.content : [];
|
|
184
|
+
return { content: [...existing, { type: "text", text: `\n\n[cratis-hooks]\n${reminder.trim()}` }] };
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ── Stop → quality gate (re-runs the gates the change touched; keeps the model going on failure) ──
|
|
188
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
189
|
+
if (!isInstalled(qualityGate)) return;
|
|
190
|
+
const payload = JSON.stringify({
|
|
191
|
+
session_id: ctx.sessionManager.getSessionId?.() ?? "nosession",
|
|
192
|
+
stop_hook_active: gateActive,
|
|
193
|
+
});
|
|
194
|
+
const run = await runScript(qualityGate, payload, ctx.cwd);
|
|
195
|
+
|
|
196
|
+
// A gate that cannot run has not passed. Say so rather than letting the turn end quietly,
|
|
197
|
+
// but only once per user turn, on the same re-entry guard a real gate failure uses.
|
|
198
|
+
if (run.failed && !gateActive) {
|
|
199
|
+
gateActive = true;
|
|
200
|
+
pi.sendUserMessage(
|
|
201
|
+
`The Cratis quality gate is installed at ${qualityGate} but could not be run, so nothing was verified this turn.` +
|
|
202
|
+
`${run.stderr.trim() ? `\n\n${run.stderr.trim()}` : ""}`,
|
|
203
|
+
{ deliverAs: "followUp" },
|
|
204
|
+
);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (run.code === 2 && !gateActive) {
|
|
208
|
+
gateActive = true; // don't block again until the next user turn resets it
|
|
209
|
+
const message = run.stderr.trim() || "A Cratis quality gate failed. Fix it and re-run the gate.";
|
|
210
|
+
pi.sendUserMessage(message, { deliverAs: "followUp" });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
8
|
+
|
|
9
|
+
const corpusRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');
|
|
10
|
+
|
|
11
|
+
/** Loads task-specific rules from the managed corpus. AGENTS.md already supplies general.md. */
|
|
12
|
+
export function managedRules(cwd: string): string {
|
|
13
|
+
const managedRoot = join(cwd, '.cratis', 'ai', 'rules');
|
|
14
|
+
const rulesRoot = existsSync(managedRoot) ? managedRoot : join(corpusRoot, 'rules');
|
|
15
|
+
return readdirSync(rulesRoot, { recursive: true, encoding: 'utf8' })
|
|
16
|
+
.filter((entry): entry is string => entry.endsWith('.md') && entry !== 'general.md')
|
|
17
|
+
.sort()
|
|
18
|
+
.map(entry => readFileSync(join(rulesRoot, entry), 'utf8'))
|
|
19
|
+
.join('\n\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Adds every task-specific managed Cratis rule to Pi without requiring the @cratis/pi package. */
|
|
23
|
+
export default function (pi: ExtensionAPI): void {
|
|
24
|
+
pi.on('before_agent_start', (event, context) => ({
|
|
25
|
+
systemPrompt: `${event.systemPrompt}\n\n${managedRules(context.cwd)}`,
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent discovery + format normalization for the Cratis subagent tool.
|
|
3
|
+
*
|
|
4
|
+
* The agent definitions are the SINGLE-SOURCE corpus files under `.cratis/ai/agents/*.md`,
|
|
5
|
+
* surfaced to Pi through symlink adapters in `.pi/agents/*.md`. Those files are written
|
|
6
|
+
* in the Claude/Copilot shape (Title-Case `name`, a YAML-list `tools:` using Claude tool
|
|
7
|
+
* names such as `Read`/`Glob`/`Bash`, and a `model:` id). Pi's built-in tools are the
|
|
8
|
+
* lowercase set `read, write, edit, bash, grep, find, ls`, so this module NORMALIZES the
|
|
9
|
+
* shared shape to Pi semantics — the adapter layer absorbs the tool difference, exactly
|
|
10
|
+
* like every other adapter in this corpus, so `.pi/agents/*.md` can stay pure symlinks.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
|
|
18
|
+
const corpusAgentsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..", "agents");
|
|
19
|
+
|
|
20
|
+
export type AgentScope = "user" | "project" | "both";
|
|
21
|
+
|
|
22
|
+
export interface AgentConfig {
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
tools?: string[];
|
|
26
|
+
model?: string;
|
|
27
|
+
systemPrompt: string;
|
|
28
|
+
source: "package" | "user" | "project";
|
|
29
|
+
filePath: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AgentDiscoveryResult {
|
|
33
|
+
agents: AgentConfig[];
|
|
34
|
+
projectAgentsDir: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type AgentFrontmatter = {
|
|
38
|
+
name?: unknown;
|
|
39
|
+
description?: unknown;
|
|
40
|
+
tools?: unknown;
|
|
41
|
+
model?: unknown;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Map a Claude/Copilot tool name onto Pi's built-in tool name.
|
|
46
|
+
*
|
|
47
|
+
* Pi built-ins: read, write, edit, bash, grep, find, ls. The corpus agents use the
|
|
48
|
+
* Claude vocabulary (Read/Write/Edit/Bash/Grep/Glob + orchestration tools Agent/Skill/
|
|
49
|
+
* TodoWrite). `Glob` is Pi's `find`; the orchestration tools have no Pi built-in and are
|
|
50
|
+
* dropped (Pi has no separate Agent/Skill/Todo tool — subagent nesting and skills are
|
|
51
|
+
* reached differently). An unknown name is dropped rather than passed through, so a stray
|
|
52
|
+
* value can never disable Pi's tool allowlist by naming a tool that does not exist.
|
|
53
|
+
*/
|
|
54
|
+
const TOOL_NAME_MAP: Record<string, string | null> = {
|
|
55
|
+
read: "read",
|
|
56
|
+
write: "write",
|
|
57
|
+
edit: "edit",
|
|
58
|
+
multiedit: "edit",
|
|
59
|
+
bash: "bash",
|
|
60
|
+
grep: "grep",
|
|
61
|
+
find: "find",
|
|
62
|
+
glob: "find",
|
|
63
|
+
ls: "ls",
|
|
64
|
+
// No Pi built-in equivalent — dropped:
|
|
65
|
+
agent: null,
|
|
66
|
+
task: null,
|
|
67
|
+
skill: null,
|
|
68
|
+
todowrite: null,
|
|
69
|
+
webfetch: null,
|
|
70
|
+
websearch: null,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Normalize a frontmatter `tools` value to a de-duplicated list of Pi tool names.
|
|
75
|
+
* Accepts both YAML spellings in use (`tools: [Read, Bash]` and `tools: Read, Bash`).
|
|
76
|
+
* Returns `undefined` when nothing maps, so the subagent inherits Pi's full default
|
|
77
|
+
* toolset rather than being launched with an empty allowlist.
|
|
78
|
+
*/
|
|
79
|
+
export function normalizeTools(value: unknown): string[] | undefined {
|
|
80
|
+
const raw = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
|
|
81
|
+
const mapped = raw
|
|
82
|
+
.filter((t): t is string => typeof t === "string")
|
|
83
|
+
.map((t) => t.trim().toLowerCase())
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.map((t) => (t in TOOL_NAME_MAP ? TOOL_NAME_MAP[t] : null))
|
|
86
|
+
.filter((t): t is string => typeof t === "string");
|
|
87
|
+
const deduped = Array.from(new Set(mapped));
|
|
88
|
+
return deduped.length > 0 ? deduped : undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function loadAgentsFromDir(dir: string, source: "package" | "user" | "project"): AgentConfig[] {
|
|
92
|
+
const agents: AgentConfig[] = [];
|
|
93
|
+
if (!fs.existsSync(dir)) return agents;
|
|
94
|
+
|
|
95
|
+
let entries: fs.Dirent[];
|
|
96
|
+
try {
|
|
97
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
98
|
+
} catch {
|
|
99
|
+
return agents;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
104
|
+
// Follow symlinks: the corpus adapters in .pi/agents are symlinks into .cratis/ai/agents.
|
|
105
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
106
|
+
|
|
107
|
+
const filePath = path.join(dir, entry.name);
|
|
108
|
+
let content: string;
|
|
109
|
+
try {
|
|
110
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
111
|
+
} catch {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { frontmatter, body } = parseFrontmatter<AgentFrontmatter>(content);
|
|
116
|
+
if (typeof frontmatter.name !== "string" || typeof frontmatter.description !== "string") continue;
|
|
117
|
+
|
|
118
|
+
agents.push({
|
|
119
|
+
name: frontmatter.name,
|
|
120
|
+
description: frontmatter.description,
|
|
121
|
+
tools: normalizeTools(frontmatter.tools),
|
|
122
|
+
model: typeof frontmatter.model === "string" ? frontmatter.model.trim() : undefined,
|
|
123
|
+
systemPrompt: body,
|
|
124
|
+
source,
|
|
125
|
+
filePath,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return agents;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isDirectory(p: string): boolean {
|
|
133
|
+
try {
|
|
134
|
+
return fs.statSync(p).isDirectory();
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Nearest `<ancestor>/.pi/agents` from `cwd` up to the filesystem root. */
|
|
141
|
+
function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
142
|
+
let currentDir = cwd;
|
|
143
|
+
while (true) {
|
|
144
|
+
const candidate = path.join(currentDir, CONFIG_DIR_NAME, "agents");
|
|
145
|
+
if (isDirectory(candidate)) return candidate;
|
|
146
|
+
const parentDir = path.dirname(currentDir);
|
|
147
|
+
if (parentDir === currentDir) return null;
|
|
148
|
+
currentDir = parentDir;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
153
|
+
const userDir = path.join(getAgentDir(), "agents");
|
|
154
|
+
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
155
|
+
|
|
156
|
+
const packageAgents = scope === "project" ? [] : loadAgentsFromDir(corpusAgentsDir, "package");
|
|
157
|
+
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
|
|
158
|
+
const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
|
159
|
+
|
|
160
|
+
// User agents override package agents; project agents override both.
|
|
161
|
+
const agentMap = new Map<string, AgentConfig>();
|
|
162
|
+
for (const agent of packageAgents) agentMap.set(agent.name, agent);
|
|
163
|
+
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
164
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
165
|
+
|
|
166
|
+
return { agents: Array.from(agentMap.values()), projectAgentsDir };
|
|
167
|
+
}
|