feat: Kilo, Cline, Antigravity IDE, project isolation, launch panel#42
feat: Kilo, Cline, Antigravity IDE, project isolation, launch panel#42Solar2004 wants to merge 1 commit into
Conversation
8378183 to
6b064f9
Compare
|
@Solar2004 Thanks for contributing! Can you split into seperate PRs? AI review: PR #42 — Architectural ReviewRepo: 0. ScopePR #42 bundles six unrelated changes in one commit:
Reviewing these as a single architectural unit is not possible because they have unrelated risk profiles. This document evaluates each on its own terms, then assesses bundle-level concerns. 0.5 Feature inventory — what already exists in hcom vs what is newBefore per-defect detail, an inventory of which PR features add capabilities not present on Already exists in hcom (PR adds parallel mechanism)
New capabilities
Bug fix, not a feature
Refinement, not a new capability
1. Integration tier taxonomyThe codebase implements two integration shapes for tools on
Per-tool gate config is centralized at PR #42 introduces two more tiers without naming or documenting them:
Cline and Antigravity are forced into the existing
A more honest design would name the tiers ( 2. Functional defects (will not work as shipped)2.1 Kilo integration — two independent fatalsThe Kilo integration is non-functional for two unrelated reasons (§2.1a and §2.1b below). Either alone is sufficient to break it. 2.1a Hook subcommand names mismatch
const KILO_HOOKS: &[&str] = &[
"kilocode-start",
"kilocode-status",
"kilocode-read",
"kilocode-stop",
];The embedded plugin Only line 214 ( Effect: Kilo session binding ( This bug has been present unchanged across PR #37 → #38 → #39 → #40 → #41 → #42 per prior reviews. 2.1b Wrong upstream namespace —
|
| Command | --project flag |
Filter applied |
|---|---|---|
send |
yes | yes (mentions only — broadcast leaks per §2.5) |
list |
yes | yes (src/commands/list.rs:217-234) |
events |
no | no |
listen |
no | no |
transcript |
no | no |
archive |
no | no |
bundle |
no | no |
events, listen, transcript, archive, bundle see across all projects. An agent in project A subscribed via hcom events sub receives notifications about project B status changes. hcom transcript <name> reads the transcript path without project gating. The bootstrap text the agent reads claims project isolation; the observation surfaces the agent uses to discover other agents do not enforce it.
This is worse than passive observability leaking. hcom events sub --on-hit "<command>" and hcom listen create active delivery mechanisms — subscriptions invoke callbacks when matching events fire. src/commands/events.rs:67-92 (EventsSubArgs) has --for, --on-hit, and filter flags but no --project. src/commands/listen.rs:498-505 creates a temp subscription with caller and SQL but no project gate. The events_v view (src/db.rs:417-435) exposes event/message fields from the events JSON column with no instance/project join, so even SQL-based filters cannot easily express project-aware queries. A project-A agent that subscribes to --type status matching everywhere will receive callbacks driven by project-B activity. Since --on-hit can run arbitrary commands (including hcom send), this is a cross-project message-delivery channel, not just a leak in the read plane.
2.6b opencode-read --format auto-ack — silent semantic regression
The PR changes the existing OpenCode --format semantics in a way that contradicts the OpenCode plugin's deferred-ack design.
Main branch (src/hooks/opencode.rs:375-398):
if format_mode {
if messages.is_empty() {
return (0, String::new());
}
let deliver = common::limit_delivery_messages(&messages);
let formatted = common::format_messages_json_for_instance(db, &deliver, &name);
return (0, formatted);
}--format reads and renders without advancing the cursor. Ack is a separate operation (--ack --up-to <id>).
PR #42 (src/hooks/opencode.rs:380-396):
if format_mode {
if messages.is_empty() {
return (0, String::new());
}
let deliver = common::limit_delivery_messages(&messages);
// Auto-ack: advance cursor so same messages aren't re-delivered
let last_id = deliver.iter()
.filter_map(|m| m.get("event_id").and_then(|v| v.as_i64()))
.max()
.unwrap_or(0);
if last_id > 0 {
let mut updates = serde_json::Map::new();
updates.insert("last_event_id".into(), serde_json::json!(last_id));
instances::update_instance_position(db, &name, &updates);
}
let formatted = common::format_messages_json_for_instance(db, &deliver, &name);
return (0, formatted);
}--format now advances the cursor as a side effect of formatting.
This contradicts the OpenCode plugin's deferred-ack design. src/opencode_plugin/hcom.ts:174 (main branch) explicitly comments that promptAsync should not ack — ack is deferred to experimental.chat.messages.transform after the message is verifiably injected. src/opencode_plugin/hcom.ts:505-510 (main) calls hcom opencode-read --ack --up-to <id> from the transform stage. The whole point of --format being non-ack is at-least-once delivery: if the formatting succeeds but the downstream injection fails (process crash, transform error, network blip), the message is still pending and will be re-tried.
The PR's auto-ack changes this to at-most-once with silent loss: any caller of --format that fails to deliver downstream loses the message. Even though the current opencode plugin (src/opencode_plugin/hcom.ts) does not call --format directly (it uses the message body and acks via a separate path), this change touches shared hook semantics. Anything else that polls via cline-read --format (PR adds this — src/opencode_plugin/UserPromptSubmit:11) or kilo-read --format is now also at-most-once-with-silent-loss.
The accompanying test was renamed from "format does not advance cursor" to "format advances cursor," locking the regression in.
This is a correctness regression on the OpenCode integration that the PR does not target as a feature. The accompanying test rename indicates the change was intentional, not accidental.
2.7 NULL-project as wildcard is undocumented
src/messages.rs:262-266 (and src/commands/list.rs:225-229):
.filter(|inst| {
inst.project
.as_deref()
.map(|p| p == proj)
.unwrap_or(true) // include instances with no project
})Instances with project = NULL are visible from any project. This is intentional backward-compat, but is undocumented. One agent launched without --project punctures the boundary for every other agent — a property users designing around "isolation" will not expect.
2.8 TUI launch panel left/right cursor regression
src/tui/input.rs:1075-1080:
KeyCode::Left => {
self.ui.launch.tool = self.ui.launch.tool.prev();
}
KeyCode::Right => {
self.ui.launch.tool = self.ui.launch.tool.next();
}Unconditional, regardless of which launch-panel field is focused. If a text-editable field exists in the launch panel (initial-prompt, name, etc.), left/right will not move the cursor — they cycle the tool selector.
3. Architectural decisions worth scrutiny
3.1 src/agent_prompts.rs adds a fourth instruction layer
hcom already has three instruction-injection layers and several tool-native equivalents:
| Layer | Where | Lifecycle | Scope |
|---|---|---|---|
| Bootstrap | src/bootstrap.rs::get_bootstrap |
Once at session start (and on compaction for tools that re-fire SessionStart) | Per-instance, includes identity + capabilities |
notes |
src/bootstrap.rs:437-439, sourced from HCOM_NOTES env or global hcom config notes |
Appended to bootstrap once | Global (per-instance not in INSTANCE_KEYS at src/commands/config.rs:152-161) |
hints |
HCOM_HINTS env or hcom config -i <name> hints |
Appended to received messages, not bootstrap | Per-instance |
--hcom-prompt / --hcom-system-prompt |
Launch flags | Once at launch | Per-launch |
| (this PR) Agent prompt | ~/.hcom/agents/<name>.md via src/agent_prompts.rs::load_agent_prompt |
Whenever bootstrap::get_bootstrap runs (per-tool) |
Per-instance, file-keyed on instance base name |
Tool-native equivalents:
- Claude:
.claude/agents/<name>.mdvia--agent <name>(Claude Code subagents) - Codex: developer instructions
- Gemini: system prompt file
- OpenCode: bootstrap transform
The PR's new module (47 lines) loads ~/.hcom/agents/<name>.md and the bootstrap renderer appends it at src/bootstrap.rs:465-468:
if let Some(agent_prompt) = crate::agent_prompts::load_agent_prompt(instance_name) {
/* … */
agent_promptSo agents launched through the get_bootstrap path do receive the file's content. The reachability is real — what overlaps is the conceptual layer.
The PR's mechanism overlaps the existing notes layer at bootstrap.rs:437-439:
if !ctx.notes.is_empty() {
result.push_str(&format!("\n\n## NOTES\n\n{}\n", ctx.notes));
}Both append text to the bootstrap. The difference is the source: notes comes from env or global config; agent_prompts comes from a per-name file under ~/.hcom/agents/. The new layer adds a second markdown-section append for substantially the same purpose.
The file is keyed on instance base name. CVCV names are allocated from a pool with reuse, and hcom reset clears the DB but does not touch ~/.hcom/agents/. So a prompt file written for instance luna persists past hcom reset and is read by the next instance named luna. The fact that this is a stable behavior depends on whether the user models <name>.md as "configuration for this specific session" (reset would surprise) or "configuration for the role" (reset behaving correctly). The PR does not specify which.
The "re-injected on every session compaction" claim in the agent-facing bootstrap text is true only for tool paths that re-fire SessionStart on compaction (Claude, OpenCode). Gemini, Codex, and Cline paths reach get_bootstrap once at session start; compaction inside those tools will not re-run the renderer. So the agent's expectation set by the bootstrap text is per-tool, not universal.
3.2 --agent-name undermines the routing invariant
The CVCV-pattern naming (luna, nova, kira — consonant-vowel-consonant-vowel) is not cosmetic. It exists for:
- Token efficiency — single-token names in most BPE vocabularies.
- Hamming-distance collision avoidance —
score_namerejects names too similar to alive instances; LLMs misroute confusable names. - Pool reservation — addressed via
@luna, mentions tracked in events. - Cross-device pool partitioning — relay devices reserve disjoint subsets so remote agents don't collide.
--agent-name custombot skips:
- Similarity check vs. live instances.
- CVCV pool reservation.
- Cross-device collision check (a remote
dev:BOXEdevice's pool can clash with localdev).
--tag api --agent-name api-bot produces display name api-api-bot because the tag prefix is mechanical (src/instances.rs:52-58).
If the goal is "memorable name," the hcom-shaped solution is an alias column that maps a human label to a stable CVCV ID, with routing using the ID and display using the alias. The PR's path collapses both into a single user-supplied string and accepts the routing risks.
Remote launches silently drop --agent-name. src/commands/launch.rs:83-95 serializes "name": hcom_flags.name into the remote launch JSON params. But RemoteLaunchRequest (src/relay/control.rs:631-644) has no name field — its struct fields are tool, count, args, tag, project, launcher, system_prompt, initial_prompt, background, pty, terminal, cwd. from_params (src/relay/control.rs:647-665) never reads name. handle_remote_launch constructs LaunchParams { ..., name: None, ... } (visible at the very end of src/relay/control.rs:735-758). So hcom claude --agent-name custom works locally; hcom claude --agent-name custom --device X --dir ... accepts the flag, sends it on the wire, and the remote silently discards it. Two-tier behavior with no error, no warning. This is independently a partial-integration bug regardless of whether --agent-name itself is a good idea.
3.3 mark_dead_instances runs on every CLI invocation
src/main.rs:67-72:
if let Ok(db) = crate::db::HcomDb::open() {
let count = instance_lifecycle::mark_dead_instances(&db);
if count > 0 {
log::log_info("startup", ...);
}
}Every hcom send, hcom list, hcom events, every hook callback (and the OpenCode/Kilo plugin polls status every 5s — src/opencode_plugin/hcom.ts 5000ms reconcile loop) executes:
iter_instances_full()— full table scan.pidtrack::is_alive(pid)— kill(0,pid) syscall per row.- For dead rows: snapshot via
log_life_event, then severalDELETEstatements.
For an 8-agent swarm with 5s status polling, that's >1 scan/second steady-state, plus every interactive hcom invocation. Concurrent hcom processes can race on DELETE for the same dead row.
The PR description frames this as "system reboot recovery" — a once-per-boot event. The implementation runs the scan unconditionally on every CLI invocation. The existing stale-cleanup machinery at src/commands/list.rs:84-85 runs only when hcom list is invoked; the new path does not gate similarly.
3.4 Cline integration mechanism mismatches Cline's actual surface
Cline's documented integration surfaces (per cline/cline repo):
- Hooks — JSON-stdin / JSON-stdout, 8 types (proto/cline/hooks.proto): TaskStart, TaskResume, TaskComplete, TaskCancel, PreToolUse, PostToolUse, UserPromptSubmit, PreCompact.
--acp— Agent Client Protocol over stdio JSON-RPC (cli/src/acp/index.ts).--json— structured stdout for machine consumption.cline task "$prompt"— direct stdin pipe.
The PR ships 5 of 8 hook types: TaskStart, TaskResume, TaskComplete, TaskCancel, UserPromptSubmit. Missing: PreToolUse, PostToolUse, PreCompact.
The shipped scripts use sed regex to parse JSON. src/opencode_plugin/TaskStart:
_T=$(echo "$_I" | sed -n 's/.*"taskId"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
_B=$(echo "$_R" | sed -n 's/.*"bootstrap"[[:space:]]*:[[:space:]]*"\(.*\)"[[:space:]]*,[[:space:]]*"/\1/p')
[ -z "$_B" ] && _B=$(echo "$_R" | sed -n 's/.*"bootstrap"[[:space:]]*:[[:space:]]*"\(.*\)"[[:space:]]*}$/\1/p')Failure modes:
- Bootstrap text contains
","patterns — both the project notice and the relay notice insrc/bootstrap.rs:90, 93contain commas, but they are not the","JSON delimiter pattern. More relevant: any user-setHCOM_NOTESor scripts list that contains","will break the second-fallback regex. - Escaped quotes in JSON (
\") — the[^"]*group stops at the first quote regardless of escape state. - Multi-line JSON (the renderer outputs
\nliterals; the.*in sed is single-line by default).
The awk re-escape stage:
_E=$(printf '%s' "$_B" | awk '{gsub(/\\/,"\\\\");gsub(/"/,"\\\"");printf "%s",$0}')Doubles backslashes then escapes quotes. If _B already contained \\ (a properly JSON-escaped backslash extracted by sed), it becomes \\\\ after awk — the literal string \\ after JSON re-decode. Information loss.
clinehook.sh is byte-identical to TaskStart. One is a duplicate.
More appropriate alternatives, ranked by stability:
--acpmode — Cline ships JSON-RPC over stdio. hcom would become an ACP client; bidirectional, typed, lifecycle-tracked properly.- Pipe through
jq -r '.taskId'instead ofsed— same dependency footprint as bash, robust JSON. - Hooks-only ad-hoc tier —
hcom hooks add clinewrites the bridge scripts; user runsclinethemselves; hcom does not own the PTY. The ad-hoc tier already exists for the supported tools:src/bootstrap.rs:122-146defines theDELIVERY_ADHOCtemplate, andsrc/bootstrap.rs:414-419selects betweenDELIVERY_AUTOandDELIVERY_ADHOCbased onis_launched— vanilla agents (started viahcom startfrom inside an unmanaged session) take the ad-hoc path.hcom listdisplays them with lowercase tool labels ([claude]vs[CLAUDE]).
The PR's choice is the most fragile of all the available options.
3.5 Cline integration targets the CLI, but installs hooks where Cline does not read from
The PR integrates with the Cline CLI (not the VS Code extension). Evidence:
--tuiis a Cline CLI option declared at cline/cli/src/index.ts:1188:"Open the legacy terminal UI instead of the kanban experience". The PR's launcher always appends--tuifor Cline atsrc/launcher.rs:917-919.- The PR PTY-spawns Cline via
LaunchBackend::HeadlessPty(src/launcher.rs:113). A VS Code extension cannot be subprocess-spawned.
Cline's default global hooks directory is hardcoded in cline/src/core/hooks/utils.ts:54:
return globalHooksDirOverride || path.join(os.homedir(), "Documents", "Cline", "Hooks")I.e. ~/Documents/Cline/Hooks/. The workspace hooks directory is .clinerules/hooks/ per cline/src/core/hooks/hook-factory.ts:909.
The PR installs to $XDG_CONFIG_HOME/cline/hcom/ (src/hooks/cline.rs:127-131):
fn get_cline_plugin_dir() -> PathBuf {
PathBuf::from(xdg_config_home()).join("cline").join("hcom")
}src/hooks/cline.rs:140-165 adds a scan_plugin_dirs() that scans six candidate paths — $XDG_CONFIG_HOME/cline/{hcom,plugins,hooks} and $TOOL_ROOT/.cline/{hcom,plugins,hooks}. None of the six match Cline's actual load paths (~/Documents/Cline/Hooks/ global, .clinerules/hooks/ workspace). The Cline CLI does support --hooks-dir <path> at cline/cli/src/index.ts:973 for runtime hook-directory injection, but the PR does not pass --hooks-dir when spawning Cline (git show pr-42:src/launcher.rs shows only --tui and the user-supplied prompt are appended).
Effect: Cline never reads from where the PR installed the hooks. The 618-line src/hooks/cline.rs plus the five shell scripts in src/opencode_plugin/ (TaskStart, TaskResume, TaskComplete, TaskCancel, UserPromptSubmit) are unreachable at runtime. The Cline integration as shipped does nothing.
The 6-directory scan exists in the code, indicating awareness of path uncertainty. The install destination, the scan dirs, and Cline's actual load paths are three disjoint sets.
3.6 Cline's surface matches Claude's; the PR routes it through OpenCode's instead
Cline's hook system is structurally identical to Claude Code's: JSON over stdin, {cancel, contextModification, errorMessage} over stdout, lifecycle event types named TaskStart / PreToolUse / PostToolUse / UserPromptSubmit / PreCompact / etc. (cline/proto/cline/hooks.proto). Stateless scripts, one file per hook type, fired on the same lifecycle points Claude fires on. The tool also exposes --hooks-dir <path> for runtime hook-directory injection (cline/cli/src/index.ts:973) and a PTY-wrappable TUI via --tui.
The shape that fits this surface is the existing Claude/Gemini/Codex integration — a two-layer DELIVERY_AUTO mechanism:
Layer 1 — PTY inject (idle wake-up). src/delivery.rs::run_delivery_loop runs as a background thread per launched instance. On a TCP knock from notify_all_instances (or a polling tick), it checks gates (src/delivery.rs::evaluate_gate) — idle status, prompt empty, no approval pending — and if they pass, injects text into the agent's PTY input box via inject_text (src/delivery.rs:444-459) followed by inject_enter. The injected text is "<hcom>" for Claude/Codex (src/delivery.rs:850) or a per-message preview for Gemini. This wakes the agent and starts a new turn.
Layer 2 — Hook delivery (in-turn message text). Once the agent starts a turn, its hooks fire. The hooks return pending message text via additionalContext (Claude/Codex/Gemini's vendor-specific name for hcom's contextModification):
- Claude
handle_posttooluse(src/hooks/claude.rs:917-963) callsdb.get_unread_messages(instance_name)on everyPostToolUseand returns them inhookSpecificOutput.additionalContext. - Codex
handle_posttooluse(src/hooks/codex.rs:301-308) callsprepare_codex_deliverywhich does the same. - Gemini
handle_aftertool(src/hooks/gemini.rs:414-462) does the same with adelivery_ack.
Layer 1 is the wake-up; Layer 2 is the actual text delivery. Both must work for DELIVERY_AUTO to function. When the agent is in a long task between <hcom> kicks, Layer 2 (hooks firing on tool calls) keeps message latency low — Claude picks up new messages on the next tool call. When the agent goes idle and a new message arrives, Layer 1 (PTY kick) wakes it.
Cline maps onto this two-layer model directly: cline --tui is a PTY-wrappable TUI; PostToolUse is the in-turn pickup hook; --hooks-dir plus matching the install path satisfies hook discovery; cline-read --check/--format (the hcom-side argv subcommands the PR already implements) provide the hook script's call shape.
The PR does not use this mapping. Instead it routes Cline through OpenCode's plugin-mediated delivery model:
src/delivery.rs:337mapsTool::Cline => Self::opencode().src/delivery.rs:614addsTool::Clineto the OpenCode-mode early-exit (if matches!(Tool::from_str(&config.tool), Ok(Tool::OpenCode) | Ok(Tool::Kilo) | Ok(Tool::Cline))).
OpenCode's delivery model differs from Claude/Gemini/Codex's: a long-lived @opencode-ai/plugin TS module (src/opencode_plugin/hcom.ts) runs inside OpenCode's process, registers a TCP notify port, and calls hcom opencode-read --format on wake. Hcom's delivery thread early-exits after injecting the first message (to seed the plugin's session); after that, the plugin handles delivery via promptAsync() and messages.transform. Cline has no plugin runtime — the CLI loads only stateless hook scripts (cline/src/core/hooks/utils.ts), not in-process TS modules. So the OpenCode-mode early-exit assumes a continuation that does not exist for Cline.
Effect on each layer:
- Layer 1: delivery thread early-exits after first message → no ongoing PTY kicks for idle wake-up.
- Layer 2: PR ships five of Cline's eight hooks (
TaskStart,TaskResume,TaskComplete,TaskCancel,UserPromptSubmit— the lifecycle and prompt hooks). The three not shipped arePreToolUse,PostToolUse,PreCompact.PostToolUseis the in-turn pickup hook used by Claude's Layer 2; it is not shipped.
The PR adds cline to the DELIVERY_AUTO bootstrap branch (src/bootstrap.rs:435-438):
if tool == "claude"
|| ((tool == "codex" || tool == "gemini" || tool == "opencode" || tool == "kilo" || tool == "cline") && ctx.is_launched)
{
parts.push(DELIVERY_AUTO);So the agent is told (per src/bootstrap.rs:111-120):
Messages instantly and automatically arrive via
<hcom>tags — end your turn to receive them.
The actual mechanism: only UserPromptSubmit fires inbound (next time the user types). Pickup latency is "next user prompt" — DELIVERY_ADHOC semantics with a DELIVERY_AUTO label.
Comparison of what each tool actually does:
| Tool | Layer 1 (idle wake) | Layer 2 (in-turn delivery) |
|---|---|---|
| Claude (PTY) | Delivery thread injects <hcom> when gates pass |
PostToolUse returns additionalContext |
| Gemini (PTY) | Delivery thread injects message preview when idle | AfterTool returns additionalContext |
| Codex (PTY) | Delivery thread injects <hcom> when idle |
PostToolUse returns additionalContext |
| OpenCode | First message via PTY (bootstrap), then plugin handles all delivery | Plugin's promptAsync() and messages.transform |
| Cline (this PR) | First message only via PTY (bootstrap). Delivery thread early-exits, but no plugin exists to take over | None (no PostToolUse shipped) |
auto-ack and 5s timeout (PR description) refer to handle_read --format's last_event_id advance at src/hooks/cline.rs:475-481 and the delivery-thread startup gate respectively. They affect what happens once a message is read, not whether one is read.
3.7 Antigravity bridge registers project non-atomically
extensions/hcom-bridge/src/extension.ts:39-45 calls hcomClient.startAgent(name, project), which at extensions/hcom-bridge/src/hcomClient.ts:98-124 runs two separate hcom invocations:
hcom start --name <name>— registers the extension as an ad-hoc instance.hcom config -i <name> project <project>— sets the project field on the registered row.
Between the two calls, the instance row exists with project = NULL. Per §2.7, NULL-project rows are wildcard-accessible from any project — so during this window, hcom messages from any project can target the just-registered Antigravity peer. If step 2 fails (network glitch, race with another writer on the same row, daemon restart), the instance stays NULL forever; the extension has no retry visible in the source.
src/commands/start.rs:77-81 shows the global --name is treated as the instance identity for hcom start. There is no --project flag accepted by hcom start, so the extension cannot register identity and project in one call.
For comparison, hcom's existing model for external participants (per README.md:215-223 and src/commands/start.rs):
- A user runs
hcom startfrom inside any external tool, terminal, or shell session. That session becomes anadhocinstance, distinguished bytool = "adhoc". Bootstrap text usesDELIVERY_ADHOC(src/bootstrap.rs:122-146): the agent must callhcom listenitself to receive messages. - The user's tool/IDE is not aware of hcom; it has no panel injection, no lifecycle tracking, no auto-registration.
- Identity is the bare CLI shell of
hcom start; bidirectional messaging is whatever the user chooses to do withhcom send/hcom listen.
The PR's Antigravity bridge differs from this model in three ways that are not represented in the existing taxonomy:
- The extension auto-registers itself on workspace open (
extensions/hcom-bridge/src/extension.ts:33-46), without user action inside Antigravity. - The extension runs a long-lived
hcom listen --jsonchild and parses its stdout (extensions/hcom-bridge/src/hcomClient.ts) — not the agent itself reading messages, but a wrapper process around the IDE. - The extension actively injects received hcom messages into Antigravity's chat panel via undocumented IDE commands (
antigravity.startNewConversation,antigravity.sendPromptToAgentPanel— see §3.8), so that the IDE-hosted agent (Jetski) sees them as if the user typed them.
This is closer in shape to a standalone connector application that bridges hcom and Antigravity than to a tool integration. The placement under src/hooks/antigravity.rs (which only installs the extension, registers no hook command names per §1) reflects that the integration does not fit the existing hook/PTY/plugin tiers.
3.8 Antigravity bridge depends on undocumented IDE commands
Antigravity's public docs cover:
- Agent Manager UI
- Chrome extension
.agents/workflows/config- Generic VS Code extension support
- MCP server registration in
~/.gemini/antigravity/mcp_config.json
The PR's extensions/hcom-bridge/src/jetskiBridge.ts invokes:
await vscode.commands.executeCommand('antigravity.startNewConversation');
await vscode.commands.executeCommand(
'antigravity.sendPromptToAgentPanel',
`${instructions}**[hcom message from @${sender}]**\n\n${text}`
);Neither antigravity.startNewConversation nor antigravity.sendPromptToAgentPanel appears in any documented surface. They are reverse-engineered IDE commands. Precedent for instability:
- The forum thread agent-manager-freezes-on-new-chat-unknown-service-agentsessions shows Google has already broken internal RPC service names between releases.
- The reverse-engineering writeup at alokbishoyi.com/blogposts/reverse-engineering-browser-automation describes the Jetski bridge surface as discovered via
ps/lsof/strings, not published. - Existing third-party tools (cafeTechne/antigravity-link-extension) integrate via Chrome DevTools Protocol, not internal commands.
src/hooks/antigravity.rs:97-145 writes the extension's metadata into ~/.antigravity/extensions/extensions.json using the internal VS Code marshalling format with $mid: 1 URI sentinels:
let entry = serde_json::json!({
"identifier": { "id": EXTENSION_ID },
"version": EXTENSION_VERSION,
"location": {
"$mid": 1,
"fsPath": ext_dir.to_string_lossy(),
"path": ext_dir.to_string_lossy(),
"scheme": "file"
},
...
});$mid: 1 is the VS Code URI marshalling internal format — undocumented for external use, subject to change.
The read-modify-write at src/hooks/antigravity.rs:84-130 has no file lock. Two concurrent hcom hooks add antigravity invocations corrupt the file. Antigravity itself writes this file on extension installs from its UI; that is also unhandled.
EXTENSION_VERSION = "0.1.0" is hardcoded as a Rust const. Bumping requires a Rust rebuild and a ship. There is no compatibility detection, no Antigravity-version pinning, no graceful degradation.
3.9 Project isolation overlaps two existing scoping primitives
hcom already has two isolation/scoping mechanisms on main. The PR adds a third without addressing the overlap.
HCOM_DIR — full per-project isolation at the filesystem level:
src/paths.rs::resolve_hcom_dir_from_env(src/paths.rs:26-53) — checks theHCOM_DIRenv var, expands~, resolves relative paths against cwd; falls back to$HOME/.hcomor./.hcom.src/paths.rs::db_path(src/paths.rs:82-84) — DB lives atHCOM_DIR/hcom.db.src/paths.rs::log_path(src/paths.rs:86-90) — logs atHCOM_DIR/.tmp/logs/.src/runtime_env.rs::tool_config_root(src/runtime_env.rs:26-35) — tool hook dirs (.claude/,.codex/,.gemini/,.opencode/) are placed underHCOM_DIR.parent(). SoHCOM_DIR=$PWD/.hcomputs.claude,.codex, etc. under$PWD.src/hooks/claude.rs:1967-1974andsrc/hooks/codex.rs:441-448— confirm tool hooks scope totool_config_root().
Two terminals each exporting their own HCOM_DIR get independent DBs, independent hooks, independent message streams. Isolation is enforced at the storage layer; no SQL filter to forget. This is the strongest available isolation guarantee.
HCOM_TAG — name-prefix scoping at the addressing level:
src/instances.rs::get_full_name(src/instances.rs:52-58) — instance display name becomes{tag}-{name}if tag is set:match &data.tag { Some(tag) if !tag.is_empty() => format!("{}-{}", tag, data.name), _ => data.name.clone(), }
src/instances.rs::resolve_display_name(src/instances.rs:68-88) — addressing matches: an@dev-lunamention splits into(tag=dev, name=luna)and resolves only if the instance withname=lunahastag="dev".src/launcher.rs:777-784— tag flows fromHCOM_TAGenv /--tagflag into the instance row.src/bootstrap.rs:316-322— instance-level tag overrides config-level tag at bootstrap time;src/bootstrap.rs:89-90adds theTAG_NOTICEblock telling agents"send @{tag}- -- msg"to address all peers in the same tag.
Tag is a soft name-based group: every agent is still in the same DB, every event stream visible to all, but addressing distinguishes groups. @dev matches all dev-* agents.
The PR adds --project as a third axis: a SQL column with read-side filters in send and list only (§2.6), broken on broadcast (§2.5), with NULL-as-wildcard backward-compat (§2.7).
The overlap matrix:
| Need | HCOM_DIR |
--tag |
--project (this PR) |
|---|---|---|---|
| Co-tenant projects in one DB | ✗ | ✓ (soft) | ✓ (partial) |
| Co-tenant projects with hard isolation | ✗ | ✗ | ✗ |
| Per-project independent DB / hooks | ✓ | ✗ | ✗ |
Filter hcom list view |
manual | (no flag) | ✓ |
Filter hcom events/listen/transcript |
✓ (different DB) | ✗ | ✗ (not implemented per §2.6) |
Group addressing (@group) |
✗ | ✓ | ✗ |
--project overlaps --tag for "co-tenant grouping" but adds no addressing affordance and removes none of HCOM_TAG's capabilities. It overlaps HCOM_DIR for "isolation" but provides a strictly weaker guarantee (one missed WHERE project=? and projects leak; SQL filtering vs. file-system separation).
--project overlaps --tag for "co-tenant grouping" but adds no addressing affordance. It overlaps HCOM_DIR for "isolation" with a strictly weaker guarantee (SQL filter vs. file-system separation; one missed WHERE project=? and projects leak). The PR's --project plumbing through SenderIdentity (src/shared/identity.rs:14), compute_scope (src/messages.rs:250-274), and cmd_send (src/commands/send.rs:933-945) is real work, but the conceptual question of why two label axes coexist is not addressed in the PR.
3.10 Project × relay has no design
hcom's relay is documented in README.md:147 as a single all-or-nothing trust domain:
hcom relayis one trust domain for one operator's devices. Membership is all-or-nothing. There are no scoped roles, read-only peers, or per-device permissions.
And README.md:164:
Per-device attribution inside a relay. Sender identity is routing metadata, not authorization. Every enrolled device speaks with full authority.
The PR adds project to the remote launch envelope (src/relay/control.rs:636, 657, 742), so a project-A agent can be spawned on a remote device. But there is no equivalent project-awareness in:
- Event replication.
src/relay/push.rs::build_push_payload(line 112) andsrc/relay/pull.rs::import_remote_events(line 522) move events between devices without project filtering. A project-A agent sending a broadcast on device X has its event replicated to device Y; any project-B instance on Y receiving viaget_unread_messageswill see it (per §2.5's read-path issue). - Subscription replication. Cross-device subscriptions (
hcom events sub --device ID --for <name>) carry the existing filter set; there is no project field onEventsSubArgs(src/commands/events.rs:69-92). - Transcript fetch.
hcom transcript <name>:DEVICEdoes not gate by project. - NULL-project remote peers. A remote device that registered an instance without
--projectgets NULL-project, which under §2.7's wildcard semantics is visible to/from every project on every relay-connected device.
Open questions the PR does not address:
- Whether project propagates across devices (a project-A agent on device X talks to project-A agents only on device Y).
- Whether each device has independent project namespaces (project "foo" on X is distinct from project "foo" on Y).
- Whether relay-level trust always supersedes project (any relay member sees everything regardless of project).
3.11 Antigravity bridge embeds a build artifact in the source tree
src/antigravity_extension/extension.js (340 lines, single-line minified) is the esbuild output of extensions/hcom-bridge/src/extension.ts + hcomClient.ts + jetskiBridge.ts. src/hooks/antigravity.rs:9-10:
pub const EXTENSION_JS: &str = include_str!("../antigravity_extension/extension.js");
pub const PACKAGE_JSON: &str = include_str!("../antigravity_extension/package.json");Workflow per change:
- Edit TS at
extensions/hcom-bridge/src/. - Run
extensions/hcom-bridge/build.sh(which would fail due to §2.3). - Copy the build output to
src/antigravity_extension/. - Commit both the source and the artifact.
- Rebuild the Rust binary.
The TS source root and the committed minified blob are two sources of truth that can drift. The build process is manual: edit TS → run build.sh → copy to embed dir → commit both → rebuild Rust.
4. Code duplication
4.1 src/hooks/cline.rs ↔ src/hooks/kilo.rs
Both files implement: argv parsing helpers, plugin install/verify/remove/scan, session-binding handle_start, status forwarding handle_status, message read handle_read, finalize handle_stop, hook dispatcher.
Functions byte-identical or near-identical:
| Function | cline.rs lines | kilo.rs lines | Difference |
|---|---|---|---|
parse_flag |
17-22 | 17-22 | none |
has_flag |
24-26 | 24-26 | none |
parse_value_arg |
28-44 | 28-44 | none |
parse_launch_model |
46-54 | 46-54 | none |
launch_agent_and_model_from_args |
56-75 | 56-75 | none |
verify_*_plugin_installed |
176-185 | 545-555 | tool name string |
install_*_plugin |
186-199 | 556-575 | tool name string |
remove_*_plugin |
200-235 | 576-611 | tool name string |
ensure_plugin_installed |
237-244 | 615-622 | tool name string |
handle_status |
(status section) | (status section) | tool name string |
handle_read |
(read section) | (read section) | tool name string |
handle_stop |
(stop section) | (stop section) | tool name string |
Approximate duplicate LOC: ~250.
4.2 hcom_kilo.ts ↔ kilo-hcom.ts ↔ hcom.ts (triple fork)
Three files, all nearly identical, in src/opencode_plugin/:
| File | Lines | Status | hcom subcommand prefix |
|---|---|---|---|
hcom.ts |
21KB on main, untouched by PR | live (used by OpenCode plugin) | opencode-* |
kilo-hcom.ts |
496 lines | live (used by Kilo plugin via include_str! in src/hooks/kilo.rs:483) |
kilo-* (mostly) + kilocode-status (one occurrence) |
hcom_kilo.ts |
506 lines | dead (zero references in repo: git grep hcom_kilo returns empty) |
opencode-* |
kilo-hcom.ts is hcom.ts with s/opencode-/kilo-/g and a few additional changes:
- Line 1:
@opencode-ai/plugin→@kilocode/plugin - Line 7:
LOG_PATHfromhcom.log→hcom-kilo.log - Line 53: subsystem
"plugin"→"kilo-plugin" - Line 214: one inconsistency — uses
kilocode-status(this is actually the only call that matches a registered hook, see §2.1)
hcom_kilo.ts is an older draft: still uses opencode-* everywhere and contains an agent-prompt injection block at lines 489-499 that reads ${HCOM_DIR}/agents/${instanceName}.md from the plugin side. That injection block does not appear in the live kilo-hcom.ts. (Per §3.1, agent prompts still reach launched agents via the Rust-side bootstrap path at src/bootstrap.rs:465-468; the plugin-side variant in hcom_kilo.ts was a separate injection mechanism that is unreferenced by anything in the repo.)
Future bug fixes to hcom.ts apply to one of three forks; hcom_kilo.ts is unreachable so fixes there are inert; the kilo and opencode plugins can diverge silently.
KiloCode is an OpenCode fork (@kilocode/plugin SDK is forked from @opencode-ai/plugin); the plugin contracts are compatible.
5. Items the PR adds correctly
5.1 Schema migration v17 → v18
src/db.rs:34-37:
(18, "ALTER TABLE instances ADD COLUMN project TEXT DEFAULT '';"),Single-column ADD with a default. Mechanical, safe. The PR also retains the existing v17→v18 repair test pattern at src/db.rs:3754 and the "stamped but not migrated" repair test at src/db.rs:3872 — the migration system continues to be maintained correctly.
5.2 mark_dead_instances — what it actually adds
The snapshot-and-resume capability is not new in this PR. src/hooks/common.rs::stop_instance (specifically stop_instance_inner at src/hooks/common.rs:937-1184) already builds a snapshot JSON object and writes it via log_life_event with action "stopped". The 18 fields the PR's mark_dead_instances snapshot includes (name, transcript_path, session_id, tool, directory, parent_name, tag, wait_timeout, subagent_timeout, hints, pid, created_at, background, agent_id, launch_args, origin_device_id, background_log_file, last_event_id — src/instance_lifecycle.rs:792-810) are byte-identical to the stop_instance snapshot at src/hooks/common.rs:1082-1099.
The resume path reads from the same lifelog stream regardless of which path produced it. src/instances.rs::resolve_display_name_or_stopped (src/instances.rs:91-110) queries:
SELECT instance FROM events
WHERE type = 'life'
AND instance = ?1
AND json_extract(data, '$.action') = 'stopped'
LIMIT 1hcom r <name> (src/commands/resume.rs::do_resume) calls this resolver and then loads the snapshot to rebuild LaunchParams. So any mechanism that writes a stopped life event with a snapshot makes the instance resumable.
What mark_dead_instances adds, then, is a faster detection trigger for dead PIDs, not a new resume capability. Concretely, comparison of the existing cleanup paths and the new one:
| Path | When it fires | Detection signal | Snapshot? | Resumable after? |
|---|---|---|---|---|
hcom stop <name> (user) → stop_instance (src/hooks/common.rs:929) |
User runs hcom stop |
n/a (explicit) | ✓ | ✓ |
Hook close (Stop/PostToolUse exit) → finalize_session → stop_instance |
Vendor hook fires on session end | hook callback | ✓ | ✓ |
cleanup_stale_instances (src/instance_lifecycle.rs:651-718), invoked from src/commands/list.rs:84-85 |
Lazy on hcom list |
heartbeat timer thresholds (HEARTBEAT_THRESHOLD_TCP=35s / NO_TCP=10s), then exit/stale/inactive timers (60s / 3600s / 12hr) |
✓ (via stop_instance) |
✓ |
cleanup_stale_remote_instances (src/instance_lifecycle.rs:720-754) |
Same as above | relay_sync_time_<device> KV staleness (90s) |
direct DELETE only — no snapshot | ✗ |
hcom start --orphan <name|pid> (src/commands/start.rs) |
User-invoked | user-supplied | n/a (recovery, not stop) | n/a |
mark_dead_instances (this PR, src/main.rs:67-69) |
Every CLI invocation | pidtrack::is_alive(pid) syscall per row |
✓ (duplicates stop_instance's snapshot) |
✓ |
The reboot scenario without this PR:
- Reboot kills agent process.
- After heartbeat threshold (10–35s with no hook activity),
get_instance_status(src/instance_lifecycle.rs:157-296) returnsST_INACTIVEwith context"stale". - Up to
max_stale_secondslater (default 3600s whencleanup_stale_instancesis called fromhcom list), the instance is cleaned up viastop_instance, which writes the snapshot. - Resumable.
Total time-to-resumability post-reboot: up to ~1 hour from the next hcom list.
The reboot scenario with mark_dead_instances:
- Reboot kills agent process.
- Next
hcom <anything>invocation runsmark_dead_instances. - PID is
!is_alive(), snapshot written, instance deleted. - Resumable.
Total time-to-resumability post-reboot: next CLI invocation.
That is the genuine improvement. Two qualifications:
(a) The snapshot building duplicates stop_instance. mark_dead_instances reproduces ~50 lines of cleanup logic (snapshot serialization, session_bindings / process_bindings / notify_endpoints / subscriptions cleanup, log_life_event, delete_instance) instead of calling stop_instance(db, &inst.name, "system", "exit:reboot") directly. stop_instance_inner:962-997 does send SIGTERM/SIGKILL for headless PIDs, but per its inline comment "ESRCH/EPERM from initial killpg is fine — process already gone or foreign" — calling it on a dead PID is a no-op. The duplication is unjustified. A two-line mark_dead_instances (find-dead → call stop_instance with "exit:reboot") would be equivalent.
(b) The placement (src/main.rs:67-69, every CLI invocation) is heavy for a rare event. See §3.3. The detection-speed improvement matters at the first interaction post-reboot; the per-invocation scan repeats it indefinitely thereafter.
The conceptual addition — using PID liveness as a synchronous death signal rather than waiting for heartbeat decay — is the genuine increment. The implementation duplicates the existing snapshot machinery and is placed on the per-CLI-invocation path.
5.3 SenderIdentity::project as a first-class field
src/shared/identity.rs:14, src/identity.rs:27-32:
fn extract_project(data: &serde_json::Value) -> Option<String> {
data.get("project")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
}Project on the sender's identity, plumbed through compute_scope. The shape is correct; the implementation is incomplete (§2.5, §2.6) but the abstraction is right.
5.4 Mention filtering by project
src/messages.rs:262-266 correctly filters mentioned instances by project before producing MessageScope::Mentions. The path hcom send --project foo @bar -- "hi" gates correctly. Only broadcast leaks (§2.5).
5.5 List filter
src/commands/list.rs:217-234 filters the list view by --project. Implementation matches the send mention path. Same NULL-as-wildcard caveat as §2.7.
6. Test coverage
6.1 test_pty_opencode was renamed, not added-alongside
tests/test_pty_delivery.rs:1448-1452 (PR diff):
-fn test_pty_opencode() {
- run_pty_test_opencode();
+fn test_pty_cline() {
+ run_pty_test_cline();
}The function run_pty_test_opencode at tests/test_pty_delivery.rs:844 still exists in the file. No #[test] calls it. Net effect: the only end-to-end PTY-bootstrap test for the OpenCode tier is silently dropped in a PR that did not touch OpenCode behavior. Cline gains a test, OpenCode loses one.
6.2 No project isolation tests
No test in src/messages.rs, src/commands/send.rs, src/commands/list.rs, or tests/ asserts:
- Message in project A is not delivered to instance in project B (regression test for §2.5).
hcom list --project Adoes not return project B instances.- NULL-project instances are visible from any project (the documented backward-compat behavior).
The mention path is non-trivial filter logic without coverage.
6.3 No Kilo PTY test
PR adds Tool::Kilo and an entire 666-line hook handler. No PTY end-to-end test. The Cline test added (test_pty_cline, tests/test_pty_delivery.rs:1144-1429) is #[ignore] and likely not CI-gated; even so, no Kilo equivalent exists. The Kilo command-name mismatch (§2.1) would have surfaced in any end-to-end test.
6.4 Send tests culled
src/commands/send.rs test module reorganization removed send_message_thread_without_members_errors and merged adjacent test functions. Coverage net change is negative.
7. PR description vs. behavior
| PR description claim | Behavior |
|---|---|
| "Kilo agent support (hooks, preprocessing, bootstrap)" | Hook command names mismatch (§2.1a); upstream namespace mismatch (§2.1b) |
| "Cline agent support (--tui, auto-ack, DELIVERY_AUTO, 5s timeout)" | Install path does not match Cline's load path (§3.5); DELIVERY_AUTO claim does not match shipped delivery layers (§3.6); JSON parsing via sed is fragile (§3.4) |
| "Antigravity IDE hcom-bridge extension" | TS source does not compile (§2.3); committed minified blob is the runtime artifact |
| "Project isolation (--project flag across all commands)" | --project flag exists on send and list only; broadcasts pass the project filter (§2.5); read path is project-blind (§2.5); config/env propagate to env but not to instance row (§2.5b); events/listen/transcript/archive/bundle are unfiltered (§2.6a); NULL-project rows are wildcard-accessible (§2.7); relay does not propagate project (§3.10) |
| "Custom agent names via --agent-name" | Skips CVCV pool reservation, similarity check, cross-device collision detection (§3.2); silently dropped on remote launches (§3.2) |
| "Agent prompts via ~/.hcom/agents/" | Reachable via bootstrap::get_bootstrap for tool paths that call it (§3.1); overlaps existing instruction-injection layers |
| "Dead agent detection on startup" | Runs on every CLI invocation, not startup (§3.3) |
| "TUI improvements (launch panel, keybindings, Fill layout)" | Left/right cursor regression in launch panel (§2.8) |
8. References
hcom internals:
- Bootstrap:
src/bootstrap.rs:111-146(DELIVERY_AUTO/DELIVERY_ADHOCconstants),src/bootstrap.rs:367-455(get_bootstrap),src/bootstrap.rs:437-439(existing## NOTESinjection from thenotesparameter — relevant to §3.1) - Tool registry & ready patterns:
src/tool.rs:1-150(Toolenum,KILO_HOOKS/CLINE_HOOKSarrays,ready_pattern,from_hook_name) - Launch tiering:
src/launcher.rs::LaunchTool(src/launcher.rs:30-90),LaunchBackend::for_tool(src/launcher.rs:108-115),ensure_hooks_installed(src/launcher.rs:380-415) - Delivery state machine:
src/delivery.rs::run_delivery_loop(~line 600),ToolConfig::for_tool(src/delivery.rs:330-340) - Identity flow:
src/identity.rs::resolve_identity_with_expectation,src/shared/identity.rs::SenderIdentity - Instance lifecycle:
src/instance_lifecycle.rs::cleanup_stale_instances,cleanup_stale_remote_instances,mark_dead_instances(added by this PR at lines 762-848) - DB schema and migrations:
src/db.rs:34-37(migrations array),src/db.rs:357-410(instance table CREATE),src/db.rs::INSTANCE_COLUMNS(~line 2882) - Hook router:
src/hooks/mod.rs, per-tool handlers insrc/hooks/{claude,gemini,codex,opencode}.rs - OpenCode plugin install pattern (template for §4.2 refactor):
src/hooks/opencode.rs::install_opencode_plugin,verify_opencode_plugin_installed,get_opencode_plugin_dir - Send pipeline:
src/commands/send.rs::cmd_sendandsend_message,src/messages.rs::compute_scope,should_deliver_message - Stale-cleanup invocation site (relevant to §3.3):
src/commands/list.rs:84-85
Cline upstream:
- Hooks proto: https://github.com/cline/cline/blob/main/proto/cline/hooks.proto
- Hook docs: https://github.com/cline/cline/blob/main/docs/customization/hooks.mdx
- ACP: https://github.com/cline/cline/tree/main/cli/src/acp
- CLI: https://github.com/cline/cline/tree/main/cli/src
KiloCode upstream:
- Plugin SDK: https://www.npmjs.com/package/@kilocode/plugin
- Repo: https://github.com/Kilo-Org/kilocode
OpenCode upstream:
- Plugin SDK: https://www.npmjs.com/package/@opencode-ai/plugin
- Repo: https://github.com/anomalyco/opencode
Antigravity:
- Public docs: https://antigravity.google/docs/
- Reverse-engineering writeup: https://alokbishoyi.com/blogposts/reverse-engineering-browser-automation.html
- agentSessions crash forum thread: https://discuss.ai.google.dev/t/bug-agent-manager-freezes-on-new-chat-unknown-service-agentsessions-leading-to-silent-extension-host-crash/136921
- Third-party precedent (CDP-based, not internal-API-based): https://github.com/cafeTechne/antigravity-link-extension
Claude Code subagents (for §3.1 comparison):
VS Code extension URI marshalling ($mid: 1):
9. Summary of state
The PR's three new tool integrations and its project-isolation feature do not function as the PR description describes:
- Kilo — hook command names registered in
src/tool.rsdo not match the names invoked by the embedded plugin (§2.1a); DB, config, and plugin paths use thekilocode/*namespace while upstream Kilo writes and readskilo/*(§2.1b). - Cline — installed hook scripts live in directories Cline does not read from (§3.5); the
DELIVERY_AUTOclaim in the bootstrap text does not match the shipped delivery layers (§3.6); JSON parsing in the hook scripts is viased/awk(§3.4). - Antigravity bridge — TS source does not compile (§2.3); the
--projectflag the bridge passes is positioned after--and is consumed as message text (§2.4); the bridge invokes undocumented Antigravity IDE commands (§3.8); registration is non-atomic across twohcomcalls with a NULL-project window (§3.7); the committed minified bundle is the working artifact while its TS source is broken (§3.11). - Project isolation — bootstrap text tells agents "You can only see and message other agents in the same project." The actual mechanism: the write path filters mention candidates only; the events table stores no project metadata; the read path's
should_deliver_to(src/db.rs:895-922) is project-blind; broadcasts pass the filter (§2.5); onlysendandlistaccept--projectwhileevents/listen/transcript/archive/bundlesee all projects (§2.6a); subscriptions are active delivery channels and are not project-aware (§2.6a); NULL-project rows are wildcard-accessible (§2.7); config/env project reaches the child env but not the instance row used for routing (§2.5b); relay does not propagate or partition project (§3.10). - OpenCode
--formatauto-ack — the PR changes shared hook semantics from at-least-once delivery to at-most-once-with-silent-loss (§2.6b), unrelated to the PR's stated features. - Remote
--agent-name— the flag is accepted locally but discarded by the relay handler (§3.2). - TUI launch panel — Left/Right cursor inputs unconditionally cycle the tool selector (§2.8).
The cross-cutting features overlap existing primitives in hcom (§0.5):
--projectoverlapsHCOM_DIR(full filesystem-level isolation) andHCOM_TAG(soft groups + addressing).--agent-nameoverlapsHCOM_TAG-based naming andhcom start --as <name>rebinding.~/.hcom/agents/<name>.mdoverlapsHCOM_NOTES/globalnotes,--hcom-prompt, and Claude's native.claude/agents/<name>.mdvia--agent.mark_dead_instancesoverlapscleanup_stale_instances(called lazily fromhcom list) andstop_instance(which already produces byte-identical resume snapshots).
Code shape:
src/hooks/cline.rs(618 lines) andsrc/hooks/kilo.rs(666 lines) duplicate ~250 LOC of parsing helpers and plugin install/verify/scan code (§4.1).src/opencode_plugin/contains three near-identical plugin sources:hcom.ts(live, used by OpenCode),kilo-hcom.ts(live, used by Kilo),hcom_kilo.ts(zero references in the repo) (§4.2).src/hooks/antigravity.rslives undersrc/hooks/but contributes no hook command names; it is an extension installer (§1).- The PR introduces two new integration tiers (hook-script bridge for Cline; VS Code extension peer for Antigravity) but routes them through the existing
Toolenum andLaunchToolenum without naming the new tiers (§1).
Test coverage:
- The
test_pty_opencode#[test]entry was renamed totest_pty_cline;run_pty_test_opencoderemains in the file but is unreachable from any test entry (§6.1). - No tests assert project isolation behavior (§6.2).
- No PTY end-to-end test exists for
Tool::Kilo(§6.3).
PR lineage: the same architecturally substantive issues (Kilo command-name mismatch, Cline integration mechanism, Antigravity IDE-command coupling, project broadcast leak) appear unchanged across PR #37 → #38 → #39 → #40 → #41 → #42, while metadata-level changes (fork URLs, vendored node_modules, committed dist/ artifacts) have been removed across iterations.
Summary
Full PR with individual commits visible, zero fork-specific changes.
Features