Repository navigation
Add the Grok harness with its hooks and setup #1422
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
5275a17
Add the Grok harness with its hooks and setup (#1421)
realtonyyoung e14ecfe
Ship short Grok sessions and keep refused ones unrecorded (#1421)
realtonyyoung b0d90d0
Refresh Grok hooks on upgrade and launch Grok for the setup handoff (…
realtonyyoung c00f6dc
Import past Grok sessions and their subagents (#1421)
realtonyyoung 2af56f0
Attach subagents when re-importing a watched Grok session (#1421)
realtonyyoung e26daaa
Match Grok's import watermark to the lines the server maps (#1421)
realtonyyoung 23e7d9c
Treat a zero-usage Grok turn as unmapped when importing (#1421)
realtonyyoung da8912d
Accept only a session id as a Grok subagent's child id (#1421)
realtonyyoung File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| namespace Capacitor.Cli.Core.Harness.Grok; | ||
|
|
||
| /// <summary>Grok Build as this process sees it.</summary> | ||
| public sealed class GrokHarness : IHarness<GrokHarness> { | ||
| GrokHarness(GrokPaths paths) => Paths = paths; | ||
|
|
||
| /// <summary>Resolves Grok's one override, <c>GROK_HOME</c>, which replaces <c>~/.grok</c>.</summary> | ||
| public static GrokHarness FromEnvironment(UserHome home) => | ||
| Over(new(home, Environment.GetEnvironmentVariable("GROK_HOME"))); | ||
|
|
||
| public static GrokHarness Over(GrokPaths paths) => new(paths); | ||
|
|
||
| public static HarnessId Id => HarnessId.Grok; | ||
| public static string Label => "Grok"; | ||
| public static string CliBinary => "grok"; | ||
|
|
||
| public GrokPaths Paths { get; } | ||
|
|
||
| public HarnessSignals Signals => new() { | ||
| LaunchSignal = probe => probe.Finds(CliBinary), | ||
| UserDataSignal = Paths.HasUserData, | ||
| Wired = () => GrokHooksInstaller.IsInstalled(Paths.KcapHooksJson), | ||
| }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| using System.Text.Json; | ||
| using System.Text.Json.Nodes; | ||
|
|
||
| namespace Capacitor.Cli.Core.Harness.Grok; | ||
|
|
||
| /// <summary> | ||
| /// kcap's user-level Grok hook file. User-level hooks need no project trust. A handler may carry only | ||
| /// Grok's own fields: it refuses a file with Claude-only ones. | ||
| /// </summary> | ||
| public static class GrokHooksInstaller { | ||
| public const string HookCommand = "kcap hook --grok"; | ||
| public const string MarkerFileName = ".kcap-hooks-version"; | ||
|
|
||
| /// <summary>Start records the session and starts the watcher; Stop restarts a watcher that died. The | ||
| /// watcher ends the session once Grok exits, which a 1.5 s session-end hook could not wait for.</summary> | ||
| public static readonly string[] Events = ["SessionStart", "Stop"]; | ||
|
|
||
| /// <summary>Seconds; Grok's default of 5 is too short for the session-start post.</summary> | ||
| const int TimeoutSeconds = 30; | ||
|
|
||
| static readonly JsonSerializerOptions WriteOptions = new() { WriteIndented = true }; | ||
|
|
||
| public static bool Install(string hooksPath) { | ||
| try { | ||
| var hooks = new JsonObject(); | ||
|
|
||
| foreach (var evt in Events) { | ||
| hooks[evt] = new JsonArray(new JsonObject { | ||
| ["hooks"] = new JsonArray(new JsonObject { | ||
| ["type"] = "command", | ||
| ["command"] = HookCommand, | ||
| ["timeout"] = TimeoutSeconds, | ||
| }), | ||
| }); | ||
| } | ||
|
|
||
| Directory.CreateDirectory(Path.GetDirectoryName(hooksPath)!); | ||
| File.WriteAllText(hooksPath, new JsonObject { ["hooks"] = hooks }.ToJsonString(WriteOptions)); | ||
| File.WriteAllText(MarkerPath(hooksPath), CapacitorVersion.Current()); | ||
|
|
||
| return true; | ||
| } catch (Exception e) when (e is IOException or UnauthorizedAccessException) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /// <summary>True when the file existed. kcap owns it whole, so nothing in it is the user's.</summary> | ||
| public static bool Remove(string hooksPath) { | ||
| var existed = File.Exists(hooksPath); | ||
|
|
||
| if (existed) File.Delete(hooksPath); | ||
| if (File.Exists(MarkerPath(hooksPath))) File.Delete(MarkerPath(hooksPath)); | ||
|
|
||
| return existed; | ||
| } | ||
|
|
||
| public static bool IsInstalled(string hooksPath) { | ||
| if (!File.Exists(hooksPath)) return false; | ||
|
|
||
| try { | ||
| return JsonNode.Parse(File.ReadAllText(hooksPath)) is JsonObject { } root | ||
| && root["hooks"] is JsonObject hooks | ||
| && hooks.Any(h => h.Value?.ToJsonString().Contains(HookCommand, StringComparison.Ordinal) == true); | ||
| } catch (Exception e) when (e is JsonException or IOException or UnauthorizedAccessException) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| static string MarkerPath(string hooksPath) => Path.Combine(Path.GetDirectoryName(hooksPath)!, MarkerFileName); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| using System.Text; | ||
|
|
||
| namespace Capacitor.Cli.Core.Harness.Grok; | ||
|
|
||
| /// <summary> | ||
| /// Filesystem layout for Grok Build: everything under <c>$GROK_HOME</c> when set, otherwise | ||
| /// <c>~/.grok</c>. Grok merges every <c>*.json</c> in <see cref="HooksDir"/>, so kcap owns | ||
| /// <see cref="KcapHooksJson"/> outright. | ||
| /// </summary> | ||
| public sealed class GrokPaths { | ||
| /// <summary>Longest directory name Grok writes before swapping the encoded cwd for a slug.</summary> | ||
| const int MaxDirNameBytes = 255; | ||
|
|
||
| public GrokPaths(UserHome home, string? grokHome) => | ||
| Home = !string.IsNullOrWhiteSpace(grokHome) ? grokHome : Path.Combine(home.Path, ".grok"); | ||
|
|
||
| public string Home { get; } | ||
|
|
||
| public string HooksDir => Path.Combine(Home, "hooks"); | ||
|
|
||
| public string KcapHooksJson => Path.Combine(HooksDir, "kcap.json"); | ||
|
|
||
| public string ConfigToml => Path.Combine(Home, "config.toml"); | ||
|
|
||
| /// <summary>One directory per cwd, holding one directory per session.</summary> | ||
| public string SessionsDir => Path.Combine(Home, "sessions"); | ||
|
|
||
| /// <summary> | ||
| /// The session's <c>updates.jsonl</c>, or null when its directory cannot be named. The directory | ||
| /// is the cwd percent-encoded; past <see cref="MaxDirNameBytes"/> Grok uses a slug instead and | ||
| /// records the real cwd in that directory's <c>.cwd</c> file. | ||
| /// </summary> | ||
| public string? UpdatesJsonl(string cwd, string sessionId) { | ||
| var encoded = Uri.EscapeDataString(cwd); | ||
|
|
||
| if (Encoding.UTF8.GetByteCount(encoded) <= MaxDirNameBytes) | ||
| return Path.Combine(SessionsDir, encoded, sessionId, "updates.jsonl"); | ||
|
|
||
| return SlugDirFor(cwd) is { } dir ? Path.Combine(dir, sessionId, "updates.jsonl") : null; | ||
| } | ||
|
|
||
| string? SlugDirFor(string cwd) { | ||
| if (!Directory.Exists(SessionsDir)) return null; | ||
|
|
||
| foreach (var dir in Directory.EnumerateDirectories(SessionsDir)) { | ||
| var marker = Path.Combine(dir, ".cwd"); | ||
|
|
||
| try { | ||
| if (File.Exists(marker) && File.ReadAllText(marker).TrimEnd('\n', '\r') == cwd) return dir; | ||
| } catch (IOException) { } catch (UnauthorizedAccessException) { } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /// <summary>Whether Grok has run here. kcap can create <see cref="HooksDir"/> first, so the root | ||
| /// alone does not count.</summary> | ||
| public bool HasUserData() => | ||
| Directory.Exists(Home) && (File.Exists(ConfigToml) || Directory.Exists(SessionsDir)); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| namespace Capacitor.Cli.Core.Harness.Grok; | ||
|
|
||
| /// <summary>A parent's record of one of its subagents starting or finishing.</summary> | ||
| public sealed record GrokSubagentEvent(string ChildSessionId, string? SubagentType, bool Finished); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| using System.Text.Json; | ||
|
|
||
| namespace Capacitor.Cli.Core.Harness.Grok; | ||
|
|
||
| /// <summary> | ||
| /// Grok fires no hook for a subagent. It runs as a session of its own beside its parent's, and the | ||
| /// parent's <c>updates.jsonl</c> is the only record of it starting and finishing. | ||
| /// </summary> | ||
| public static class GrokSubagents { | ||
| public static IEnumerable<GrokSubagentEvent> Read(IEnumerable<string> parentLines) { | ||
| foreach (var line in parentLines) { | ||
| if (!line.Contains("subagent_", StringComparison.Ordinal)) continue; | ||
|
|
||
| GrokSubagentEvent? found = null; | ||
| try { | ||
| using var doc = JsonDocument.Parse(line); | ||
| var root = doc.RootElement; | ||
|
|
||
| if (root.Str("method") == "_x.ai/session/update" | ||
| && root.Obj("params")?.Obj("update") is { } update | ||
| // The id names a sibling directory, so a non-GUID one could point outside the sessions dir. | ||
| && update.Str("child_session_id") is { Length: > 0 } child | ||
| && Guid.TryParse(child, out _)) { | ||
| found = update.Str("sessionUpdate") switch { | ||
| "subagent_spawned" => new(child, update.Str("subagent_type"), Finished: false), | ||
| "subagent_finished" => new(child, update.Str("subagent_type"), Finished: true), | ||
| _ => null | ||
| }; | ||
| } | ||
| } catch (JsonException) { } | ||
|
|
||
| if (found is not null) yield return found; | ||
| } | ||
| } | ||
|
|
||
| public static string ChildTranscript(string parentTranscript, string childSessionId) => | ||
| Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(parentTranscript)!)!, childSessionId, "updates.jsonl"); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| public static string AgentId(string childSessionId) => childSessionId.Replace("-", ""); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,4 +17,5 @@ public enum HarnessId { | |
| OpenCode, | ||
| Antigravity, | ||
| MistralVibe, | ||
| Grok, | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.