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.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.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.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/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;
+ }
}
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);
+ }
}
}
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;
}
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; }
///