Skip to content

feat: Kilo, Cline, Antigravity IDE, project isolation, launch panel#42

Open
Solar2004 wants to merge 1 commit into
aannoo:mainfrom
Solar2004:pr-final
Open

feat: Kilo, Cline, Antigravity IDE, project isolation, launch panel#42
Solar2004 wants to merge 1 commit into
aannoo:mainfrom
Solar2004:pr-final

Conversation

@Solar2004
Copy link
Copy Markdown

Summary

Full PR with individual commits visible, zero fork-specific changes.

Features

  • Kilo agent support (hooks, preprocessing, bootstrap)
  • Cline agent support (--tui, auto-ack, DELIVERY_AUTO, 5s timeout)
  • Antigravity IDE hcom-bridge extension
  • Project isolation (--project flag across all commands)
  • TUI improvements (launch panel, keybindings, Fill layout)
  • Custom agent names via --agent-name
  • Agent prompts via ~/.hcom/agents/
  • Dead agent detection on startup

@Solar2004 Solar2004 force-pushed the pr-final branch 2 times, most recently from 8378183 to 6b064f9 Compare May 3, 2026 23:44
@aannoo
Copy link
Copy Markdown
Owner

aannoo commented May 4, 2026

@Solar2004 Thanks for contributing! Can you split into seperate PRs?


AI review:

PR #42 — Architectural Review

Repo: aannoo/hcom
PR: #42feat: Kilo, Cline, Antigravity IDE, project isolation, launch panel
Author: Solar2004
Head: pr-final @Solar2004 a758515d523b11bcd57ac99a20193a4a7`
Diff: 80 files, +5,491 / −275


0. Scope

PR #42 bundles six unrelated changes in one commit:

  1. KiloCode tool integration — new Tool::Kilo variant, src/hooks/kilo.rs (666 lines), embedded plugin src/opencode_plugin/kilo-hcom.ts.
  2. Cline tool integration — new Tool::Cline variant, src/hooks/cline.rs (618 lines), five shell hook scripts in src/opencode_plugin/.
  3. Antigravity IDE bridge — VS Code extension at extensions/hcom-bridge/, Rust installer at src/hooks/antigravity.rs, minified bundle at src/antigravity_extension/extension.js.
  4. Project isolation--project flag, DB column (migration v18), SenderIdentity.project, sender-side filter in compute_scope.
  5. Launch panel TUIsrc/tui/render/launch.rs, src/tui/input.rs keybindings.
  6. Cross-cutting inframark_dead_instances reboot recovery, agent_prompts.rs per-agent markdown loader, dead-agent detection on every CLI invocation.

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 new

Before per-defect detail, an inventory of which PR features add capabilities not present on main and which add parallel mechanisms for capabilities that already exist.

Already exists in hcom (PR adds parallel mechanism)

PR feature Existing equivalent on main
--project isolation HCOM_DIR=$PWD/.hcom (full per-project DB+hooks isolation; src/paths.rs:26-90, src/runtime_env.rs:26-35); HCOM_TAG/--tag (soft groups + addressing via @tag-name and @tag-; src/instances.rs:52-58, 68-88)
--agent-name custom names HCOM_TAG produces api-luna-style names (src/instances.rs:52-58); hcom start --as <name> rebinds identity (src/commands/start.rs:34-35); HCOM_NAME_EXPORT exposes auto-name to a user-chosen env var (src/launcher.rs:935-941); per-instance config via hcom config -i <name>
~/.hcom/agents/<name>.md per-agent prompts --hcom-prompt "$(cat file.md)" (any file, any tool); HCOM_NOTES / global hcom config notes renders as ## NOTES in bootstrap (src/bootstrap.rs:437-439); Claude's native .claude/agents/<name>.md via --agent <name>; hints via hcom config -i <name> hints for per-message injection
Dead-agent snapshot for resume stop_instance already snapshots and writes the life/stopped event with byte-identical 18 fields (src/hooks/common.rs:1082-1099); resolve_display_name_or_stopped (src/instances.rs:91-110) reads it for hcom r <name>; cleanup_stale_instances runs lazily on hcom list (src/commands/list.rs:84-85)
Cline DELIVERY_AUTO bootstrap claim The two-layer model (PTY inject + hook callback) already implements DELIVERY_AUTO for Claude/Gemini/Codex (see §3.6)
Plugin install scaffolding (cline/kilo) Same install/verify/scan pattern as src/hooks/opencode.rs::install_opencode_plugin etc.

New capabilities

Feature What it adds
KiloCode tool integration New tool integration (KiloCode is an OpenCode fork)
Cline tool integration New tool integration
Antigravity bridge VS Code extension peer talking to hcom listen --json; not represented in the existing Tool enum
Co-tenant projects in one DB The one thing HCOM_DIR does not provide — multiple projects in a single store; implementation per §2.5–§2.6, §3.10
TUI launch panel Spawn-from-TUI UX
@hcom chat participant Chat-panel surface in Antigravity IDE

Bug fix, not a feature

  • bootstrap.rs::get_active_instances switched from a status_time >= cutoff heuristic to get_instance_status (src/bootstrap.rs:209-228 in PR diff). Correctness fix.

Refinement, not a new capability

  • mark_dead_instances — same dead-detection that already runs lazily on hcom list, but triggered by pidtrack::is_alive(pid) syscall instead of heartbeat-timer thresholds (§5.2). Faster post-reboot detection (next CLI invocation vs. up to ~1hr); the snapshot model itself is duplicate of stop_instance.

1. Integration tier taxonomy

The codebase implements two integration shapes for tools on main:

Tier Mechanism Tools Delivery path
1 PTY-wrapped TUI Claude, Gemini, Codex Hcom owns the PTY (src/launcher.rs::LaunchBackend::HeadlessPty); runs the delivery state machine in src/delivery.rs (PendingWaitTextRenderWaitTextClearVerifyCursor)
2 TS plugin, embedded via include_str! OpenCode Plugin source at src/opencode_plugin/hcom.ts, embedded at src/hooks/opencode.rs::PLUGIN_SOURCE (include_str!); PTY state machine bypassed at src/delivery.rs:614 (Tool::OpenCode early-exit in run_delivery_loop)

Per-tool gate config is centralized at src/delivery.rs::ToolConfig::for_tool (src/delivery.rs:330-340); ready-pattern detection is at src/tool.rs::Tool::ready_pattern (src/tool.rs:48-60).

PR #42 introduces two more tiers without naming or documenting them:

Tier Mechanism Tools (this PR) Delivery path
3 Hook-script bridge Cline Vendor-side hooks → shell scripts → hcom <tool>-* argv subcommands
4 VS Code extension peer Antigravity Extension spawns hcom listen --json as child; brokers via IDE commands

Cline and Antigravity are forced into the existing Tool enum and LaunchTool enum even though their integration shapes are fundamentally different. Concretely:

  • Tool::Cline gets ready_pattern = b"ctrl+p commands" (src/tool.rs:55) — that is OpenCode's TUI footer (anomalyco/opencode source). Cline's TUI is React-Ink based (cline/cline cli/src) and does not display that string.
  • Tool::Cline is routed through OpenCode's delivery config: src/delivery.rs:337 maps Tool::Cline => Self::opencode(). This works only because src/delivery.rs:614 adds Cline to the early-exit that skips the entire gate machine.
  • Antigravity has no Tool variant and cannot be launched via hcom; its hook module src/hooks/antigravity.rs registers zero hook command names and contributes nothing to Tool::hooks() (src/tool.rs:62-71). Despite this, it lives under src/hooks/. The placement is misleading.

A more honest design would name the tiers (LaunchBackend::HookBridge, Integration::ExternalPeer) so the taxonomy matches the code shape.


2. Functional defects (will not work as shipped)

2.1 Kilo integration — two independent fatals

The 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

src/tool.rs:41-46 registers exactly four hook subcommand names for Tool::Kilo:

const KILO_HOOKS: &[&str] = &[
    "kilocode-start",
    "kilocode-status",
    "kilocode-read",
    "kilocode-stop",
];

The embedded plugin src/opencode_plugin/kilo-hcom.ts calls eight different subcommand strings:

line 138:  hcom kilo-read --name ${instanceName}
line 214:  hcom kilocode-status --name ${instanceName} --status ${hcomStatus}
line 273:  hcom kilo-start --session-id ${sid} --notify-port ${port}
line 274:  hcom kilo-start --session-id ${sid}
line 335:  hcom kilo-status ... --status blocked --context approval
line 348:  hcom kilo-status ... --status ${hcomStatus}
line 373:  hcom kilo-status ... --status ${hcomStatus}
line 388:  hcom kilo-stop --name ${instanceName} --reason closed
line 404:  hcom kilo-status ... --status active --context tool:write
line 473:  hcom kilo-read --name ${instanceName} --ack --up-to ${ackId}

Only line 214 (kilocode-status) matches a registered hook. The plugin is internally inconsistent: nine kilo-* calls, one kilocode-* call.

Effect: Kilo session binding (kilo-start), message reading (kilo-read), ack (kilo-read --ack), and shutdown (kilo-stop) all fail to dispatch. Tool::from_hook_name (src/tool.rs:73-79) returns None, the router emits no handler, the plugin sees a non-zero exit from each invocation. Status updates partially work because kilocode-status is the one matching name.

This bug has been present unchanged across PR #37#38#39#40#41#42 per prior reviews.

2.1b Wrong upstream namespace — kilocode/* vs kilo/*

The PR consistently writes and reads under kilocode namespaces:

  • src/hooks/kilo.rs:95-103 (get_kilocode_db_path): $XDG_DATA_HOME/kilocode/kilocode.db
  • src/hooks/kilo.rs:500-508 (get_kilocode_plugin_dir): global $XDG_CONFIG_HOME/kilocode/plugins, project-local .kilocode/plugins
  • src/hooks/kilo.rs:516-535 (scan_plugin_dirs): scans $XDG_CONFIG_HOME/kilocode/{plugin,plugins}, env KILOCODE_CONFIG_DIR, .kilocode/{plugin,plugins}
  • src/commands/resume.rs:1233-1238: derives kilocode/kilocode.db for transcript path

KiloCode upstream uses the kilo namespace, not kilocode:

Effect:

  • Hcom looks for KiloCode's session DB at $XDG_DATA_HOME/kilocode/kilocode.db. KiloCode actually writes it under Global.Path.data (an XDG path keyed on app="kilo") as kilo.db. Hcom never finds the DB → transcript_path recorded on the instance row points to a non-existent file → hcom transcript <kilo-name> returns nothing, hcom resume cannot replay.
  • Hcom installs the plugin to $XDG_CONFIG_HOME/kilocode/plugins/. KiloCode looks under the kilo config root. The plugin is never loaded.
  • Hcom honors KILOCODE_CONFIG_DIR. KiloCode honors KILO_CONFIG_DIR. A user's existing config-dir override is invisible to hcom; setting KILOCODE_CONFIG_DIR does nothing for Kilo.

The project-local case (.kilocode/plugins/) may work because KiloCode does honor .kilocode/ as a project config dir name (legacy from before the rename to kilo), but the global case definitely does not.

The two parts of §2.1 are independent. Fixing only the hook command names (§2.1a) leaves the plugin in a directory KiloCode does not read; fixing only the paths (§2.1b) leaves the dispatched commands as hcom kilo-* against a registry that only knows hcom kilocode-*. Both fixes are required.

2.2 Recovery message points to a CLI-rejected verb

src/launcher.rs:404 (PR diff):

LaunchTool::Kilo => {
    if crate::hooks::kilo::ensure_plugin_installed() {
        return Ok(());
    }
    let diag = install_diag_context(tool, &[]);
    bail!("Failed to setup Kilo plugin. Run: hcom hooks add kilo\n{diag}");
}

src/commands/hooks.rs:19 defines the accepted verbs:

const HOOK_TOOLS: &[&str] = &["claude", "gemini", "codex", "opencode", "kilocode", "cline", "clinecode", "antigravity"];

hooks add matching at src/commands/hooks.rs:99, 146, 225 accepts "kilocode" only — never "kilo". A user following the error message gets:

Valid options: claude, gemini, codex, opencode, kilocode, cline, clinecode, antigravity, all

Two-line break: the launcher tells the user one command; the CLI rejects it.

2.3 Antigravity extension fails to compile

extensions/hcom-bridge/src/extension.ts imports:

import * as vscode from 'vscode';
import * as fs from 'fs';
import { HcomClient } from './hcomClient';
import { JetskiBridge } from './jetskiBridge';

It then uses path.join in logToFile:

const logDir = path.join(home, '.hcom', 'extensions');

path is never imported. extensions/hcom-bridge/build.sh runs tsc first (VS Code extension build pattern), so tsc will reject the file with TS2304: Cannot find name 'path'.

The committed minified blob src/antigravity_extension/extension.js (340-line single-line bundle) was generated when path was imported (its require chain includes path). The TS source in extensions/hcom-bridge/ is therefore stale relative to the JS in src/antigravity_extension/. The include_str! mechanism at src/hooks/antigravity.rs:9-10 ships the minified runtime artifact, so the Rust binary's behavior may not reveal this — but the source-of-truth in extensions/hcom-bridge/ cannot be rebuilt.

2.4 Antigravity bridge --project is positional argument, not flag

extensions/hcom-bridge/src/hcomClient.ts:243-247:

const args = ['send', `@${target}`, '--', message];
if (project) {
    args.push('--project', project);
}

hcom send argument grammar (src/commands/send.rs and hcom send --help):

Everything after -- is the message (no quotes needed). All flags must come before --.

So args = ['send', '@target', '--', 'hello world', '--project', 'foo'] results in the message text "hello world --project foo". The --project flag is never set. Project isolation is non-functional through the Antigravity bridge regardless of how the rest of the project plumbing works.

2.5 Project broadcast bypasses the project filter — wrong layer entirely

The PR filters at the wrong architectural layer. The filter runs at message write time over the candidate-instance set, but the events table stores a normal message envelope with scope: "broadcast" and no project metadata. Future readers re-evaluate from the stored event, not from the original candidate set.

The send-side filter is at src/messages.rs:250-274 (the compute_scope function). It correctly filters enabled_instances by project before producing the MessageScope. For broadcast scope, the function returns MessageScope::Broadcast with no per-instance carryover.

Then src/commands/send.rs:317-322:

let delivered_to: Vec<String> = rows
    .iter()
    .filter(|inst| {
        should_deliver_message(&scope_data, &inst.name, &identity.name).unwrap_or(false)
    })
    .map(|inst| inst.name.clone())
    .collect();

scope_data contains only scope, optionally mentions, optionally group_id. It does not contain the project filter that was applied upstream. should_deliver_message for a broadcast scope returns true for every instance.

But this is only the immediate symptom. The deeper architectural issue is that the read path is also project-blind. src/db.rs:895-922 (should_deliver_to):

fn should_deliver_to(json: &serde_json::Value, receiver: &str) -> bool {
    let from = json.get("from").and_then(|v| v.as_str()).unwrap_or("");
    if from == receiver { return false; }
    let scope = json.get("scope").and_then(|s| s.as_str()).unwrap_or("broadcast");
    match scope {
        "broadcast" => true,
        "mentions" => { /* ... mention list match ... */ }
        _ => false,
    }
}

src/db.rs:971-1016 (get_unread_messages) calls should_deliver_to on every message event past the receiver's last_event_id. There is no project field examined.

So even if §2.5's send-time filter were fixed (project carried into delivered_to for broadcasts), the message is still written into the events table as a generic broadcast envelope. The next time any instance polls — for instance, a project-B agent calling hcom listen or hitting a PostToolUse hook — get_unread_messages re-evaluates the stored events and treats the project-A broadcast as deliverable. The delivered_to array is metadata for tracking; it does not gate future reads.

As shipped: bootstrap claims isolation; the write path filters the candidate set for mentions and delivered_to metadata; the read path is project-blind; the event envelope has no project metadata. The bootstrap text and the actual delivery semantics describe different system behaviors.

2.5b Config/env project reaches the child but not the instance row

The PR adds launch.project config and HCOM_PROJECT env var. Both reach the child process via build_launch_env:

  • src/config.rs::FIELD_TO_ENV includes ("project", "HCOM_PROJECT") (line 117).
  • src/config.rs::HcomConfig::to_env_dict (line 632) emits HCOM_PROJECT for nonempty configs.
  • src/launcher.rs::build_launch_env (line 217) inserts the result into the spawn env.

So hcom config project foo and HCOM_PROJECT=foo hcom claude both put HCOM_PROJECT=foo in the child's environment.

The instance row in the DB is populated separately:

src/commands/launch.rs:34:

let project = hcom_flags.project.clone();  // CLI flag only

src/launcher.rs:1010:

params.project.as_deref(),  // → instance row's project column

params.project is sourced only from the --project CLI flag. There is no env-then-config fallback. Tag's resolution at src/launcher.rs:806-815 does have that fallback — params.tag falls back to HCOM_TAG env, then hcom_config.tag config. Project does not.

Effect: launches that rely on config or env for project end up with HCOM_PROJECT=foo in the child's environment but project = NULL on the instance row. Identity/routing reads from the row (see src/identity.rs::extract_project reading from instance JSON), not from the env. Per §2.7, a NULL-project row is wildcard-accessible.

So the child sees HCOM_PROJECT=foo but routing treats the instance as project-less. Tag/project behavior is asymmetric in env-and-config fallback.

2.6a Project filter only on send + list

The PR description claims project isolation "across all commands." Verified inventory:

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>.md via --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_prompt

So 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 avoidancescore_name rejects 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:BOXE device's pool can clash with local dev).

--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:

  1. iter_instances_full() — full table scan.
  2. pidtrack::is_alive(pid) — kill(0,pid) syscall per row.
  3. For dead rows: snapshot via log_life_event, then several DELETE statements.

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 in src/bootstrap.rs:90, 93 contain commas, but they are not the "," JSON delimiter pattern. More relevant: any user-set HCOM_NOTES or 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 \n literals; 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:

  1. --acp mode — Cline ships JSON-RPC over stdio. hcom would become an ACP client; bidirectional, typed, lifecycle-tracked properly.
  2. Pipe through jq -r '.taskId' instead of sed — same dependency footprint as bash, robust JSON.
  3. Hooks-only ad-hoc tierhcom hooks add cline writes the bridge scripts; user runs cline themselves; hcom does not own the PTY. The ad-hoc tier already exists for the supported tools: src/bootstrap.rs:122-146 defines the DELIVERY_ADHOC template, and src/bootstrap.rs:414-419 selects between DELIVERY_AUTO and DELIVERY_ADHOC based on is_launched — vanilla agents (started via hcom start from inside an unmanaged session) take the ad-hoc path. hcom list displays 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:

  • --tui is 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 --tui for Cline at src/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) calls db.get_unread_messages(instance_name) on every PostToolUse and returns them in hookSpecificOutput.additionalContext.
  • Codex handle_posttooluse (src/hooks/codex.rs:301-308) calls prepare_codex_delivery which does the same.
  • Gemini handle_aftertool (src/hooks/gemini.rs:414-462) does the same with a delivery_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:337 maps Tool::Cline => Self::opencode().
  • src/delivery.rs:614 adds Tool::Cline to 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 are PreToolUse, PostToolUse, PreCompact. PostToolUse is 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:

  1. hcom start --name <name> — registers the extension as an ad-hoc instance.
  2. 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 start from inside any external tool, terminal, or shell session. That session becomes an adhoc instance, distinguished by tool = "adhoc". Bootstrap text uses DELIVERY_ADHOC (src/bootstrap.rs:122-146): the agent must call hcom listen itself 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 with hcom 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 --json child 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:

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 the HCOM_DIR env var, expands ~, resolves relative paths against cwd; falls back to $HOME/.hcom or ./.hcom.
  • src/paths.rs::db_path (src/paths.rs:82-84) — DB lives at HCOM_DIR/hcom.db.
  • src/paths.rs::log_path (src/paths.rs:86-90) — logs at HCOM_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 under HCOM_DIR.parent(). So HCOM_DIR=$PWD/.hcom puts .claude, .codex, etc. under $PWD.
  • src/hooks/claude.rs:1967-1974 and src/hooks/codex.rs:441-448 — confirm tool hooks scope to tool_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-luna mention splits into (tag=dev, name=luna) and resolves only if the instance with name=luna has tag="dev".
  • src/launcher.rs:777-784 — tag flows from HCOM_TAG env / --tag flag into the instance row.
  • src/bootstrap.rs:316-322 — instance-level tag overrides config-level tag at bootstrap time; src/bootstrap.rs:89-90 adds the TAG_NOTICE block 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 relay is 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) and src/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 via get_unread_messages will 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 on EventsSubArgs (src/commands/events.rs:69-92).
  • Transcript fetch. hcom transcript <name>:DEVICE does not gate by project.
  • NULL-project remote peers. A remote device that registered an instance without --project gets 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:

  1. Edit TS at extensions/hcom-bridge/src/.
  2. Run extensions/hcom-bridge/build.sh (which would fail due to §2.3).
  3. Copy the build output to src/antigravity_extension/.
  4. Commit both the source and the artifact.
  5. 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.rssrc/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.tskilo-hcom.tshcom.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_PATH from hcom.loghcom-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 1

hcom 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_sessionstop_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:

  1. Reboot kills agent process.
  2. After heartbeat threshold (10–35s with no hook activity), get_instance_status (src/instance_lifecycle.rs:157-296) returns ST_INACTIVE with context "stale".
  3. Up to max_stale_seconds later (default 3600s when cleanup_stale_instances is called from hcom list), the instance is cleaned up via stop_instance, which writes the snapshot.
  4. Resumable.

Total time-to-resumability post-reboot: up to ~1 hour from the next hcom list.

The reboot scenario with mark_dead_instances:

  1. Reboot kills agent process.
  2. Next hcom <anything> invocation runs mark_dead_instances.
  3. PID is !is_alive(), snapshot written, instance deleted.
  4. 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 A does 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_ADHOC constants), src/bootstrap.rs:367-455 (get_bootstrap), src/bootstrap.rs:437-439 (existing ## NOTES injection from the notes parameter — relevant to §3.1)
  • Tool registry & ready patterns: src/tool.rs:1-150 (Tool enum, KILO_HOOKS/CLINE_HOOKS arrays, 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 in src/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_send and send_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:

KiloCode upstream:

OpenCode upstream:

Antigravity:

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.rs do not match the names invoked by the embedded plugin (§2.1a); DB, config, and plugin paths use the kilocode/* namespace while upstream Kilo writes and reads kilo/* (§2.1b).
  • Cline — installed hook scripts live in directories Cline does not read from (§3.5); the DELIVERY_AUTO claim in the bootstrap text does not match the shipped delivery layers (§3.6); JSON parsing in the hook scripts is via sed/awk (§3.4).
  • Antigravity bridge — TS source does not compile (§2.3); the --project flag 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 two hcom calls 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); only send and list accept --project while events/listen/transcript/archive/bundle see 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 --format auto-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):

  • --project overlaps HCOM_DIR (full filesystem-level isolation) and HCOM_TAG (soft groups + addressing).
  • --agent-name overlaps HCOM_TAG-based naming and hcom start --as <name> rebinding.
  • ~/.hcom/agents/<name>.md overlaps HCOM_NOTES/global notes, --hcom-prompt, and Claude's native .claude/agents/<name>.md via --agent.
  • mark_dead_instances overlaps cleanup_stale_instances (called lazily from hcom list) and stop_instance (which already produces byte-identical resume snapshots).

Code shape:

  • src/hooks/cline.rs (618 lines) and src/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.rs lives under src/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 Tool enum and LaunchTool enum without naming the new tiers (§1).

Test coverage:

  • The test_pty_opencode #[test] entry was renamed to test_pty_cline; run_pty_test_opencode remains 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants