huddle-cli 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/QUICKSTART.md +102 -0
  3. package/README.md +23 -4
  4. package/dist/adapters/amp.js +1 -1
  5. package/dist/adapters/antigravity.js +2 -0
  6. package/dist/adapters/claude.js +19 -3
  7. package/dist/adapters/codex.js +6 -1
  8. package/dist/adapters/cursor.js +12 -5
  9. package/dist/adapters/custom.js +4 -2
  10. package/dist/adapters/droid.js +1 -0
  11. package/dist/adapters/goose.js +2 -0
  12. package/dist/adapters/index.js +44 -18
  13. package/dist/adapters/kiro.js +1 -1
  14. package/dist/adapters/opencode.js +1 -1
  15. package/dist/attach/connect.js +4 -1
  16. package/dist/attach/guard.js +21 -2
  17. package/dist/attach/live.js +5 -1
  18. package/dist/attach/plug.js +14 -3
  19. package/dist/attach/screen.js +150 -0
  20. package/dist/attach/session.js +7 -2
  21. package/dist/cli.js +108 -40
  22. package/dist/mcp/context-tools.js +79 -0
  23. package/dist/mcp/server.js +38 -11
  24. package/dist/orchestrator/ask.js +13 -10
  25. package/dist/orchestrator/brain.js +13 -3
  26. package/dist/orchestrator/council.js +427 -86
  27. package/dist/orchestrator/goal.js +196 -0
  28. package/dist/orchestrator/handoff.js +2 -2
  29. package/dist/orchestrator/launcher.js +29 -13
  30. package/dist/orchestrator/learn.js +105 -0
  31. package/dist/orchestrator/planner.js +25 -7
  32. package/dist/orchestrator/qa.js +62 -14
  33. package/dist/orchestrator/radar.js +719 -0
  34. package/dist/orchestrator/scheduler.js +498 -39
  35. package/dist/orchestrator/takeover.js +155 -0
  36. package/dist/platform.js +273 -0
  37. package/dist/room/context.js +1285 -0
  38. package/dist/room/contract.js +132 -0
  39. package/dist/room/db.js +79 -16
  40. package/dist/room/followup.js +233 -0
  41. package/dist/room/identity.js +2 -2
  42. package/dist/room/lessons.js +275 -0
  43. package/dist/room/live.js +75 -20
  44. package/dist/room/mail.js +212 -0
  45. package/dist/room/progress.js +40 -1
  46. package/dist/room/server.js +169 -39
  47. package/dist/room/tunnel.js +6 -5
  48. package/dist/sandbox/sync.js +4 -4
  49. package/dist/sandbox/worktree.js +145 -14
  50. package/dist/ui/app.js +554 -133
  51. package/dist/ui/catalog.js +62 -24
  52. package/dist/ui/dashboard.js +194 -51
  53. package/dist/ui/doctor.js +15 -11
  54. package/dist/ui/lessons.js +81 -0
  55. package/dist/ui/paste.js +515 -0
  56. package/dist/ui/remote.js +11 -5
  57. package/dist/ui/setup.js +152 -0
  58. package/dist/ui/story.js +666 -69
  59. package/dist/ui/term.js +151 -16
  60. package/dist/ui/watch.js +145 -48
  61. package/package.json +6 -3
@@ -8,6 +8,8 @@
8
8
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
10
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
11
+ import { contextTools, toContent } from "./context-tools.js";
12
+ import { packageInfo } from "../preflight.js";
11
13
  const ROOM = process.env.HUDDLE_ROOM_URL ?? "http://localhost:4737";
12
14
  const AGENT = process.env.HUDDLE_AGENT_ID ?? "unknown-agent";
13
15
  // Proves to the Room that this call comes from one of this session's own agents.
@@ -105,11 +107,11 @@ const TOOLS = [
105
107
  },
106
108
  {
107
109
  name: "huddle_remember",
108
- description: "Write to SHARED MEMORY so other agents learn from you. kind = decision | fact | gotcha. Always include 'why' — teammates need the reasoning, not just the what.",
110
+ description: "Write to SHARED MEMORY so other agents learn from you. kind = decision | fact | gotcha | lesson. Always include 'why' — teammates need the reasoning, not just the what. kind=lesson is also kept for FUTURE runs in this repo: one short, general rule you'd want next time (e.g. 'run npm run typecheck too: npm test doesn't type-check').",
109
111
  inputSchema: {
110
112
  type: "object",
111
113
  properties: {
112
- kind: { type: "string", enum: ["decision", "fact", "gotcha"] },
114
+ kind: { type: "string", enum: ["decision", "fact", "gotcha", "lesson"] },
113
115
  body: { type: "string" },
114
116
  why: { type: "string" },
115
117
  },
@@ -141,7 +143,7 @@ const TOOLS = [
141
143
  },
142
144
  {
143
145
  name: "huddle_send",
144
- description: "Message a teammate agent directly: a heads-up ('I renamed X to Y'), an interface you're exposing, or an answer to their question (pass reply_to = the question's id). `to` = an agent id from huddle_peers, a task id, or \"all\" for everyone working now. They see it the next time they call any huddle tool.",
146
+ description: "Message a teammate agent directly: a heads-up ('I renamed X to Y'), an interface you're exposing, or an answer to their question (pass reply_to = the question's id). `to` = an agent id from huddle_peers, a task id, \"all\" for everyone working now, or \"huddle\" to answer Huddle itself. They see it the next time they call any huddle tool.",
145
147
  inputSchema: {
146
148
  type: "object",
147
149
  properties: {
@@ -154,10 +156,30 @@ const TOOLS = [
154
156
  handler: (a) => room("/send", "POST", { to: a.to, message: a.message, reply_to: a.reply_to }),
155
157
  },
156
158
  {
157
- name: "huddle_inbox",
158
- description: "Read messages and questions teammates sent you (marks them read). Any huddle tool result that says 'unread_messages' means: call this now. Answer questions with huddle_send(reply_to=…).",
159
+ name: "huddle_contract",
160
+ description: "Read the team's CONTRACT: the exact cross-task names everyone codes to (files and owners, function signatures, data fields, CLI flags, routes, error strings), its version, and every amendment since. Check it before you name anything another task uses.",
159
161
  inputSchema: { type: "object", properties: {} },
160
- handler: () => room("/inbox", "GET"),
162
+ handler: () => room("/contract", "GET"),
163
+ },
164
+ {
165
+ name: "huddle_amend_contract",
166
+ description: "Change or add a name in the CONTRACT (a signature, a field, a file owner, an error string) when your task needs one that isn't there or is wrong. Bumps the version and messages every teammate working now. Never invent a name another task depends on without this.",
167
+ inputSchema: {
168
+ type: "object",
169
+ properties: {
170
+ section: { type: "string", description: "what part it changes, e.g. 'store.js exports' or 'error strings'" },
171
+ change: { type: "string", description: "the exact new name/signature/shape" },
172
+ why: { type: "string" },
173
+ },
174
+ required: ["section", "change", "why"],
175
+ },
176
+ handler: (a) => room("/amend_contract", "POST", { section: a.section, change: a.change, why: a.why }),
177
+ },
178
+ {
179
+ name: "huddle_inbox",
180
+ description: "Read messages and questions teammates sent you (marks them read). Huddle tool results already carry new ones (new_messages); call this for more, or with id for a message shown cut short. Answer questions with huddle_send(reply_to=…).",
181
+ inputSchema: { type: "object", properties: { id: { type: "string", description: "optional: one message's id" } } },
182
+ handler: (a) => room(a?.id ? `/inbox?id=${encodeURIComponent(String(a.id))}` : "/inbox", "GET"),
161
183
  },
162
184
  {
163
185
  name: "huddle_report_bug",
@@ -196,7 +218,9 @@ const TOOLS = [
196
218
  },
197
219
  {
198
220
  name: "huddle_done",
199
- description: "Mark your task ready. This triggers the test-gate and, if green, the merge into main. Include a short summary of what you did.",
221
+ description: "Mark your task ready. This triggers the test-gate and, if green, the merge into main. Include a short summary of what you did. " +
222
+ "If a teammate's buddy note or a radar finding on your task has no answer yet, it is refused once with the list: fix each one, or reply " +
223
+ "huddle_send(to=<who sent it>, reply_to=<its id>, message=\"won't fix: <why>\"), then call huddle_done again.",
200
224
  inputSchema: {
201
225
  type: "object",
202
226
  properties: { task_id: { type: "string" }, summary: { type: "string" } },
@@ -204,6 +228,7 @@ const TOOLS = [
204
228
  },
205
229
  handler: (a) => room("/done", "POST", { task_id: a.task_id, summary: a.summary }),
206
230
  },
231
+ ...contextTools(room),
207
232
  ];
208
233
  /**
209
234
  * What an MCP host shows the model about the Plug as a whole: the verbs by
@@ -212,10 +237,12 @@ const TOOLS = [
212
237
  */
213
238
  export const PLUG_INSTRUCTIONS = `Huddle: you are one of several AI coding agents working on one repo at once, each in its own git worktree. These tools are how the team talks.
214
239
  - Start: huddle_board (the plan and who is doing what), huddle_recall (shared memory: decisions, contracts, the design council's notes: huddle_recall "council"), huddle_peers, huddle_inbox.
215
- - Before editing: huddle_announce_plan (what you will touch), huddle_check_overlap (is someone else in that file).
216
- - While working: huddle_progress (a short note; the reply shows the team's in-flight work), huddle_remember (a decision or contract others need), huddle_send (message one teammate), huddle_ask (ask a teammate or the brain a question), huddle_inbox (read replies).
240
+ - Before editing: huddle_announce_plan (what you will touch), huddle_check_overlap (is someone else in that file), huddle_contract (the exact names tasks share).
241
+ - Names other tasks use: code to the contract verbatim. Never invent one that isn't in it — huddle_amend_contract(section, change, why) first; it tells every teammate.
242
+ - While working: huddle_progress (a short note; the reply shows the team's in-flight work), huddle_remember (a decision or contract others need; kind "lesson" is kept for future runs), huddle_send (message one teammate), huddle_ask (ask a teammate or the brain a question), huddle_inbox (read replies).
217
243
  - Sharing work: when huddle_offer_help reaches you, huddle_split hands part of your task to a free agent; huddle_take_backlog picks up the next task; huddle_claim takes a specific one.
218
244
  - QA rounds: huddle_report_bug files one reproducible bug in the merged build; it becomes a fix task for the file's author.
245
+ - The shared screen (what the user pasted, dropped or uploaded — logs, docs, screenshots): huddle_context lists it, huddle_context_read opens one item in chunks (images/PDFs as a file path), huddle_context_search finds text in it, huddle_context_note pins a comment on an item for everyone.
219
246
  - Finish: huddle_done with a short summary. Huddle tests and merges it.`;
220
247
  /** The Plug's verb names, in the order listed to agents. */
221
248
  export const PLUG_VERBS = TOOLS.map((t) => t.name);
@@ -223,7 +250,7 @@ export async function runPlug() {
223
250
  // Attach mode (`huddle connect`): a session you started yourself, no scheduler.
224
251
  if (process.env.HUDDLE_LIVE)
225
252
  return (await import("../attach/plug.js")).runLivePlug();
226
- const server = new Server({ name: "huddle", version: "0.1.0" }, { capabilities: { tools: {} }, instructions: PLUG_INSTRUCTIONS });
253
+ const server = new Server({ name: "huddle", version: packageInfo().version }, { capabilities: { tools: {} }, instructions: PLUG_INSTRUCTIONS });
227
254
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
228
255
  tools: TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
229
256
  }));
@@ -232,7 +259,7 @@ export async function runPlug() {
232
259
  if (!tool)
233
260
  throw new Error(`unknown tool: ${req.params.name}`);
234
261
  const result = await tool.handler((req.params.arguments ?? {}));
235
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
262
+ return { content: toContent(result) };
236
263
  });
237
264
  const transport = new StdioServerTransport();
238
265
  await server.connect(transport);
@@ -1,11 +1,12 @@
1
1
  // One-shot, read-only questions to an agent: answering "where are we with
2
2
  // this project?" and planning a goal. Runs in the repo itself (no worktree),
3
3
  // with each vendor's read-only / plan mode, so nothing can be edited.
4
- import { spawn } from "node:child_process";
5
4
  import { randomUUID } from "node:crypto";
6
5
  import fs from "node:fs";
7
6
  import os from "node:os";
8
7
  import path from "node:path";
8
+ import { stopChild } from "./launcher.js";
9
+ import { onFinished, spawnTool } from "../platform.js";
9
10
  /** Preference order for who answers questions and plans. */
10
11
  const ASK_ORDER = ["claude", "codex", "antigravity", "cursor", "gemini"];
11
12
  export function pickAsker(ready) {
@@ -16,21 +17,20 @@ export function askAgent(adapter, prompt, cwd, opts = {}) {
16
17
  const outFile = path.join(os.tmpdir(), `huddle-ask-${process.pid}-${Date.now()}-${randomUUID().slice(0, 8)}.txt`);
17
18
  const spec = adapter.ask({ prompt, cwd, outFile, model: opts.model, noTools: opts.noTools, quick: opts.quick });
18
19
  return new Promise((resolve) => {
19
- const child = spawn(spec.cmd[0], spec.cmd.slice(1), { cwd: spec.cwd, stdio: ["ignore", "pipe", "pipe"] });
20
+ const child = spawnTool(spec.cmd, { cwd: spec.cwd, stdio: ["ignore", "pipe", "pipe"] }, { prompt, stdinPrompt: adapter.stdinPrompt });
20
21
  let out = "";
21
22
  let err = "";
22
23
  child.stdout.on("data", (d) => (out += d));
23
24
  child.stderr.on("data", (d) => (err += d));
24
25
  // SIGKILL after a grace period: a stalled CLI can ignore SIGTERM (seen live with cursor-agent).
25
- const stop = () => {
26
- if (child.exitCode !== null || child.signalCode !== null)
27
- return;
28
- child.kill("SIGTERM");
29
- setTimeout(() => child.exitCode === null && child.signalCode === null && child.kill("SIGKILL"), 5000).unref();
30
- };
26
+ const stop = () => stopChild(child);
31
27
  const timer = setTimeout(stop, opts.timeoutMs ?? 5 * 60_000);
32
28
  opts.signal?.addEventListener("abort", stop);
29
+ let over = false;
33
30
  const finish = (code) => {
31
+ if (over)
32
+ return;
33
+ over = true;
34
34
  clearTimeout(timer);
35
35
  let text = out;
36
36
  if (spec.usesOutFile) {
@@ -44,8 +44,11 @@ export function askAgent(adapter, prompt, cwd, opts = {}) {
44
44
  }
45
45
  resolve({ ok: code === 0 && text.trim().length > 0, text: text.trim() || err.trim() });
46
46
  };
47
- child.on("exit", finish);
48
- child.on("error", () => finish(-1));
47
+ // "exit" can arrive before the last of stdout has been read — when our event
48
+ // loop was busy (a merge's test run blocks it) the answer was lost and the
49
+ // review counted as timed out (found on CI). onFinished waits for the pipes
50
+ // to drain, but not forever: a CLI's leftover child can hold them open.
51
+ onFinished(child, finish);
49
52
  });
50
53
  }
51
54
  //# sourceMappingURL=ask.js.map
@@ -15,6 +15,8 @@ import fs from "node:fs";
15
15
  import path from "node:path";
16
16
  import { ADAPTERS } from "../adapters/index.js";
17
17
  import { askAgent } from "./ask.js";
18
+ import { lessonsBrief } from "../room/lessons.js";
19
+ import { screenForPlanner } from "../room/context.js";
18
20
  const OLLAMA = process.env.OLLAMA_HOST?.replace(/\/$/, "") || "http://127.0.0.1:11434";
19
21
  /** Models a local Ollama has pulled, or [] if Ollama isn't running. */
20
22
  export async function ollamaModels() {
@@ -100,8 +102,8 @@ export async function brainOptions(readyAgents) {
100
102
  out.push({ spec: "cursor:grok-4.7-low-fast", note: "cheap + fast, via your Cursor plan" });
101
103
  if (readyAgents.includes("claude")) {
102
104
  out.push({ spec: "claude:haiku", note: "smallest Claude — cheapest hosted option" });
103
- out.push({ spec: "claude:sonnet", note: "default — plans in ~15s, a fraction of Opus's tokens" });
104
- out.push({ spec: "claude", note: "Opus — strongest plans, most tokens" });
105
+ out.push({ spec: "claude:sonnet", note: "fast — plans in ~15s, a fraction of Opus's tokens" });
106
+ out.push({ spec: "claude:opus", note: "default — Opus, strongest plans, most tokens" });
105
107
  }
106
108
  if (readyAgents.includes("codex"))
107
109
  out.push({ spec: "codex", note: "your Codex default model" });
@@ -112,7 +114,7 @@ export async function brainOptions(readyAgents) {
112
114
  * manifest. ~a few thousand tokens; lets even a local model plan sensibly.
113
115
  */
114
116
  export function repoContext(cwd, maxFiles = 250) {
115
- const files = (spawnSync("git", ["ls-files"], { cwd, encoding: "utf8" }).stdout ?? "").split("\n").filter(Boolean);
117
+ const files = (spawnSync("git", ["ls-files"], { cwd, encoding: "utf8" }).stdout ?? "").split(/\r?\n/).filter(Boolean);
116
118
  const head = (f, lines) => {
117
119
  try {
118
120
  return fs.readFileSync(path.join(cwd, f), "utf8").split("\n").slice(0, lines).join("\n");
@@ -159,6 +161,14 @@ export function repoContext(cwd, maxFiles = 250) {
159
161
  parts.push(`${f}:\n${body.trimEnd()}`);
160
162
  }
161
163
  }
164
+ // The planner and the council plan from this snapshot: what earlier runs taught goes with it.
165
+ const lessons = lessonsBrief({ repo: cwd }).trim();
166
+ if (lessons)
167
+ parts.push(lessons);
168
+ // What the user pasted / dropped: the index (+ small items), not N full copies.
169
+ const screen = screenForPlanner(cwd);
170
+ if (screen)
171
+ parts.push(screen);
162
172
  return parts.join("\n\n");
163
173
  }
164
174
  //# sourceMappingURL=brain.js.map