diff --git a/Documentation/getting-started/index.mdx b/Documentation/getting-started/index.mdx
index eea503b..ac7b261 100644
--- a/Documentation/getting-started/index.mdx
+++ b/Documentation/getting-started/index.mdx
@@ -154,6 +154,8 @@ cratis init
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.
+If the instruction file it would edit is generated from a shared corpus and propagated across repositories, pass `--no-context`. The skill and prompt files are still written; only the `@CHRONICLE.md` reference is left out, and `init` reports where to add it instead — editing a generated file would work until the next sync silently removed it.
+
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 64d38b9..b5c3489 100644
--- a/README.md
+++ b/README.md
@@ -347,6 +347,11 @@ The catalog `init` writes is a snapshot, not a live lookup. After upgrading the
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.
+If your instruction file (`AGENTS.md`, `CLAUDE.md`, `.github/copilot-instructions.md`) is generated
+from a shared corpus and propagated, pass `--no-context`: the skill and prompt are still written,
+and `init` tells you to add the `@CHRONICLE.md` line to whatever generates that file instead of
+editing a file the next sync will overwrite.
+
## Tab completion asks the server
`cratis completions install` writes a completion script for bash, zsh, fish or PowerShell.
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
index a125291..2305c40 100644
--- 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
@@ -20,7 +20,7 @@ void Establish()
File.WriteAllText(_agentsMd, "# House rules\n\n@CHRONICLE.md\n");
}
- void Because() => AiToolConfigurator.Configure(AiTool.Pi, _tempDir, force: false, includeCommands: false, llmContextJson: "{}");
+ void Because() => AiToolConfigurator.Configure(AiTool.Pi, _tempDir, new(Force: false, IncludeCommands: false, IncludeContext: true, LlmContextJson: "{}"));
[Fact] void should_not_add_a_second_reference() =>
File.ReadAllText(_agentsMd).Split("@CHRONICLE.md").Length.ShouldEqual(2);
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
index eee8393..27c93b9 100644
--- 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
@@ -19,7 +19,7 @@ void Establish()
Directory.CreateDirectory(_tempDir);
}
- void Because() => _actions = AiToolConfigurator.Configure(AiTool.Pi, _tempDir, force: false, includeCommands: true, llmContextJson: "{}");
+ void Because() => _actions = AiToolConfigurator.Configure(AiTool.Pi, _tempDir, new(Force: false, IncludeCommands: true, IncludeContext: 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();
diff --git a/Source/Cli.Specs/for_InitCommand/when_the_context_file_is_generated/and_configuring_pi.cs b/Source/Cli.Specs/for_InitCommand/when_the_context_file_is_generated/and_configuring_pi.cs
new file mode 100644
index 0000000..e678a92
--- /dev/null
+++ b/Source/Cli.Specs/for_InitCommand/when_the_context_file_is_generated/and_configuring_pi.cs
@@ -0,0 +1,53 @@
+// 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_the_context_file_is_generated;
+
+///
+/// Some repositories generate their instruction files from a shared corpus and propagate them, so appending
+/// to one is undone by the next sync - the reference disappears at an unpredictable later moment, which is
+/// worse than never adding it. The skill and prompt are unaffected and still written.
+///
+/// The skip is reported rather than silent, because a project that looks configured and loads nothing is
+/// the harder failure to notice.
+///
+///
+public class and_configuring_pi : Specification
+{
+ string _tempDir;
+ string _agentsMd;
+ IReadOnlyList _actions;
+
+ void Establish()
+ {
+ _tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(_tempDir);
+ _agentsMd = Path.Combine(_tempDir, "AGENTS.md");
+ File.WriteAllText(_agentsMd, "# Generated - do not edit\n");
+ }
+
+ void Because() => _actions = AiToolConfigurator.Configure(
+ AiTool.Pi,
+ _tempDir,
+ new(Force: false, IncludeCommands: true, IncludeContext: false, LlmContextJson: "{}"));
+
+ [Fact] void should_leave_the_generated_file_untouched() =>
+ File.ReadAllText(_agentsMd).ShouldEqual("# Generated - do not edit\n");
+
+ [Fact] void should_still_write_the_skill() =>
+ File.Exists(Path.Combine(_tempDir, ".pi", "skills", "chronicle-cli", "SKILL.md")).ShouldBeTrue();
+
+ [Fact] void should_still_write_the_prompt() =>
+ File.Exists(Path.Combine(_tempDir, ".pi", "prompts", "chronicle-diagnose.md")).ShouldBeTrue();
+
+ [Fact] void should_say_it_skipped_the_reference() =>
+ _actions.ShouldContain(_ => _.Contains("Skipped the @CHRONICLE.md reference in AGENTS.md", StringComparison.Ordinal));
+
+ void Destroy()
+ {
+ if (Directory.Exists(_tempDir))
+ {
+ Directory.Delete(_tempDir, true);
+ }
+ }
+}
diff --git a/Source/Cli.Specs/for_InitCommand/when_the_context_file_is_generated/and_the_tool_only_writes_a_context_file.cs b/Source/Cli.Specs/for_InitCommand/when_the_context_file_is_generated/and_the_tool_only_writes_a_context_file.cs
new file mode 100644
index 0000000..f880e21
--- /dev/null
+++ b/Source/Cli.Specs/for_InitCommand/when_the_context_file_is_generated/and_the_tool_only_writes_a_context_file.cs
@@ -0,0 +1,48 @@
+// 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_the_context_file_is_generated;
+
+///
+/// Cursor and Windsurf are configured entirely through a rules file, so --no-context leaves them with
+/// nothing to write. Reporting that is the whole value: silently doing nothing reads as success.
+///
+public class and_the_tool_only_writes_a_context_file : Specification
+{
+ string _tempDir;
+ IReadOnlyList _cursor;
+ IReadOnlyList _windsurf;
+
+ void Establish()
+ {
+ _tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(_tempDir);
+ }
+
+ void Because()
+ {
+ var configuration = new AiToolConfiguration(Force: true, IncludeCommands: true, IncludeContext: false, LlmContextJson: "{}");
+ _cursor = AiToolConfigurator.Configure(AiTool.Cursor, _tempDir, configuration);
+ _windsurf = AiToolConfigurator.Configure(AiTool.Windsurf, _tempDir, configuration);
+ }
+
+ [Fact] void should_not_write_the_cursor_rule() =>
+ File.Exists(Path.Combine(_tempDir, ".cursor", "rules", "chronicle.mdc")).ShouldBeFalse();
+
+ [Fact] void should_not_write_the_windsurf_rules() =>
+ File.Exists(Path.Combine(_tempDir, ".windsurfrules")).ShouldBeFalse();
+
+ [Fact] void should_say_why_cursor_got_nothing() =>
+ _cursor.ShouldContain(_ => _.Contains("Skipped the @CHRONICLE.md reference", StringComparison.Ordinal));
+
+ [Fact] void should_say_why_windsurf_got_nothing() =>
+ _windsurf.ShouldContain(_ => _.Contains("Skipped the @CHRONICLE.md reference", StringComparison.Ordinal));
+
+ void Destroy()
+ {
+ if (Directory.Exists(_tempDir))
+ {
+ Directory.Delete(_tempDir, true);
+ }
+ }
+}
diff --git a/Source/Cli/Commands/Init/AiToolConfiguration.cs b/Source/Cli/Commands/Init/AiToolConfiguration.cs
new file mode 100644
index 0000000..830dc61
--- /dev/null
+++ b/Source/Cli/Commands/Init/AiToolConfiguration.cs
@@ -0,0 +1,24 @@
+// 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.Commands.Init;
+
+///
+/// What cratis init should write for one AI tool.
+///
+///
+/// A record rather than a parameter list because the three switches all read as bare booleans at the call
+/// site, where transposing two of them produces a configuration that looks configured and is not.
+///
+/// Whether to overwrite files that already exist.
+/// Whether to write the skill and slash-command/prompt files.
+///
+/// Whether to add the @CHRONICLE.md reference to the tool's instruction file. False when that file is
+/// generated from a shared corpus, where the edit would be overwritten on the next sync.
+///
+/// The serialized llm-context JSON to embed in skill files.
+public record AiToolConfiguration(
+ bool Force,
+ bool IncludeCommands,
+ bool IncludeContext,
+ string LlmContextJson);
diff --git a/Source/Cli/Commands/Init/AiToolConfigurator.cs b/Source/Cli/Commands/Init/AiToolConfigurator.cs
index 0b47e88..6fcd2a4 100644
--- a/Source/Cli/Commands/Init/AiToolConfigurator.cs
+++ b/Source/Cli/Commands/Init/AiToolConfigurator.cs
@@ -16,19 +16,17 @@ public static class AiToolConfigurator
///
/// The AI tool to configure.
/// The project base directory.
- /// Whether to overwrite existing files.
- /// Whether to generate slash command files.
- /// The serialized llm-context JSON to embed in skill files.
+ /// What to write for this tool.
/// A list of actions taken.
- public static IReadOnlyList Configure(AiTool tool, string basePath, bool force, bool includeCommands, string llmContextJson)
+ public static IReadOnlyList Configure(AiTool tool, string basePath, AiToolConfiguration configuration)
{
return tool switch
{
- AiTool.Claude => ConfigureClaude(basePath, force, includeCommands, llmContextJson),
- AiTool.Copilot => ConfigureCopilot(basePath, force, includeCommands, llmContextJson),
- AiTool.Cursor => ConfigureCursor(basePath, force),
- AiTool.Windsurf => ConfigureWindsurf(basePath, force),
- AiTool.Pi => ConfigurePi(basePath, force, includeCommands, llmContextJson),
+ AiTool.Claude => ConfigureClaude(basePath, configuration),
+ AiTool.Copilot => ConfigureCopilot(basePath, configuration),
+ AiTool.Cursor => ConfigureCursor(basePath, configuration),
+ AiTool.Windsurf => ConfigureWindsurf(basePath, configuration),
+ AiTool.Pi => ConfigurePi(basePath, configuration),
_ => [],
};
}
@@ -69,12 +67,29 @@ public static IReadOnlyList RefreshSkillFiles(string basePath, string ll
return actions;
}
- static List ConfigureClaude(string basePath, bool force, bool includeCommands, string llmContextJson)
+ ///
+ /// Reports a context reference that was deliberately not written, and says what to do instead.
+ ///
+ ///
+ /// Skipping silently would leave a project that looks configured and loads nothing - the skill is on
+ /// disk but no instruction file points at CHRONICLE.md, so an agent never reads it. Naming the
+ /// file and the line to add turns the skip into an instruction rather than an omission.
+ ///
+ /// The instruction file that was left alone.
+ /// The action to report.
+ static string SkippedContext(string file) =>
+ $"Skipped the @CHRONICLE.md reference in {file} (--no-context) - add it to whatever generates that file, or the catalog is written but never loaded";
+
+ static List ConfigureClaude(string basePath, AiToolConfiguration configuration)
{
var actions = new List();
var claudeMd = Path.Combine(basePath, "CLAUDE.md");
- if (File.Exists(claudeMd))
+ if (!configuration.IncludeContext)
+ {
+ actions.Add(SkippedContext("CLAUDE.md"));
+ }
+ else if (File.Exists(claudeMd))
{
var content = File.ReadAllText(claudeMd);
if (!content.Contains(ChronicleReference, StringComparison.Ordinal))
@@ -93,12 +108,12 @@ static List ConfigureClaude(string basePath, bool force, bool includeCom
actions.Add("Created CLAUDE.md with @CHRONICLE.md reference");
}
- if (includeCommands)
+ if (configuration.IncludeCommands)
{
var commandsDir = Path.Combine(basePath, ".claude", "commands");
var commandPath = Path.Combine(commandsDir, $"{DiagnoseCommandName}.md");
- if (!File.Exists(commandPath) || force)
+ if (!File.Exists(commandPath) || configuration.Force)
{
Directory.CreateDirectory(commandsDir);
File.WriteAllText(commandPath, SlashCommands.ChronicleDiagnose);
@@ -111,10 +126,10 @@ static List ConfigureClaude(string basePath, bool force, bool includeCom
var skillPath = Path.Combine(commandsDir, $"{ChronicleSkillGenerator.SkillName}.md");
- if (!File.Exists(skillPath) || force)
+ if (!File.Exists(skillPath) || configuration.Force)
{
Directory.CreateDirectory(commandsDir);
- File.WriteAllText(skillPath, ChronicleSkillGenerator.Generate(llmContextJson));
+ File.WriteAllText(skillPath, ChronicleSkillGenerator.Generate(configuration.LlmContextJson));
actions.Add($"Created .claude/commands/{ChronicleSkillGenerator.SkillName}.md");
}
else
@@ -126,12 +141,16 @@ static List ConfigureClaude(string basePath, bool force, bool includeCom
return actions;
}
- static List ConfigureCopilot(string basePath, bool force, bool includeCommands, string llmContextJson)
+ static List ConfigureCopilot(string basePath, AiToolConfiguration configuration)
{
var actions = new List();
var instructionsPath = Path.Combine(basePath, ".github", "copilot-instructions.md");
- if (File.Exists(instructionsPath))
+ if (!configuration.IncludeContext)
+ {
+ actions.Add(SkippedContext(".github/copilot-instructions.md"));
+ }
+ else if (File.Exists(instructionsPath))
{
var content = File.ReadAllText(instructionsPath);
if (!content.Contains(ChronicleReference, StringComparison.Ordinal))
@@ -152,12 +171,12 @@ static List ConfigureCopilot(string basePath, bool force, bool includeCo
actions.Add("Created .github/copilot-instructions.md with @CHRONICLE.md reference");
}
- if (includeCommands)
+ if (configuration.IncludeCommands)
{
var promptsDir = Path.Combine(basePath, ".github", "copilot", "prompts");
var promptPath = Path.Combine(promptsDir, $"{DiagnoseCommandName}.prompt.md");
- if (!File.Exists(promptPath) || force)
+ if (!File.Exists(promptPath) || configuration.Force)
{
Directory.CreateDirectory(promptsDir);
File.WriteAllText(promptPath, SlashCommands.ChronicleDiagnose);
@@ -171,10 +190,10 @@ static List ConfigureCopilot(string basePath, bool force, bool includeCo
var skillDir = Path.Combine(basePath, ".github", "skills", ChronicleSkillGenerator.SkillName);
var skillPath = Path.Combine(skillDir, "SKILL.md");
- if (!File.Exists(skillPath) || force)
+ if (!File.Exists(skillPath) || configuration.Force)
{
Directory.CreateDirectory(skillDir);
- File.WriteAllText(skillPath, ChronicleSkillGenerator.Generate(llmContextJson));
+ File.WriteAllText(skillPath, ChronicleSkillGenerator.Generate(configuration.LlmContextJson));
actions.Add($"Created .github/skills/{ChronicleSkillGenerator.SkillName}/SKILL.md");
}
else
@@ -186,13 +205,19 @@ static List ConfigureCopilot(string basePath, bool force, bool includeCo
return actions;
}
- static List ConfigureCursor(string basePath, bool force)
+ static List ConfigureCursor(string basePath, AiToolConfiguration configuration)
{
var actions = new List();
+
+ if (!configuration.IncludeContext)
+ {
+ return [SkippedContext(".cursor/rules/chronicle.mdc")];
+ }
+
var rulesDir = Path.Combine(basePath, ".cursor", "rules");
var rulePath = Path.Combine(rulesDir, "chronicle.mdc");
- if (!File.Exists(rulePath) || force)
+ if (!File.Exists(rulePath) || configuration.Force)
{
Directory.CreateDirectory(rulesDir);
File.WriteAllText(rulePath, $"{ChronicleReference}\n");
@@ -206,9 +231,15 @@ static List ConfigureCursor(string basePath, bool force)
return actions;
}
- static List ConfigureWindsurf(string basePath, bool force)
+ static List ConfigureWindsurf(string basePath, AiToolConfiguration configuration)
{
var actions = new List();
+
+ if (!configuration.IncludeContext)
+ {
+ return [SkippedContext(".windsurfrules")];
+ }
+
var rulesPath = Path.Combine(basePath, ".windsurfrules");
if (File.Exists(rulesPath))
@@ -224,7 +255,7 @@ static List ConfigureWindsurf(string basePath, bool force)
actions.Add(".windsurfrules already references @CHRONICLE.md (skipped)");
}
}
- else if (force)
+ else if (configuration.Force)
{
File.WriteAllText(rulesPath, $"{ChronicleReference}\n");
actions.Add("Created .windsurfrules with @CHRONICLE.md reference");
@@ -248,16 +279,18 @@ static List ConfigureWindsurf(string basePath, bool force)
/// 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.
+ /// What to write for Pi.
/// A list of actions taken.
- static List ConfigurePi(string basePath, bool force, bool includeCommands, string llmContextJson)
+ static List ConfigurePi(string basePath, AiToolConfiguration configuration)
{
var actions = new List();
var agentsMd = Path.Combine(basePath, "AGENTS.md");
- if (File.Exists(agentsMd))
+ if (!configuration.IncludeContext)
+ {
+ actions.Add(SkippedContext("AGENTS.md"));
+ }
+ else if (File.Exists(agentsMd))
{
var content = File.ReadAllText(agentsMd);
if (!content.Contains(ChronicleReference, StringComparison.Ordinal))
@@ -276,12 +309,12 @@ static List ConfigurePi(string basePath, bool force, bool includeCommand
actions.Add("Created AGENTS.md with @CHRONICLE.md reference");
}
- if (includeCommands)
+ if (configuration.IncludeCommands)
{
var promptsDir = Path.Combine(basePath, ".pi", "prompts");
var promptPath = Path.Combine(promptsDir, $"{DiagnoseCommandName}.md");
- if (!File.Exists(promptPath) || force)
+ if (!File.Exists(promptPath) || configuration.Force)
{
Directory.CreateDirectory(promptsDir);
File.WriteAllText(promptPath, SlashCommands.ChronicleDiagnose);
@@ -295,10 +328,10 @@ static List ConfigurePi(string basePath, bool force, bool includeCommand
var skillDir = Path.Combine(basePath, ".pi", "skills", ChronicleSkillGenerator.SkillName);
var skillPath = Path.Combine(skillDir, "SKILL.md");
- if (!File.Exists(skillPath) || force)
+ if (!File.Exists(skillPath) || configuration.Force)
{
Directory.CreateDirectory(skillDir);
- File.WriteAllText(skillPath, ChronicleSkillGenerator.Generate(llmContextJson));
+ File.WriteAllText(skillPath, ChronicleSkillGenerator.Generate(configuration.LlmContextJson));
actions.Add($"Created .pi/skills/{ChronicleSkillGenerator.SkillName}/SKILL.md");
}
else
diff --git a/Source/Cli/Commands/Init/InitCommand.cs b/Source/Cli/Commands/Init/InitCommand.cs
index b57a358..4fbc74c 100644
--- a/Source/Cli/Commands/Init/InitCommand.cs
+++ b/Source/Cli/Commands/Init/InitCommand.cs
@@ -16,6 +16,7 @@ namespace Cratis.Cli.Commands.Init;
[LlmOption("--force", "bool", "Overwrite existing files")]
[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("--no-context", "bool", "Skip adding the @CHRONICLE.md reference to the tool's instruction file. Use when that file is generated from a shared corpus and the edit would be overwritten.")]
[LlmOption("--refresh", "bool", "Re-capture the llm-context snapshot in CHRONICLE.md without reconfiguring AI tool integrations.")]
public class InitCommand : AsyncCommand
{
@@ -101,11 +102,15 @@ protected override async Task ExecuteAsync(CommandContext context, InitSett
}
else
{
- var includeCommands = !settings.NoCommands;
+ var configuration = new AiToolConfiguration(
+ Force: settings.Force,
+ IncludeCommands: !settings.NoCommands,
+ IncludeContext: !settings.NoContext,
+ LlmContextJson: llmJson);
+
foreach (var tool in tools)
{
- var actions = AiToolConfigurator.Configure(tool, basePath, settings.Force, includeCommands, llmJson);
- allActions.AddRange(actions);
+ allActions.AddRange(AiToolConfigurator.Configure(tool, basePath, configuration));
}
}
diff --git a/Source/Cli/Commands/Init/InitSettings.cs b/Source/Cli/Commands/Init/InitSettings.cs
index 81a43a5..bfbc8df 100644
--- a/Source/Cli/Commands/Init/InitSettings.cs
+++ b/Source/Cli/Commands/Init/InitSettings.cs
@@ -39,4 +39,13 @@ public class InitSettings : GlobalSettings
[Description("Re-capture the llm-context snapshot in CHRONICLE.md. Skips AI tool configuration.")]
[DefaultValue(false)]
public bool Refresh { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the context reference should be left out of the tool's
+ /// instruction file.
+ ///
+ [CommandOption("--no-context")]
+ [Description("Skip adding the @CHRONICLE.md reference to the tool's instruction file. Use when that file is generated from a shared corpus and would overwrite the edit.")]
+ [DefaultValue(false)]
+ public bool NoContext { get; set; }
}