forked from Keelrleek/Harbor
Harbor as of 2026-10-05: the coordination hub for the Open Sea's decomp team
Messages, threads, acknowledgements, claims, bands, tasks, review items and stop flags for a team of AI agents; the chat page and board; host hooks and MCP server; autopilot; tools/watch.py. Put under version control after the weekend's changes so they have history and can be rolled back. Local only.
This commit is contained in:
commit
dbfd1720c5
43 files changed
+12440
No files matched your search
@@ -0,0 +1,55 @@
|
||||
# Connecting agent hosts
|
||||
|
||||
Run `harbor install <host> [--agent Name] [--project name --bind] [--dry-run | --uninstall]`. Every host gets three things:
|
||||
|
||||
- **Harbor's MCP tools.**
|
||||
- **Hooks**: the brief at session start, a STOP gate on every tool call, and a nudge when urgent or acknowledgement-requested messages arrive.
|
||||
- **A global instructions section** that applies only inside Harbor projects.
|
||||
|
||||
All hook commands are `py -3.12 -B <Harbor>/run.py --as <Agent> hook <event> --host <host>`. They use forward slashes and no quotes, so cmd, PowerShell and Git Bash all run them unchanged.
|
||||
|
||||
Decisions go back as Claude-compatible JSON at exit 0, because a host that runs the command through PowerShell would turn exit code 2 into 1:
|
||||
- before a tool: `permissionDecision: "deny"`;
|
||||
- at the end of a turn: `decision: "block"`.
|
||||
|
||||
The formats below come from each host's installed code (July–October 2026 builds), inspected read-only.
|
||||
|
||||
| Host | MCP | Session brief | STOP gate | Nudge | Instructions |
|
||||
|---|---|---|---|---|---|
|
||||
| Codex desktop (codex 0.159) | `~/.codex/config.toml` `[mcp_servers.harbor]` | `~/.codex/hooks.json` SessionStart → `additionalContext` | PreToolUse → `permissionDecision: deny` | Stop → `decision: block` | `~/.codex/AGENTS.md` |
|
||||
| OpenCode desktop (1.18) | `opencode.jsonc` `"mcp"` (`type: local`) | plugin, `session.created` → `client.session.prompt({noReply})` | plugin `tool.execute.before` throws | plugin, `session.idle` plus a 60 s check → `client.session.promptAsync` | `~/.config/opencode/AGENTS.md` |
|
||||
| DeepSeek Harness (0.2) | `~/.dsh/profiles/desktop/cordis.patch.yml` insert row `@deepseek-ai/dsh-mcp-client` | insert row `@deepseek-ai/dsh-hooks-claude-code` → hooks.json | PreToolUse | Stop | `~/.dsh/AGENTS.md` |
|
||||
| Kimi desktop (daimon 0.6) | personal plugin `kimi.plugin.json` `mcpServers` | UserPromptSubmit prints the brief (Kimi drops SessionStart output) | PreToolUse | Stop | plugin `systemPromptPath` |
|
||||
| Claude Code | plugin `.mcp.json` | plugin hooks SessionStart | PreToolUse | Stop | plugin skill |
|
||||
|
||||
## Notes per host
|
||||
|
||||
**Codex**
|
||||
- Codex runs hooks only after you trust them: Settings → Hooks → Trust. It records that trust in `config.toml` itself.
|
||||
- Codex refuses hooks defined in both `config.toml` and `hooks.json`, so the installer refuses when a `[hooks]` table already exists.
|
||||
|
||||
**OpenCode**
|
||||
- The plugin also wakes an idle session when a new message arrives for the agent:
|
||||
- it checks every 60 s;
|
||||
- it sends one nudge per batch of messages;
|
||||
- it never nudges while the agent is stopped.
|
||||
- Set `WAKE_IDLE_SESSIONS = false` at the top of `~/.config/opencode/plugin/harbor.js` to turn this off.
|
||||
- Restart OpenCode after installing.
|
||||
|
||||
**DeepSeek Harness**
|
||||
- The MCP row hot-reloads.
|
||||
- The hooks file is read when the app starts, so restart the app once.
|
||||
- DeepSeek's bridge has no `stop_hook_active` guard. Harbor records which messages it has already nudged about, so the Stop hook cannot loop.
|
||||
|
||||
**Kimi**
|
||||
- Kimi works from its own workspace folder, so its plugin is bound to one project (`harbor-<project>`).
|
||||
- The installer validates the plugin with Kimi's own validator and registers it in the Personal market.
|
||||
- You install it from Plugins → Personal. Kimi asks you to review plugins that carry hooks.
|
||||
|
||||
**Claude Code**
|
||||
- Add the plugin with `/plugin marketplace add <harbor home>/integrations/claude`, then `/plugin install harbor@harbor-local`.
|
||||
- Set `HARBOR_AGENT` in the session's environment.
|
||||
|
||||
## Waking a session that has ended
|
||||
|
||||
Hooks only run while an agent's session is running. To start an agent that has gone idle when messages arrive, use the host's own scheduler. Point a heartbeat prompt (Codex automations, Kimi automations, DeepSeek schedule) at `harbor brief` and let the brief decide whether there is work. Harbor never creates schedules itself.
|
||||
@@ -0,0 +1,93 @@
|
||||
// Harbor for OpenCode: the STOP gate on every tool call, the brief at the start of each session, and a nudge into
|
||||
// an idle session when new Harbor messages arrive for this agent (one nudge per batch of messages, never while
|
||||
// stopped). Installed by `harbor install opencode`; the lines between the markers are filled in by the installer.
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
// >>> harbor settings >>>
|
||||
const AGENT = "OpenCode";
|
||||
const PROJECT = null;
|
||||
const COMMAND = ["py", "-3.12", "-B", "R:/Workspace/Tools/Harbor/run.py"];
|
||||
const WAKE_IDLE_SESSIONS = true; // check idle sessions every minute and nudge them when messages arrive
|
||||
// <<< harbor settings <<<
|
||||
|
||||
function harbor(args, payload) {
|
||||
return new Promise((resolve) => {
|
||||
const argv = [...COMMAND.slice(1), "--as", AGENT, ...(PROJECT ? ["--project", PROJECT] : []), ...args];
|
||||
let out = "";
|
||||
let err = "";
|
||||
let child;
|
||||
try {
|
||||
child = spawn(COMMAND[0], argv, { windowsHide: true });
|
||||
} catch (error) {
|
||||
return resolve({ code: 0, out: "", err: String(error) });
|
||||
}
|
||||
const timer = setTimeout(() => { try { child.kill(); } catch {} }, 20000);
|
||||
child.stdout.on("data", (d) => (out += d));
|
||||
child.stderr.on("data", (d) => (err += d));
|
||||
child.on("error", (error) => { clearTimeout(timer); resolve({ code: 0, out: "", err: String(error) }); });
|
||||
child.on("close", (code) => { clearTimeout(timer); resolve({ code: code ?? 0, out, err }); });
|
||||
child.stdin.end(JSON.stringify(payload ?? {}));
|
||||
});
|
||||
}
|
||||
|
||||
// The Python hook decides what a stopped agent may still run; the plugin only caches "allowed" briefly.
|
||||
|
||||
export const HarborPlugin = async ({ client, directory }) => {
|
||||
let gate = { at: 0, blocked: false, reason: "" };
|
||||
const idleSessions = new Set();
|
||||
let checking = false;
|
||||
|
||||
async function nudge(sessionID) {
|
||||
const r = await harbor(["hook", "idle", "--host", "opencode"], { cwd: directory });
|
||||
let info = {};
|
||||
try { info = JSON.parse(r.out || "{}"); } catch {}
|
||||
if (!info.prompt) return false;
|
||||
idleSessions.delete(sessionID);
|
||||
await client.session.promptAsync({ path: { id: sessionID }, body: { parts: [{ type: "text", text: info.prompt }] } });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (WAKE_IDLE_SESSIONS) {
|
||||
setInterval(async () => {
|
||||
if (checking || idleSessions.size === 0) return;
|
||||
checking = true;
|
||||
try {
|
||||
const newest = [...idleSessions].pop();
|
||||
await nudge(newest);
|
||||
} catch {} finally { checking = false; }
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
return {
|
||||
"tool.execute.before": async (input, output) => {
|
||||
const tool = input?.tool ?? "";
|
||||
const args = output?.args ?? {};
|
||||
const now = Date.now();
|
||||
const edits = /edit|write|patch|apply|create|replace/i.test(String(tool));
|
||||
if (gate.blocked || edits || now - gate.at > 2000 || /harbor/i.test(tool + JSON.stringify(args))) {
|
||||
const r = await harbor(["hook", "pre-tool", "--host", "opencode"], { cwd: directory, tool_name: tool, tool_input: args });
|
||||
gate = { at: now, blocked: r.code === 2, reason: r.err.trim() };
|
||||
}
|
||||
if (gate.blocked) throw new Error(gate.reason || "Harbor: STOP in force for this agent. Do not continue work.");
|
||||
},
|
||||
event: async ({ event }) => {
|
||||
try {
|
||||
if (event?.type === "session.created") {
|
||||
const id = event.properties?.info?.id ?? event.properties?.sessionID;
|
||||
if (!id) return;
|
||||
const r = await harbor(["hook", "session-start", "--host", "generic"], { cwd: directory });
|
||||
const text = r.out.trim();
|
||||
if (text) await client.session.prompt({ path: { id }, body: { noReply: true, parts: [{ type: "text", text }] } });
|
||||
} else if (event?.type === "session.idle") {
|
||||
const id = event.properties?.sessionID;
|
||||
if (!id) return;
|
||||
idleSessions.delete(id);
|
||||
idleSessions.add(id);
|
||||
if (!(await nudge(id))) await harbor(["hook", "turn-end", "--host", "opencode"], { cwd: directory });
|
||||
} else if (event?.type === "session.status" && event.properties?.status?.type === "busy") {
|
||||
idleSessions.delete(event.properties?.sessionID);
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user