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:
KeelrLeek committed 2026-10-05 10:44:23 -05:00
commit dbfd1720c5
43 files changed
+12440

No files matched your search

+475
View File
@@ -0,0 +1,475 @@
"""Harbor as MCP tools (stdio), so an agent can coordinate without composing shell commands.
Run with the agent's identity fixed in its environment (it cannot be changed from a tool call):
command: py -3.12 -B <Harbor>\\harbor_mcp.py env: HARBOR_AGENT=<Agent> (HARBOR_PROJECT optional)
The project comes from $HARBOR_PROJECT or the .harbor.toml above the working directory. The human is refused.
"""
import asyncio
import os
import threading
import time
from typing import Literal
os.environ.setdefault("FASTMCP_CHECK_FOR_UPDATES", "off") # no network calls from a local tool
os.environ.setdefault("FASTMCP_SHOW_SERVER_BANNER", "false")
from fastmcp import FastMCP # noqa: E402
from fastmcp.exceptions import ToolError # noqa: E402
from . import projects # noqa: E402
from .core import OPEN_ITEM_STATES, Harbor, HarborError # noqa: E402
INSTRUCTIONS = """Harbor is the team's shared coordination store: messages in threads, acknowledgements, claims, a
review queue, presence and stop flags. Protocol:
1. Call brief() at the start of every session and after long work. If it reports a STOP, do not start or continue
work; only the human can resume. If it shows your last handoff, continue from it.
2. Take work with next_task() instead of waiting to be told; submit(task=...) or update_task(..., "done") finishes it.
Keep the team informed with doing("what you are working on") instead of status messages to everyone.
3. Put each conversation in a thread named after its subject: fn:0x529350, commit:b4b44f0, item:12, or a short
topic. Answer with reply() so the thread and the people stay together.
4. Write plain sentences. There is no length limit to squeeze into (20,000 characters); never run words together.
5. Ask for an acknowledgement (ack=True) only when you need to know the message was acted on; acknowledge with
ack() and a short note instead of sending a "received" message.
6. Before working on a function or file, claim it. A refused claim names the holder: ask them in a thread.
7. Put finished work in the review queue with submit(); do not announce commits in chat. A coordinator lands it.
8. When you are waiting on someone, use wait_for_messages() instead of polling or sleeping.
9. A correction is send(..., corrects=<id of your earlier message>).
10. Before your session ends or your allowance runs out, write handoff(): finished, in flight, next."""
KindName = Literal["note", "question", "answer", "request", "finding", "retraction", "decision", "status", "handoff"]
Importance = Literal["normal", "high", "urgent"]
ReviewState = Literal["in_review", "changes_requested", "approved", "held", "rejected", "submitted"]
ClaimKind = Literal["draft", "naming", "edit", "review", "band"]
mcp = FastMCP("harbor", instructions=INSTRUCTIONS)
_lock = threading.Lock()
_state = {"seen": 0.0}
def harbor():
if "h" not in _state:
project = _state.get("project") or projects.resolve()
_state["h"] = Harbor(project, shared=True)
_state["h"].refresh_project() # project-file edits apply without restarting the app
return _state["h"]
def me():
h = harbor()
agent = os.environ.get("HARBOR_AGENT") or h.project.identity_for(os.getcwd()) or ""
if agent not in h.project.ai_names:
raise HarborError(f"HARBOR_AGENT must be one of {', '.join(h.project.ai_names)} (set it in this server's MCP "
"config)")
if time.monotonic() - _state.get("active", 0.0) > 20: # a tool call is activity (apps without hooks too)
_state["active"] = time.monotonic()
try:
import sys as _sys
h.activity(agent, "harbor " + _sys._getframe(1).f_code.co_name, os.environ.get("HARBOR_HOST", ""),
channel="mcp")
except Exception: # noqa: BLE001 - bookkeeping is best effort
pass
return agent
def compact(m):
out = {"id": m["id"], "at": m["created_at"], "from": m["sender"], "to": m["recipients"],
"thread": m.get("topic"), "text": m["body"]}
for key in ("kind", "importance"):
if m[key] not in ("note", "normal"):
out[key] = m[key]
if m.get("ack_required"):
out["ack_requested"] = True
if m.get("reply_to"):
out["reply_to"] = m["reply_to"]
if m.get("refs"):
out["refs"] = m["refs"]
return out
def recipients(to):
if to is None or to == "All" or to == ["All"]:
return "All"
if isinstance(to, str):
return [name.strip() for name in to.split(",") if name.strip()]
return list(to)
def run(fn):
"""Harbor's refusals go back to the agent as a plain tool error (the message says what to do instead)."""
try:
with _lock:
h = harbor()
if time.monotonic() - _state.get("seen", 0.0) > 60: # record the live MCP connection, once a minute
_state["seen"] = time.monotonic()
try:
h.connect(me(), "mcp", os.environ.get("HARBOR_HOST", ""))
except HarborError:
pass
return fn(h)
except (HarborError, projects.ProjectError) as error:
raise ToolError(str(error)) from None
@mcp.tool
def brief(mark_read: bool = False) -> str:
"""What you need at the start of a session: stop flags, unread messages (urgent first), acknowledgements you
owe, your review items and claims, what the rest of the team is doing."""
return run(lambda h: h.brief(me(), mark_read=mark_read))
@mcp.tool
def doing(text: str = "") -> dict:
"""Set the one line the whole team sees about what you are doing now (empty clears it)."""
return run(lambda h: h.doing(me(), text))
@mcp.tool
def inbox(limit: int = 20, include_read: bool = False, peek: bool = False) -> list[dict]:
"""Your unread messages, urgent first (marked read unless peek=True). include_read=True lists recent ones."""
return run(lambda h: [compact(m) for m in h.inbox(me(), unread_only=not include_read, limit=limit,
mark_read=not peek)])
@mcp.tool
async def wait_for_messages(timeout_seconds: int = 300) -> dict:
"""Wait (up to timeout_seconds, max 1800) until a message for you arrives or a stop applies, then return the
new messages. Use this instead of sleeping or polling when you are waiting on someone."""
agent = run(lambda h: me())
timeout = max(1, min(int(timeout_seconds), 1800))
deadline = asyncio.get_running_loop().time() + timeout
while True:
state = run(lambda h: (h.unread_count(agent)[0], [f["key"] for f in h.stopped(agent)]))
if state[0] or state[1] or asyncio.get_running_loop().time() >= deadline:
messages = run(lambda h: [compact(m) for m in h.inbox(agent, limit=30)]) if state[0] else []
run(lambda h: h.touch(agent))
return {"messages": messages, "stopped": state[1], "timed_out": not state[0] and not state[1]}
await asyncio.sleep(2)
@mcp.tool
def send(to: str | list[str], text: str, thread: str | None = None, subject: str | None = None,
kind: KindName = "note", importance: Importance = "normal", ack: bool = False,
refs: dict[str, str] | None = None, attachments: list[str] | None = None, corrects: int | None = None,
due: str | None = None) -> dict:
"""Send a message. to: an agent, a list of agents, or "All". thread: a topic such as fn:0x529350,
commit:b4b44f0, task:5 or item:12 (default "general"); subject is the thread's title when it is new.
refs: what the message is about, e.g. {"commit": "b4b44f0", "fn": "0x529350"}.
attachments: local file paths to copy into Harbor (reports, logs, diffs).
corrects: the id of your earlier message this corrects (goes to the same thread and people).
due: when you need an answer ("30m", "2h", or an ISO time); missing it raises an alert."""
return run(lambda h: h.send(me(), recipients(to), text, subject=subject, thread=thread, kind=kind,
importance=importance, ack_required=ack, refs=refs, origin="mcp",
attachments=attachments, corrects=corrects, due=due))
@mcp.tool
def reply(message_id: int, text: str, kind: KindName = "answer", importance: Importance = "normal",
ack: bool = False, to: str | list[str] | None = None) -> dict:
"""Reply in the same thread to the same people (or to= someone else)."""
return run(lambda h: h.send(me(), None if to is None else recipients(to), text, reply_to=message_id, kind=kind,
importance=importance, ack_required=ack, origin="mcp"))
@mcp.tool
def ack(message_ids: list[int], note: str | None = None) -> list[int]:
"""Acknowledge messages addressed to you (use instead of a "received" message); note says what you did."""
return run(lambda h: h.ack(me(), message_ids, note))
@mcp.tool
def thread(thread: str) -> dict:
"""A whole thread by id or topic (e.g. "fn:0x529350")."""
def get(h):
t, messages = h.thread(thread)
return {"thread": t, "messages": [compact(m) for m in messages]}
return run(get)
@mcp.tool
def threads(limit: int = 25, include_closed: bool = False) -> list[dict]:
"""Recent threads with your unread count in each."""
return run(lambda h: h.threads(limit=limit, agent=me(), include_closed=include_closed))
@mcp.tool
def close_thread(thread: str, reopen: bool = False) -> dict:
"""Close a thread whose question is settled (or reopen it). A new message reopens it automatically."""
return run(lambda h: h.set_thread_state(me(), thread, "open" if reopen else "closed"))
@mcp.tool
def search(query: str, limit: int = 15) -> list[dict]:
"""Full-text search over every message, including imported chat history."""
return run(lambda h: [compact(m) for m in h.search(query, limit=limit)])
@mcp.tool
def claim(scope: str, kind: ClaimKind = "draft", ttl_hours: float | None = None, note: str | None = None) -> dict:
"""Claim fn:0xADDR, range:0xLO-0xHI, name:Symbol, task:text or file:path before working on it. Claiming the same
scope again renews it. Refused (with the holder named) when someone holds an overlapping exclusive claim."""
return run(lambda h: h.claim(me(), scope, kind=kind, ttl_hours=ttl_hours, note=note))
@mcp.tool
def release(claim_id: int | None = None, scope: str | None = None) -> list[int]:
"""Release your claim by id or scope when the work is submitted or abandoned."""
if claim_id is None and not scope:
raise ToolError("give claim_id or scope")
return run(lambda h: h.release(me(), claim_id, scope))
@mcp.tool
def claims(agent: str | None = None) -> list[dict]:
"""Active claims (everyone's, or one agent's)."""
return run(lambda h: h.active_claims(agent=agent))
@mcp.tool
def submit(title: str, scope: str | None = None, commit: str | None = None, branch: str | None = None,
note: str | None = None, task: int | None = None) -> dict:
"""Put finished work in the review queue (opens thread item:<id> with the reviewers). task: the task this
completes; landing the item closes the task, a change request reopens it for you."""
return run(lambda h: h.submit(me(), title, scope=scope, commit=commit, branch=branch, note=note, task=task))
TaskPriority = Literal["urgent", "high", "normal", "low"]
TaskMove = Literal["done", "blocked", "open", "cancelled", "in_progress"]
@mcp.tool
def next_task(take: bool = True) -> dict:
"""Take the best open task you may do now: highest priority first, dependencies finished, scope not claimed by
someone else. Taking it claims its scope and sets your `doing` line; the result includes what the team already
knows about that scope (notes, past threads, review items). take=False only shows it."""
def get(h):
task = h.next_task(me(), take=take)
if not task:
return {"task": None, "reason": "no open task you can take right now (or you are stopped)"}
return {"task": task, "context": h.context_text(task["scope"]) if task["scope"] else None}
return run(get)
@mcp.tool
def ask(question: str, options: list[str] | None = None, to: str | None = None, due: str | None = None,
fallback: str | None = None) -> dict:
"""Ask the human (default) or an agent a question. options become answer buttons on the chat page (keep them
short: "Yes", "No", "Later"). fallback: a coordinator who may answer while the human is away. Then use
wait_for_messages(): the answer arrives as a reply."""
return run(lambda h: h.ask(me(), question, options or (), to=to, due=due, fallback=fallback, origin="mcp"))
@mcp.tool
def answer(question_id: int, answer: str) -> dict:
"""Answer a question addressed to you (one of its options, or 'other: ...')."""
return run(lambda h: h.answer(me(), question_id, answer, origin="mcp"))
@mcp.tool
def questions(include_answered: bool = False) -> list[dict]:
"""Questions to you and from you."""
def get(h):
agent = me()
return [q for q in h.questions(open_only=not include_answered, limit=100) if agent in (q["to_name"], q["asker"])]
return run(get)
@mcp.tool
def note(scope: str, text: str, replaces: int | None = None) -> dict:
"""Add to the team's memory about a scope (fn:0x..., range:..., file:..., name:...): an ABI detail, a trap, a
decision. Everyone who later takes work on that scope sees it."""
return run(lambda h: h.note(me(), scope, text, replaces=replaces))
@mcp.tool
def context(scope: str) -> str:
"""Everything the team knows about a scope: notes, threads, messages that mention it, tasks, review items and
who claimed it before. Read it before you start on a function."""
return run(lambda h: h.context_text(scope, limit=15))
@mcp.tool
def metric(name: str, value: float, note: str | None = None) -> dict:
"""Report a number the board charts over time (e.g. readable=779 after a landing, tests_passing=58)."""
return run(lambda h: h.metric(me(), name, value, note))
@mcp.tool
def tasks(mine: bool = False, include_closed: bool = False) -> list[dict]:
"""The task list: open, in progress, blocked and in review (include_closed adds done and cancelled)."""
from .core import TASK_STATES
states = TASK_STATES if include_closed else ("open", "in_progress", "blocked", "review")
return run(lambda h: h.tasks(states=states, assignee=me() if mine else None))
@mcp.tool
def add_task(title: str, body: str | None = None, scope: str | None = None, assignee: str | None = None,
priority: TaskPriority = "normal", after: list[int] | None = None, due: str | None = None) -> dict:
"""Add a task (split your work, or record follow-up work). scope: fn:0x..., range:..., file:..., task:...;
after: task ids that must be done first; assignee: yourself, or anyone if you are a coordinator."""
return run(lambda h: h.add_task(me(), title, body=body, scope=scope, assignee=assignee, priority=priority,
after=after or [], due=due, origin="mcp"))
@mcp.tool
def take_task(task_id: int) -> dict:
"""Take a specific open task (claims its scope)."""
return run(lambda h: h.take_task(me(), task_id))
@mcp.tool
def update_task(task_id: int, state: TaskMove, note: str | None = None) -> dict:
"""Move a task: done (with a note on what you did), blocked (note says by what), open (hand it back),
cancelled, in_progress (resume a blocked task)."""
return run(lambda h: h.update_task(me(), task_id, state, note=note))
@mcp.tool
def handoff(text: str) -> dict:
"""Record where you left off: what is finished, what is in flight (branch, files, half-done steps), what comes
next. Shown at the start of your next session and to the team. Write one before your session ends."""
return run(lambda h: h.handoff(me(), text))
@mcp.tool
def pin(message_id: int, note: str | None = None, unpin: bool = False) -> list[dict]:
"""Pin a message the whole team should keep seeing (a rule, a decision, an address list), or unpin it."""
return run(lambda h: [compact(m) for m in (h.unpin(me(), message_id) if unpin else h.pin(me(), message_id, note))])
@mcp.tool
def review(item_id: int, state: ReviewState, note: str | None = None, commit: str | None = None) -> dict:
"""Move a review item: reviewers use in_review / changes_requested / approved / held / rejected; the author
re-submits with state="submitted" (and the new commit)."""
return run(lambda h: h.transition(me(), item_id, state, note=note, commit=commit))
@mcp.tool
def withdraw(item_id: int, note: str | None = None) -> dict:
"""The author withdraws their own review item."""
return run(lambda h: h.transition(me(), item_id, "withdrawn", note=note))
@mcp.tool
def land(item_id: int, commit: str, note: str | None = None) -> dict:
"""Coordinator only: the approved item is on main in this commit."""
return run(lambda h: h.transition(me(), item_id, "landed", note=note, commit=commit))
@mcp.tool
def queue(include_closed: bool = False) -> list[dict]:
"""The review queue (open items; include_closed adds landed, withdrawn and rejected)."""
states = OPEN_ITEM_STATES + (("landed", "withdrawn", "rejected") if include_closed else ())
return run(lambda h: h.items(states=states))
@mcp.tool
def status() -> dict:
"""Stop flags, everyone's presence (what they are doing, when last seen), unread counts, queue size and anything
that needs attention."""
def get(h):
agents = []
for a in h.agents():
unread, urgent = h.unread_count(a["name"])
agents.append({"name": a["name"], "doing": a.get("doing"), "last_seen": a["last_seen"], "unread": unread,
"urgent": urgent})
return {"stopped_for_me": [f["key"] for f in h.stopped(me())], "flags": list(h.flags().values()),
"agents": agents, "open_review_items": len(h.items()), "active_claims": len(h.active_claims()),
"attention": [a["text"] for a in h.attention()]}
return run(get)
@mcp.tool
def digest(since: str = "8h") -> dict:
"""What happened since a time ("8h", "90m", "2d" or an ISO time): per-agent activity, review and claim changes,
stops, and messages to the human."""
def get(h):
d = h.digest(since)
d["to_human"] = [compact(m) for m in d["to_human"]]
return d
return run(get)
@mcp.tool
def stop(target: str = "self", reason: str | None = None, relay_of: int | None = None) -> dict:
"""Stop yourself (target="self"). A coordinator may stop anyone. To stop or resume someone on the human's
request, relay their chat-page message: relay_of = that message id."""
def do(h):
agent = me()
who = agent if target == "self" else target
key = "stop:all" if who.lower() == "all" else f"stop:{who}"
return h.set_flag(key, "on", agent, "mcp", reason=reason, relay_of=relay_of)
return run(do)
@mcp.tool
def summarize(thread: str, text: str) -> dict:
"""Pin a short summary to a long thread: what was decided, what is open, who does what next. It is shown on top
of the thread and in `context` for its scope."""
return run(lambda h: h.summarize(me(), thread, text))
@mcp.tool
def who_knows(scope: str) -> list[dict]:
"""Agents ranked by how much they have worked on a scope - ask them first."""
return run(lambda h: h.experts(scope))
@mcp.tool
def git_status() -> dict:
"""Your worktree as Harbor sees it: branch, last commit, uncommitted files, commits behind the main branch, and
commits made since your last submit (work not yet in the review queue)."""
return run(lambda h: h.git_status(me(), max_age=5) or {"error": "no workdir for you in the project file"})
@mcp.tool
def add_tasks(lines: str, template: str | None = None, priority: TaskPriority = "normal") -> dict:
"""Add many tasks at once, one per line ("0x445310 RouteCellCost" or a plain title). Addresses go to their band
owner. template: run a template from the project file for every line."""
return run(lambda h: h.bulk_tasks(me(), lines, template=template, priority=priority, origin="mcp"))
@mcp.tool
def resume(target: str, relay_of: int | None = None, reason: str | None = None) -> dict:
"""Only the human resumes. A coordinator may lift a stop it set itself (not one it relayed for the human)."""
def do(h):
key = "stop:all" if target.lower() == "all" else f"stop:{target}"
return h.set_flag(key, "off", me(), "mcp", reason=reason, relay_of=relay_of)
return run(do)
def _report_in():
"""Tell Harbor this agent's app has its tools running: at start, then every 5 minutes while the app keeps the
server open (updates the connection only, not the agent's last-seen time)."""
def loop():
while True:
time.sleep(300)
try:
with _lock:
h = harbor()
h._write(lambda c: c.execute(
"UPDATE connections SET last_at=? WHERE agent=? AND channel='mcp' AND host=?",
(h.now(), me(), os.environ.get("HARBOR_HOST", ""))))
except Exception: # noqa: BLE001 - reporting in is best effort
pass
try:
with _lock:
h = harbor()
h.connect(me(), "mcp", os.environ.get("HARBOR_HOST", ""))
_state["seen"] = time.monotonic()
except Exception: # noqa: BLE001
pass
threading.Thread(target=loop, daemon=True).start()
def main(project=None):
if project is not None:
_state["project"] = project
try:
me() # refuse to start without a valid identity
except (HarborError, projects.ProjectError) as error:
raise SystemExit(f"harbor mcp: {error}")
_report_in()
mcp.run(show_banner=False)
if __name__ == "__main__":
main()