Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ Once set up, Capacitor runs silently in the background. Every Claude Code (and C
- **SessionStart context injection** — at every session start the server injects top evaluation-derived fact clusters for the current repo into the session's context. The injected block is split into two sections: `## Known patterns` (repo/project facts relevant to any reader) and `## Guidance from past sessions` (agent-targeted action items derived from prior eval suggestions with `audience: "agent"`). Delivered for every supported harness (Claude Code, Codex CLI, GitHub Copilot CLI, Gemini CLI, AWS Kiro CLI, Google Antigravity, Pi, OpenCode, and Cursor's `cursor-agent`): Claude reads it from its hook response, while the other eight fetch `GET /api/repositories/{hash}/guidelines` alongside the team-memory index and emit both in one combined block, through the same per-harness delivery seam as that index (see the next bullet). Opt out by setting `disable_session_guidelines: true` in `~/.config/kcap/config.json` or via `kcap config set disable_session_guidelines true`.
- **SessionStart team-memory index** — at every session start (Claude Code, Codex CLI, GitHub Copilot CLI, Gemini CLI, AWS Kiro CLI, Google Antigravity, Pi, OpenCode, and Cursor CLI's `cursor-agent` — see the capability matrix below for the full per-harness rollout) `kcap` also fetches a compact index of durable [team memories](#memory-mcp-server-for-agents) visible for the current repo/machine and appends a `## Team memory` block to the session's injected context (`additionalContext` for Claude, Codex, Copilot, and Gemini, `additional_context` for Cursor, raw stdout for Kiro, `injectSteps`/`userMessage` for Antigravity, system-prompt append via the kcap Pi extension for Pi, and system-entry append via the kcap OpenCode plugin for OpenCode): one `slug: description` line per memory, grouped **Org / Team / Yours**, with a nudge to call `get_memory` / `search_memories` for full content. Only the index is injected — never the bodies — so the cost stays roughly flat as the pool grows (mirrors a local `MEMORY.md`). Best-effort and fail-open (a slow or failed fetch injects nothing, never blocking the hook), and only ever injected once per conversation. Opt out with `disable_memory_index: true` in `~/.config/kcap/config.json` or `kcap config set disable_memory_index true`.
- **SessionStart work-items nudge** — at every session start (Claude Code, Codex CLI, GitHub Copilot CLI, Gemini CLI, AWS Kiro CLI, Google Antigravity, Pi, OpenCode, and Cursor CLI's `cursor-agent`) `kcap` appends a short `## Work items` block carrying the current session id and a reminder to register the session with its work item via the [`kcap-workitems` MCP tools](#work-items-mcp-server-for-agents) (`declare_work_item`) and to declare structure as it is discovered (`declare_work_breakdown` for a parent→parts split, `declare_work_relation` for a `blocks`/`blocked_by` dependency). It rides the same per-harness delivery seam as the team-memory index, is composed independently of that index (so it never affects the index's once-per-session lease), and is shown only when `kcap-workitems` is actually registered for the harness. Opt out with `disable_workitems_nudge: true` in `~/.config/kcap/config.json` or `kcap config set disable_workitems_nudge true`.
- **SessionStart coordination notices** — at every session start (Claude Code / the generic route only) `kcap` advertises a `coordination_notices` capability on its `/hooks/session-start` request, and when the server has pending coordination notices for you — a heads-up that other people have in-flight work that may overlap yours (work-overlap / work-item adjacency) — it appends a `## Coordination notices` block to the session's injected context (`additionalContext`), one short line per notice (bounded, with a `+N more in the notification centre` tail when there are more). The same notices always reach the in-app notification centre and Slack regardless; this block just surfaces the most relevant few directly in the agent's context at the moment you start. Best-effort and fail-open (a missing or malformed field injects nothing, never blocking the hook), and the capability is advertised only on a live session start — never from `kcap import`/backfill. Opt out with `disable_coordination_notices: true` in `~/.config/kcap/config.json` or `kcap config set disable_coordination_notices true`; when set, the capability is not sent at all, so the notices stay in the notification centre / Slack only.
- **Crash resilience** — if a `kcap` command hits an unexpected error it records the exception (with stack trace) to `~/.config/kcap/crash.log` (honours `KCAP_CONFIG_DIR`; size-capped) and exits cleanly instead of aborting. Hook and detached-generator commands the coding agent spawns **fail open** (exit 0, nothing surfaced to the agent); other commands exit non-zero with a one-line stderr message pointing at the log.

The SessionStart memory foundation is deliberately separate from harness activation. Every row uses
Expand Down
9 changes: 9 additions & 0 deletions src/Capacitor.Cli.Core/Config/ProfileConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ public record Profile {
[JsonPropertyName("disable_workitems_nudge")]
public bool? DisableWorkItemsNudge { get; init; }

/// <summary>
/// when true, kcap does not advertise the coordination-notices capability at SessionStart, so the
/// server injects no coordination notices (heads-up about others' in-flight work that may overlap
/// yours) — they still reach the notification centre and Slack. Independent of the other SessionStart
/// opt-outs. Mirrors <see cref="DisableMemoryIndex"/>.
/// </summary>
[JsonPropertyName("disable_coordination_notices")]
public bool? DisableCoordinationNotices { get; init; }

/// <summary>
/// When true, kcap keeps <c>ANTHROPIC_API_KEY</c> / <c>OPENAI_API_KEY</c>
/// in the spawn environment for headless agent CLIs (title generation,
Expand Down
1 change: 1 addition & 0 deletions src/Capacitor.Cli.Core/Resources/help-config.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Config keys:
disable_session_guidelines Skip injecting recurring-lessons context at SessionStart (true/false)
disable_memory_index Skip injecting the team-memory index at SessionStart (true/false)
disable_workitems_nudge Skip injecting the work-items nudge at SessionStart (true/false)
disable_coordination_notices Skip injecting coordination notices (others' overlapping work) at SessionStart (true/false)
use_provider_api_key Keep ANTHROPIC_API_KEY/OPENAI_API_KEY in headless agent spawns (true/false)
excluded_repos Excluded repos, comma-separated (owner/repo,owner/repo)
update_check Enable update check (true/false)
Expand Down
3 changes: 3 additions & 0 deletions src/Capacitor.Cli/Commands/ConfigCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ public static Profile ApplySet(Profile profile, string key, string value) =>
"disable_session_guidelines" => throw new ArgumentException($"Invalid value for disable_session_guidelines: '{value}'. Must be true or false."),
"disable_memory_index" when bool.TryParse(value, out var b) => profile with { DisableMemoryIndex = b },
"disable_memory_index" => throw new ArgumentException($"Invalid value for disable_memory_index: '{value}'. Must be true or false."),
"disable_coordination_notices" when bool.TryParse(value, out var b) => profile with { DisableCoordinationNotices = b },
"disable_coordination_notices" => throw new ArgumentException($"Invalid value for disable_coordination_notices: '{value}'. Must be true or false."),
Comment thread
realtonyyoung marked this conversation as resolved.
Resolved
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Resolved
"disable_workitems_nudge" when bool.TryParse(value, out var b) => profile with { DisableWorkItemsNudge = b },
"disable_workitems_nudge" => throw new ArgumentException($"Invalid value for disable_workitems_nudge: '{value}'. Must be true or false."),
"use_provider_api_key" when bool.TryParse(value, out var b) => profile with { UseProviderApiKey = b },
Expand Down Expand Up @@ -204,6 +206,7 @@ static int SetUsage() {
Console.Error.WriteLine(" default_visibility Default session visibility (private, project, org_public, public)");
Console.Error.WriteLine(" disable_session_guidelines Skip injecting recurring-lessons context at SessionStart (true/false)");
Console.Error.WriteLine(" disable_workitems_nudge Skip injecting the work-items nudge at SessionStart (true/false)");
Console.Error.WriteLine(" disable_coordination_notices Skip injecting coordination notices (others' overlapping work) at SessionStart (true/false)");
Console.Error.WriteLine(" use_provider_api_key Keep ANTHROPIC_API_KEY/OPENAI_API_KEY in headless agent spawns (true/false)");
Console.Error.WriteLine(" excluded_repos Excluded repos, comma-separated (owner/repo,owner/repo)");
Console.Error.WriteLine(" flows.reviewer_vendor Preferred review-flow reviewer vendor (used only when the definition names none)");
Expand Down
36 changes: 34 additions & 2 deletions src/Capacitor.Cli/Commands/Harness/ClaudeHookCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,29 @@ await WatcherManager.EnsureWatcherRunning(
return 0;
}

// Advertise the coordination-notices capability so the server MAY return work-overlap
// notices to render below (next to the memory index). Injected into a SEPARATE postBody,
// never `body`: `body` is what the transient-failure and ordering-guard paths spool, and a
// replay is a catch-up, not a live render — a spooled capability would let the server mark
// notices delivered that the replay can never inject (they stay in the bell/Slack and reach
// the next LIVE session-start instead). Live-only by construction: `kcap import` posts
// /hooks/session-start/{vendor} with origin=historical and never reaches here. Suppressed by
// the disable_coordination_notices opt-out, read from the EFFECTIVE profile (honoured for
// KCAP_URL users too, unlike the memory read above). Fail-open.
var coordinationNoticesDisabled = activeProfile?.DisableCoordinationNotices is true;
var postBody = body;
if (!coordinationNoticesDisabled) {
try {
var node = JsonNode.Parse(body);
if (node is not null) {
node["coordination_notices"] = CoordinationNoticesEmitter.CapabilityVersion;
postBody = node.ToJsonString();
}
} catch {
// Best effort — never fail the hook building the capability field.
}
}

// kick off the team-memory index fetch in PARALLEL with the hook POST so
// it adds no latency to the critical path. Fully best-effort / fail-open: any failure,
// a 401, or a budget overrun yields a null fragment and nothing is injected. Started
Expand All @@ -602,7 +625,9 @@ await WatcherManager.EnsureWatcherRunning(
HttpResponseMessage? resp = null;
try {
if (remaining > TimeSpan.Zero) {
using var content = new StringContent(body, Encoding.UTF8, "application/json");
// postBody carries the coordination-notices capability; the spool below uses the
// capability-free `body` so a replay never claims notices it cannot render.
using var content = new StringContent(postBody, Encoding.UTF8, "application/json");
resp = await client.PostOnceAsync($"{baseUrl}/hooks/session-start", content, remaining, CancellationToken.None);
}
} catch { resp = null; }
Expand Down Expand Up @@ -670,13 +695,20 @@ await WatcherManager.EnsureWatcherRunning(
// hook budget so a slow fetch can't delay the hook (fail-open → null).
var memoryFragment = await AwaitMemoryFragmentAsync(memoryIndexTask, processStart);

// Coordination notices ride the hook POST response (no extra fetch), same as the
// guidelines fragment. Gated on the same opt-out that suppressed the capability
// above: when disabled the server was never asked and returns nothing, but the
// gate is defense-in-depth so the opt-out holds even against an over-eager server.
var coordinationFragment = CoordinationNoticesEmitter.BuildFragment(
responseNode, coordinationNoticesDisabled);

// The static work-items nudge. Claude has always carried kcap-workitems, so
// the availability gate is always satisfied here; only the opt-out can suppress it.
var workItemsNudge = WorkItemsNudgeEmitter.Resolve(
SessionStartHarness.Claude, sessionId, activeProfile?.DisableWorkItemsNudge is true);

var envelope = SessionStartAdditionalContext.BuildEnvelope(
lessonsFragment, nudgeFragment, memoryFragment, workItemsNudge);
lessonsFragment, nudgeFragment, memoryFragment, coordinationFragment, workItemsNudge);

if (envelope is not null) {
writer.WriteLine(envelope);
Expand Down
61 changes: 61 additions & 0 deletions src/Capacitor.Cli/CoordinationNoticesEmitter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Text;
using System.Text.Json.Nodes;

namespace Capacitor.Cli;

/// <summary>
/// Builds the SessionStart "coordination notices" fragment from a server
/// <c>/hooks/session-start</c> response. Plain text, not a JSON envelope — the caller
/// (<c>SessionStartAdditionalContext</c>) joins it with the other fragments and wraps one
/// Claude Code <c>hookSpecificOutput</c> envelope.
/// <para>
/// The server returns <c>coordination_notices</c> as a bounded array of <c>{ text }</c> one-line
/// warnings (optionally with a "+N more in the notification centre" tail) about others' in-flight
/// work that may overlap this session — the same notices that also reach the notification centre
/// and Slack. Each renders as one bullet under a <b>Coordination notices</b> heading. Returns
/// <c>null</c> when disabled, absent, empty, or malformed, so the caller emits nothing (fail-open,
/// like the guidelines and memory-index fragments).
/// </para>
/// </summary>
static class CoordinationNoticesEmitter {
/// <summary>
/// The capability token the CLI advertises on the SessionStart request
/// (<c>coordination_notices: "v1"</c>). An old/opted-out CLI sends nothing and
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Resolved
/// the server does no selection/claim/render; a future wire-shape change bumps
/// this token so a server keyed on the old one simply declines.
/// </summary>
internal const string CapabilityVersion = "v1";

/// <summary>
/// Returns the coordination-notices block text, or <c>null</c> when there is
/// nothing to emit (no <c>coordination_notices</c>, all empty, user opted out,
/// malformed response).
/// </summary>
/// <param name="responseNode">The hook response body parsed as a <see cref="JsonNode"/>.</param>
/// <param name="disabled">True when the user has set <c>disable_coordination_notices</c> on their active profile.</param>
public static string? BuildFragment(JsonNode? responseNode, bool disabled) {
if (disabled) return null;
if (responseNode is not JsonObject obj) return null;
if (obj["coordination_notices"] is not JsonArray notices || notices.Count == 0) return null;

var lines = new List<string>();
foreach (var node in notices) {
if (node is not JsonObject o) continue;

string? text;
try { text = o["text"]?.GetValue<string>(); }
catch { continue; }

if (string.IsNullOrWhiteSpace(text)) continue;
lines.Add($"- {text.Trim()}");
}

if (lines.Count == 0) return null;

var sb = new StringBuilder();
sb.AppendLine("## Coordination notices");
sb.AppendLine("Heads-up: other people are working on things that may overlap yours. Coordinate before you collide.");
foreach (var l in lines) sb.AppendLine(l);
return sb.ToString().TrimEnd();
}
}
Loading