From 18bbf6913fccd84fb85a188c62e3046d54a62970 Mon Sep 17 00:00:00 2001 From: woksin Date: Wed, 19 Aug 2026 14:32:00 +0200 Subject: [PATCH 1/4] Detect Pi as an AI tool Pi keeps its project resources under .pi/, and exports PI_* variables into every command it runs. Detecting both means running `cratis init` from inside a Pi session configures Pi even on a project that has nothing yet - the same first-run behaviour Claude Code already gets. PI_CODING_AGENT identifies the harness itself, where PI_SESSION_ID only says a session is in flight, so both are accepted and the former stays the signal. --- .../and_pi_detected_from_environment.cs | 37 +++++++++++++++++++ .../and_pi_directory_exists.cs | 29 +++++++++++++++ Source/Cli/Commands/Init/AiTool.cs | 5 +++ Source/Cli/Commands/Init/AiToolDetector.cs | 18 +++++++++ 4 files changed, 89 insertions(+) create mode 100644 Source/Cli.Specs/for_InitCommand/when_detecting_ai_tools/and_pi_detected_from_environment.cs create mode 100644 Source/Cli.Specs/for_InitCommand/when_detecting_ai_tools/and_pi_directory_exists.cs diff --git a/Source/Cli.Specs/for_InitCommand/when_detecting_ai_tools/and_pi_detected_from_environment.cs b/Source/Cli.Specs/for_InitCommand/when_detecting_ai_tools/and_pi_detected_from_environment.cs new file mode 100644 index 0000000..60f9c3f --- /dev/null +++ b/Source/Cli.Specs/for_InitCommand/when_detecting_ai_tools/and_pi_detected_from_environment.cs @@ -0,0 +1,37 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Cli.for_InitCommand.when_detecting_ai_tools; + +/// +/// Pi exports its PI_* variables into every command it runs, which is what lets a first `cratis init` from +/// inside a Pi session configure Pi even though the project carries no .pi directory yet. +/// +[Collection(CliSpecsCollection.Name)] +public class and_pi_detected_from_environment : Specification +{ + string _tempDir; + string? _previousValue; + IReadOnlyList _result; + + void Establish() + { + _tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(_tempDir); + _previousValue = Environment.GetEnvironmentVariable("PI_CODING_AGENT"); + Environment.SetEnvironmentVariable("PI_CODING_AGENT", "1"); + } + + void Because() => _result = AiToolDetector.Detect(_tempDir); + + [Fact] void should_detect_pi_without_project_files() => _result.ShouldContain(AiTool.Pi); + + void Destroy() + { + Environment.SetEnvironmentVariable("PI_CODING_AGENT", _previousValue); + if (Directory.Exists(_tempDir)) + { + Directory.Delete(_tempDir, true); + } + } +} diff --git a/Source/Cli.Specs/for_InitCommand/when_detecting_ai_tools/and_pi_directory_exists.cs b/Source/Cli.Specs/for_InitCommand/when_detecting_ai_tools/and_pi_directory_exists.cs new file mode 100644 index 0000000..114ab61 --- /dev/null +++ b/Source/Cli.Specs/for_InitCommand/when_detecting_ai_tools/and_pi_directory_exists.cs @@ -0,0 +1,29 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Cli.for_InitCommand.when_detecting_ai_tools; + +public class and_pi_directory_exists : Specification +{ + string _tempDir; + IReadOnlyList _result; + + void Establish() + { + _tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(_tempDir); + Directory.CreateDirectory(Path.Combine(_tempDir, ".pi")); + } + + void Because() => _result = AiToolDetector.Detect(_tempDir); + + [Fact] void should_detect_pi() => _result.ShouldContain(AiTool.Pi); + + void Destroy() + { + if (Directory.Exists(_tempDir)) + { + Directory.Delete(_tempDir, true); + } + } +} diff --git a/Source/Cli/Commands/Init/AiTool.cs b/Source/Cli/Commands/Init/AiTool.cs index f1b957e..45f772d 100644 --- a/Source/Cli/Commands/Init/AiTool.cs +++ b/Source/Cli/Commands/Init/AiTool.cs @@ -27,4 +27,9 @@ public enum AiTool /// Windsurf IDE. /// Windsurf = 3, + + /// + /// Pi coding agent. + /// + Pi = 4, } diff --git a/Source/Cli/Commands/Init/AiToolDetector.cs b/Source/Cli/Commands/Init/AiToolDetector.cs index c2ab766..b9261da 100644 --- a/Source/Cli/Commands/Init/AiToolDetector.cs +++ b/Source/Cli/Commands/Init/AiToolDetector.cs @@ -47,6 +47,9 @@ public static bool TryParse(string name, out AiTool tool) case "windsurf": tool = AiTool.Windsurf; return true; + case "pi": + tool = AiTool.Pi; + return true; default: tool = default; return false; @@ -81,6 +84,12 @@ static void DetectFromProjectFiles(string basePath, HashSet tools) { tools.Add(AiTool.Windsurf); } + + // Pi keeps project resources under .pi/ (skills, prompts, extensions, settings). + if (Directory.Exists(Path.Combine(basePath, ".pi"))) + { + tools.Add(AiTool.Pi); + } } /// @@ -120,5 +129,14 @@ static void DetectFromEnvironment(HashSet tools) { tools.Add(AiTool.Windsurf); } + + // Pi exports PI_* variables into every command it runs. PI_CODING_AGENT identifies the harness + // itself, where PI_SESSION_ID and friends only say a session is in flight - so it stays the + // signal even if the session variables are ever narrowed. + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("PI_CODING_AGENT")) || + !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("PI_SESSION_ID"))) + { + tools.Add(AiTool.Pi); + } } } From 419d8c93c55659628104097fe196516f00fd0608 Mon Sep 17 00:00:00 2001 From: woksin Date: Wed, 19 Aug 2026 14:32:07 +0200 Subject: [PATCH 2/4] Write the Chronicle skill where Pi looks for it Pi discovers skills from .pi/skills//SKILL.md and prompts from .pi/prompts, and reads its context from AGENTS.md. The generated skill already carries the name/description frontmatter Pi requires - the same shape Copilot uses - so it is written unchanged and only the path differs. AGENTS.md rather than a Pi-specific file because that is what Pi reads and it is the cross-tool convention: a project already carrying one gets the reference appended rather than a second file to keep in sync. Appending is idempotent, since AGENTS.md is usually hand-maintained. --refresh now updates the Pi skill too, or an upgrade would leave it behind while refreshing the others. --- ..._agents_md_already_references_chronicle.cs | 38 +++++++++ .../and_the_project_has_nothing_yet.cs | 46 +++++++++++ .../Cli/Commands/Init/AiToolConfigurator.cs | 81 +++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 Source/Cli.Specs/for_InitCommand/when_configuring_pi/and_agents_md_already_references_chronicle.cs create mode 100644 Source/Cli.Specs/for_InitCommand/when_configuring_pi/and_the_project_has_nothing_yet.cs diff --git a/Source/Cli.Specs/for_InitCommand/when_configuring_pi/and_agents_md_already_references_chronicle.cs b/Source/Cli.Specs/for_InitCommand/when_configuring_pi/and_agents_md_already_references_chronicle.cs new file mode 100644 index 0000000..a125291 --- /dev/null +++ b/Source/Cli.Specs/for_InitCommand/when_configuring_pi/and_agents_md_already_references_chronicle.cs @@ -0,0 +1,38 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Cli.for_InitCommand.when_configuring_pi; + +/// +/// AGENTS.md is shared with other tools and is frequently hand-maintained, so configuring Pi has to be +/// idempotent against it - running init twice must not stack duplicate references into somebody's file. +/// +public class and_agents_md_already_references_chronicle : Specification +{ + string _tempDir; + string _agentsMd; + + void Establish() + { + _tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(_tempDir); + _agentsMd = Path.Combine(_tempDir, "AGENTS.md"); + File.WriteAllText(_agentsMd, "# House rules\n\n@CHRONICLE.md\n"); + } + + void Because() => AiToolConfigurator.Configure(AiTool.Pi, _tempDir, force: false, includeCommands: false, llmContextJson: "{}"); + + [Fact] void should_not_add_a_second_reference() => + File.ReadAllText(_agentsMd).Split("@CHRONICLE.md").Length.ShouldEqual(2); + + [Fact] void should_leave_the_existing_content_alone() => + File.ReadAllText(_agentsMd).ShouldContain("# House rules"); + + void Destroy() + { + if (Directory.Exists(_tempDir)) + { + Directory.Delete(_tempDir, true); + } + } +} diff --git a/Source/Cli.Specs/for_InitCommand/when_configuring_pi/and_the_project_has_nothing_yet.cs b/Source/Cli.Specs/for_InitCommand/when_configuring_pi/and_the_project_has_nothing_yet.cs new file mode 100644 index 0000000..eee8393 --- /dev/null +++ b/Source/Cli.Specs/for_InitCommand/when_configuring_pi/and_the_project_has_nothing_yet.cs @@ -0,0 +1,46 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Cli.for_InitCommand.when_configuring_pi; + +/// +/// Pi discovers skills from .pi/skills/<name>/SKILL.md and prompts from .pi/prompts, and reads its +/// context from AGENTS.md. Writing anywhere else produces files Pi never loads - which looks like success +/// and delivers nothing. +/// +public class and_the_project_has_nothing_yet : Specification +{ + string _tempDir; + IReadOnlyList _actions; + + void Establish() + { + _tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(_tempDir); + } + + void Because() => _actions = AiToolConfigurator.Configure(AiTool.Pi, _tempDir, force: false, includeCommands: true, llmContextJson: "{}"); + + [Fact] void should_write_the_skill_where_pi_looks_for_it() => + File.Exists(Path.Combine(_tempDir, ".pi", "skills", "chronicle-cli", "SKILL.md")).ShouldBeTrue(); + + [Fact] void should_write_the_diagnose_prompt() => + File.Exists(Path.Combine(_tempDir, ".pi", "prompts", "chronicle-diagnose.md")).ShouldBeTrue(); + + [Fact] void should_reference_chronicle_from_agents_md() => + File.ReadAllText(Path.Combine(_tempDir, "AGENTS.md")).ShouldContain("@CHRONICLE.md"); + + [Fact] void should_give_the_skill_the_frontmatter_pi_requires() => + File.ReadAllText(Path.Combine(_tempDir, ".pi", "skills", "chronicle-cli", "SKILL.md")) + .ShouldContain("name: chronicle-cli"); + + [Fact] void should_report_what_it_did() => _actions.ShouldNotBeEmpty(); + + void Destroy() + { + if (Directory.Exists(_tempDir)) + { + Directory.Delete(_tempDir, true); + } + } +} diff --git a/Source/Cli/Commands/Init/AiToolConfigurator.cs b/Source/Cli/Commands/Init/AiToolConfigurator.cs index 6d0c4f9..0b47e88 100644 --- a/Source/Cli/Commands/Init/AiToolConfigurator.cs +++ b/Source/Cli/Commands/Init/AiToolConfigurator.cs @@ -28,6 +28,7 @@ public static IReadOnlyList Configure(AiTool tool, string basePath, bool AiTool.Copilot => ConfigureCopilot(basePath, force, includeCommands, llmContextJson), AiTool.Cursor => ConfigureCursor(basePath, force), AiTool.Windsurf => ConfigureWindsurf(basePath, force), + AiTool.Pi => ConfigurePi(basePath, force, includeCommands, llmContextJson), _ => [], }; } @@ -58,6 +59,13 @@ public static IReadOnlyList RefreshSkillFiles(string basePath, string ll actions.Add($"Refreshed .claude/commands/{ChronicleSkillGenerator.SkillName}.md"); } + var piSkillPath = Path.Combine(basePath, ".pi", "skills", ChronicleSkillGenerator.SkillName, "SKILL.md"); + if (File.Exists(piSkillPath)) + { + File.WriteAllText(piSkillPath, skillContent); + actions.Add($"Refreshed .pi/skills/{ChronicleSkillGenerator.SkillName}/SKILL.md"); + } + return actions; } @@ -228,4 +236,77 @@ static List ConfigureWindsurf(string basePath, bool force) return actions; } + + /// + /// Configures Pi, whose project resources live under .pi/. + /// + /// + /// The context reference goes in AGENTS.md rather than a Pi-specific file, because that is what + /// Pi reads and because it is the cross-tool convention - a project already carrying one for another + /// agent gets the reference appended rather than a second file to keep in sync. Skills are discovered + /// from .pi/skills/<name>/SKILL.md, which is the same directory-with-frontmatter shape + /// Copilot uses, so the generated skill is written unchanged. + /// + /// The project base directory. + /// Whether to overwrite existing files. + /// Whether to generate the prompt and skill files. + /// The serialized llm-context JSON to embed in the skill file. + /// A list of actions taken. + static List ConfigurePi(string basePath, bool force, bool includeCommands, string llmContextJson) + { + var actions = new List(); + var agentsMd = Path.Combine(basePath, "AGENTS.md"); + + if (File.Exists(agentsMd)) + { + var content = File.ReadAllText(agentsMd); + if (!content.Contains(ChronicleReference, StringComparison.Ordinal)) + { + File.AppendAllText(agentsMd, $"\n{ChronicleReference}\n"); + actions.Add("Appended @CHRONICLE.md reference to AGENTS.md"); + } + else + { + actions.Add("AGENTS.md already references @CHRONICLE.md (skipped)"); + } + } + else + { + File.WriteAllText(agentsMd, $"{ChronicleReference}\n"); + actions.Add("Created AGENTS.md with @CHRONICLE.md reference"); + } + + if (includeCommands) + { + var promptsDir = Path.Combine(basePath, ".pi", "prompts"); + var promptPath = Path.Combine(promptsDir, $"{DiagnoseCommandName}.md"); + + if (!File.Exists(promptPath) || force) + { + Directory.CreateDirectory(promptsDir); + File.WriteAllText(promptPath, SlashCommands.ChronicleDiagnose); + actions.Add($"Created .pi/prompts/{DiagnoseCommandName}.md"); + } + else + { + actions.Add($".pi/prompts/{DiagnoseCommandName}.md already exists (skipped, use --force to overwrite)"); + } + + var skillDir = Path.Combine(basePath, ".pi", "skills", ChronicleSkillGenerator.SkillName); + var skillPath = Path.Combine(skillDir, "SKILL.md"); + + if (!File.Exists(skillPath) || force) + { + Directory.CreateDirectory(skillDir); + File.WriteAllText(skillPath, ChronicleSkillGenerator.Generate(llmContextJson)); + actions.Add($"Created .pi/skills/{ChronicleSkillGenerator.SkillName}/SKILL.md"); + } + else + { + actions.Add($".pi/skills/{ChronicleSkillGenerator.SkillName}/SKILL.md already exists (skipped, use --force to overwrite)"); + } + } + + return actions; + } } From 10ad51d3eaa401fcdf5d4faf7e4a4eaff446a453 Mon Sep 17 00:00:00 2001 From: woksin Date: Wed, 19 Aug 2026 14:32:16 +0200 Subject: [PATCH 3/4] Say when the command catalog was generated by a different CLI The catalog embedded in CHRONICLE.md and the generated skill is a snapshot taken when init ran, not a live lookup. After upgrading the CLI it keeps describing the older surface: commands added since are invisible to an agent, and ones renamed or removed are still advertised. Nothing said so, so the failure was silent - an agent confidently calls a command that is gone. Init already skips an existing CHRONICLE.md, which is right since it may have been edited. It now reads the version out of it first and, when that differs from the running CLI, says so and names --refresh instead of reporting a bare "already exists". A file carrying no version is deliberately not reported as stale: it predates stamping or was hand written, and a warning nobody can act on is how people learn to ignore warnings. --- .../when_deciding_whether_it_is_stale.cs | 26 +++++++ .../when_reading_the_generating_version.cs | 26 +++++++ .../Commands/Init/GeneratedContextVersion.cs | 70 +++++++++++++++++++ Source/Cli/Commands/Init/InitCommand.cs | 19 +++-- 4 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 Source/Cli.Specs/for_GeneratedContextVersion/when_deciding_whether_it_is_stale.cs create mode 100644 Source/Cli.Specs/for_GeneratedContextVersion/when_reading_the_generating_version.cs create mode 100644 Source/Cli/Commands/Init/GeneratedContextVersion.cs diff --git a/Source/Cli.Specs/for_GeneratedContextVersion/when_deciding_whether_it_is_stale.cs b/Source/Cli.Specs/for_GeneratedContextVersion/when_deciding_whether_it_is_stale.cs new file mode 100644 index 0000000..00b52a5 --- /dev/null +++ b/Source/Cli.Specs/for_GeneratedContextVersion/when_deciding_whether_it_is_stale.cs @@ -0,0 +1,26 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Cli.for_GeneratedContextVersion; + +/// +/// A file with no version predates stamping or was hand written, so it is deliberately not reported as +/// stale - a warning nobody can act on is how people learn to ignore warnings. +/// +public class when_deciding_whether_it_is_stale : Specification +{ + [Fact] void should_be_stale_when_generated_by_an_older_cli() => + GeneratedContextVersion.IsStale("2.8.2.0", "2.9.0.0").ShouldBeTrue(); + + [Fact] void should_be_stale_when_generated_by_a_newer_cli() => + GeneratedContextVersion.IsStale("2.9.0.0", "2.8.2.0").ShouldBeTrue(); + + [Fact] void should_not_be_stale_when_the_versions_match() => + GeneratedContextVersion.IsStale("2.8.2.0", "2.8.2.0").ShouldBeFalse(); + + [Fact] void should_not_be_stale_when_the_file_carries_no_version() => + GeneratedContextVersion.IsStale(null, "2.8.2.0").ShouldBeFalse(); + + [Fact] void should_not_be_stale_for_a_blank_version() => + GeneratedContextVersion.IsStale(" ", "2.8.2.0").ShouldBeFalse(); +} diff --git a/Source/Cli.Specs/for_GeneratedContextVersion/when_reading_the_generating_version.cs b/Source/Cli.Specs/for_GeneratedContextVersion/when_reading_the_generating_version.cs new file mode 100644 index 0000000..8d4827d --- /dev/null +++ b/Source/Cli.Specs/for_GeneratedContextVersion/when_reading_the_generating_version.cs @@ -0,0 +1,26 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Cli.for_GeneratedContextVersion; + +/// +/// The two generated files state their version differently - CHRONICLE.md in prose, the skill inside the +/// embedded descriptor - so either has to answer, or a stale catalog goes unnoticed in whichever file the +/// check happened to read. +/// +public class when_reading_the_generating_version : Specification +{ + [Fact] void should_read_it_from_the_chronicle_md_preamble() => + GeneratedContextVersion.ReadFrom("Generated by `cratis init` (CLI v2.8.2.0). Run `cratis init --refresh` to update.") + .ShouldEqual("2.8.2.0"); + + [Fact] void should_read_it_from_an_embedded_descriptor() => + GeneratedContextVersion.ReadFrom("""{ "tool": "cratis", "version": "2.9.0.0" }""") + .ShouldEqual("2.9.0.0"); + + [Fact] void should_answer_nothing_when_no_version_is_present() => + GeneratedContextVersion.ReadFrom("# Some hand written file").ShouldBeNull(); + + [Fact] void should_answer_nothing_for_empty_content() => + GeneratedContextVersion.ReadFrom(string.Empty).ShouldBeNull(); +} diff --git a/Source/Cli/Commands/Init/GeneratedContextVersion.cs b/Source/Cli/Commands/Init/GeneratedContextVersion.cs new file mode 100644 index 0000000..dd18c81 --- /dev/null +++ b/Source/Cli/Commands/Init/GeneratedContextVersion.cs @@ -0,0 +1,70 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Text.RegularExpressions; + +namespace Cratis.Cli.Commands.Init; + +/// +/// Reads the CLI version that generated the files cratis init wrote, so a stale snapshot can be +/// reported rather than silently believed. +/// +/// +/// The command catalog embedded in CHRONICLE.md and the generated skill is a snapshot taken when +/// cratis init ran, not something resolved live. After a CLI upgrade it keeps describing the older +/// surface: commands that have since been added are invisible to an agent, and ones that were renamed or +/// removed are still advertised. Nothing in the file says it is out of date, so the failure is silent and +/// an agent confidently writes a command that no longer exists. +/// +public static partial class GeneratedContextVersion +{ + /// + /// The version reported when a generated file carries no recognizable version at all. + /// + public const string Unknown = "unknown"; + + [GeneratedRegex(@"CLI v(?[0-9]+(?:\.[0-9]+)+)", RegexOptions.ExplicitCapture, 1000)] + private static partial Regex ProseVersion { get; } + + [GeneratedRegex(@"""version""\s*:\s*""(?[0-9]+(?:\.[0-9]+)+)""", RegexOptions.ExplicitCapture, 1000)] + private static partial Regex DescriptorVersion { get; } + + /// + /// Reads the generating CLI version out of the content of a file cratis init produced. + /// + /// The file content to read. + /// The version string, or when the content carries none. + public static string? ReadFrom(string content) + { + if (string.IsNullOrWhiteSpace(content)) + { + return null; + } + + // CHRONICLE.md states it in prose ("CLI v2.8.2.0"); the skill carries it inside the embedded + // descriptor ("version": "2.8.2.0"). Both are matched so either file can answer. + var prose = ProseVersion.Match(content); + if (prose.Success) + { + return prose.Groups["version"].Value; + } + + var json = DescriptorVersion.Match(content); + return json.Success ? json.Groups["version"].Value : null; + } + + /// + /// Decides whether a generated file is stale relative to the running CLI. + /// + /// The version read from the file, or when absent. + /// The version of the running CLI. + /// when the file was generated by a different version. + /// + /// A file carrying no version is deliberately not reported as stale. It predates version + /// stamping or was hand-written, and warning about something the user cannot act on teaches them to + /// ignore the warning - which costs more than the case it catches. + /// + public static bool IsStale(string? generatedWith, string running) => + !string.IsNullOrWhiteSpace(generatedWith) && + !string.Equals(generatedWith, running, StringComparison.Ordinal); +} diff --git a/Source/Cli/Commands/Init/InitCommand.cs b/Source/Cli/Commands/Init/InitCommand.cs index 12d255f..b57a358 100644 --- a/Source/Cli/Commands/Init/InitCommand.cs +++ b/Source/Cli/Commands/Init/InitCommand.cs @@ -8,17 +8,19 @@ namespace Cratis.Cli.Commands.Init; /// /// Generates CHRONICLE.md and configures AI tools for the current project directory. /// -[LlmDescription("Generates a CHRONICLE.md documentation file and configures AI tools (Claude Code, GitHub Copilot, Cursor, Windsurf) for the current project. Run once per project.")] +[LlmDescription("Generates a CHRONICLE.md documentation file and configures AI tools (Claude Code, GitHub Copilot, Cursor, Windsurf, Pi) for the current project. Run once per project, and again with --refresh after upgrading the CLI.")] [CliCommand("init", "Generate CHRONICLE.md and configure AI tools for the current project")] [CliExample("init")] [CliExample("init", "--tool", "claude")] [CliExample("init", "--force", "--no-commands")] [LlmOption("--force", "bool", "Overwrite existing files")] -[LlmOption("--tool", "string", "Target a specific AI tool: claude, copilot, cursor, windsurf. Omit to auto-detect.")] +[LlmOption("--tool", "string", "Target a specific AI tool: claude, copilot, cursor, windsurf, pi. Omit to auto-detect.")] [LlmOption("--no-commands", "bool", "Skip generating slash commands / prompt files")] [LlmOption("--refresh", "bool", "Re-capture the llm-context snapshot in CHRONICLE.md without reconfiguring AI tool integrations.")] public class InitCommand : AsyncCommand { + static string RunningVersion => typeof(Program).Assembly.GetName().Version?.ToString() ?? "0.0.0"; + /// protected override async Task ExecuteAsync(CommandContext context, InitSettings settings, CancellationToken cancellationToken) { @@ -53,7 +55,16 @@ protected override async Task ExecuteAsync(CommandContext context, InitSett if (File.Exists(chronicleMdPath) && !settings.Force) { - allActions.Add("CHRONICLE.md already exists (skipped, use --force to overwrite)"); + // Skipping an existing file is right - it may have been edited - but staying silent about a + // catalog generated by an older CLI is not: it still describes that CLI's commands, and an + // agent reading it has no way to know. Say so, and name the command that fixes it. + var existing = await File.ReadAllTextAsync(chronicleMdPath, cancellationToken); + var generatedWith = GeneratedContextVersion.ReadFrom(existing); + + allActions.Add(GeneratedContextVersion.IsStale(generatedWith, RunningVersion) + ? $"CHRONICLE.md was generated by CLI v{generatedWith} but this is v{RunningVersion} - run 'cratis init --refresh' to update the command catalog" + : "CHRONICLE.md already exists (skipped, use --force to overwrite)"); + llmJson = LlmContextCommand.BuildDescriptorJson(); } else @@ -72,7 +83,7 @@ protected override async Task ExecuteAsync(CommandContext context, InitSett { if (!AiToolDetector.TryParse(settings.Tool, out var tool)) { - OutputFormatter.WriteError(format, $"Unknown AI tool: '{settings.Tool}'", "Valid tools: claude, copilot, cursor, windsurf", ExitCodes.ValidationErrorCode); + OutputFormatter.WriteError(format, $"Unknown AI tool: '{settings.Tool}'", "Valid tools: claude, copilot, cursor, windsurf, pi", ExitCodes.ValidationErrorCode); return ExitCodes.ValidationError; } From bf1c32b61234456cdcf774814b9cfd98189148e7 Mon Sep 17 00:00:00 2001 From: woksin Date: Wed, 19 Aug 2026 14:32:21 +0200 Subject: [PATCH 4/4] Document Pi and the snapshot's staleness --tool now accepts pi, so its help has to say so or the option is undiscoverable. The docs also stated the catalog was refreshable without saying why anyone would need to, which is the part that matters. --- Documentation/getting-started/index.mdx | 6 ++++-- README.md | 17 ++++++++++++----- Source/Cli/Commands/Init/InitSettings.cs | 2 +- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Documentation/getting-started/index.mdx b/Documentation/getting-started/index.mdx index 0b18449..eea503b 100644 --- a/Documentation/getting-started/index.mdx +++ b/Documentation/getting-started/index.mdx @@ -146,13 +146,15 @@ cratis completions install It detects your shell automatically (override with `--shell bash|zsh|fish|powershell`); on Windows it writes the hook to your PowerShell `$PROFILE`. Restart your shell to pick it up. -**Teach your AI assistant about your store.** Run `cratis init` inside a project and the CLI writes a `CHRONICLE.md` describing every command it can run, installs instruction files for Claude Code, GitHub Copilot, Cursor, and Windsurf, and adds a `chronicle-diagnose` slash command — so your assistant can help operate the store too: +**Teach your AI assistant about your store.** Run `cratis init` inside a project and the CLI writes a `CHRONICLE.md` describing every command it can run, installs instruction files for Claude Code, GitHub Copilot, Cursor, Windsurf, and Pi, and adds a `chronicle-diagnose` slash command — so your assistant can help operate the store too: ```bash title="Set up AI tooling" cratis init ``` -Refresh the embedded snapshot after a CLI upgrade with `cratis init --refresh`. For the same catalog as raw JSON, run `cratis llm-context` (add `--schema` for its JSON Schema). +Detection uses both the files a project already has and the environment variables each tool exports, so running `cratis init` from inside an assistant's own terminal configures that assistant even before the project has any of its files. + +The catalog is a snapshot taken when `init` runs. After upgrading the CLI it still describes the surface it was generated from, so `cratis init` reports the mismatch and points at `cratis init --refresh`, which re-captures it. For the same catalog as raw JSON, run `cratis llm-context` (add `--schema` for its JSON Schema). ## Recap diff --git a/README.md b/README.md index 493f2ee..64d38b9 100644 --- a/README.md +++ b/README.md @@ -332,13 +332,20 @@ rather than making it the default. That catalog is the other half: ```bash -cratis init # writes CHRONICLE.md, wires up Claude / Copilot / Cursor / Windsurf -cratis llm-context # the whole command surface as JSON, ~50 KB +cratis init # writes CHRONICLE.md, wires up Claude / Copilot / Cursor / Windsurf / Pi +cratis init --refresh # re-capture the catalog after upgrading the CLI +cratis llm-context # the whole command surface as JSON, ~50 KB ``` -`init` detects which tools a project already uses rather than assuming, and `llm-context` -carries per-command descriptions, options, examples and output-format advice — so an agent -can work out that a stuck observer means `failed-partitions show` without being told. +`init` detects which tools a project already uses rather than assuming — from its files, and +from the environment variables the tool exports, so running it inside an agent's own terminal +configures that agent even on a project that has nothing yet. `llm-context` carries per-command +descriptions, options, examples and output-format advice — so an agent can work out that a stuck +observer means `failed-partitions show` without being told. + +The catalog `init` writes is a snapshot, not a live lookup. After upgrading the CLI it still +describes the surface it was generated from, so `init` says so and names `--refresh` as the fix +rather than leaving an agent to confidently call a command that has since changed. ## Tab completion asks the server diff --git a/Source/Cli/Commands/Init/InitSettings.cs b/Source/Cli/Commands/Init/InitSettings.cs index 571615d..81a43a5 100644 --- a/Source/Cli/Commands/Init/InitSettings.cs +++ b/Source/Cli/Commands/Init/InitSettings.cs @@ -20,7 +20,7 @@ public class InitSettings : GlobalSettings /// Gets or sets the specific AI tool to configure. Omit to auto-detect. /// [CommandOption("--tool ")] - [Description("Target a specific AI tool: claude, copilot, cursor, windsurf. Omit to auto-detect.")] + [Description("Target a specific AI tool: claude, copilot, cursor, windsurf, pi. Omit to auto-detect.")] public string? Tool { get; set; } ///