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.
- package/CHANGELOG.md +31 -0
- package/QUICKSTART.md +102 -0
- package/README.md +23 -4
- package/dist/adapters/amp.js +1 -1
- package/dist/adapters/antigravity.js +2 -0
- package/dist/adapters/claude.js +19 -3
- package/dist/adapters/codex.js +6 -1
- package/dist/adapters/cursor.js +12 -5
- package/dist/adapters/custom.js +4 -2
- package/dist/adapters/droid.js +1 -0
- package/dist/adapters/goose.js +2 -0
- package/dist/adapters/index.js +44 -18
- package/dist/adapters/kiro.js +1 -1
- package/dist/adapters/opencode.js +1 -1
- package/dist/attach/connect.js +4 -1
- package/dist/attach/guard.js +21 -2
- package/dist/attach/live.js +5 -1
- package/dist/attach/plug.js +14 -3
- package/dist/attach/screen.js +150 -0
- package/dist/attach/session.js +7 -2
- package/dist/cli.js +108 -40
- package/dist/mcp/context-tools.js +79 -0
- package/dist/mcp/server.js +38 -11
- package/dist/orchestrator/ask.js +13 -10
- package/dist/orchestrator/brain.js +13 -3
- package/dist/orchestrator/council.js +427 -86
- package/dist/orchestrator/goal.js +196 -0
- package/dist/orchestrator/handoff.js +2 -2
- package/dist/orchestrator/launcher.js +29 -13
- package/dist/orchestrator/learn.js +105 -0
- package/dist/orchestrator/planner.js +25 -7
- package/dist/orchestrator/qa.js +62 -14
- package/dist/orchestrator/radar.js +719 -0
- package/dist/orchestrator/scheduler.js +498 -39
- package/dist/orchestrator/takeover.js +155 -0
- package/dist/platform.js +273 -0
- package/dist/room/context.js +1285 -0
- package/dist/room/contract.js +132 -0
- package/dist/room/db.js +79 -16
- package/dist/room/followup.js +233 -0
- package/dist/room/identity.js +2 -2
- package/dist/room/lessons.js +275 -0
- package/dist/room/live.js +75 -20
- package/dist/room/mail.js +212 -0
- package/dist/room/progress.js +40 -1
- package/dist/room/server.js +169 -39
- package/dist/room/tunnel.js +6 -5
- package/dist/sandbox/sync.js +4 -4
- package/dist/sandbox/worktree.js +145 -14
- package/dist/ui/app.js +554 -133
- package/dist/ui/catalog.js +62 -24
- package/dist/ui/dashboard.js +194 -51
- package/dist/ui/doctor.js +15 -11
- package/dist/ui/lessons.js +81 -0
- package/dist/ui/paste.js +515 -0
- package/dist/ui/remote.js +11 -5
- package/dist/ui/setup.js +152 -0
- package/dist/ui/story.js +666 -69
- package/dist/ui/term.js +151 -16
- package/dist/ui/watch.js +145 -48
- package/package.json +6 -3
package/dist/ui/catalog.js
CHANGED
|
@@ -1,34 +1,45 @@
|
|
|
1
1
|
// `huddle agents --all`: every connector Huddle knows — built in, or declared
|
|
2
2
|
// in agents.json — with whether it's on this machine and how to get it.
|
|
3
3
|
// `huddle agents install <id>` installs one, always after asking.
|
|
4
|
+
import os from "node:os";
|
|
4
5
|
import readline from "node:readline/promises";
|
|
5
|
-
import {
|
|
6
|
-
import { ADAPTERS, AGENTS_FILE_ERRORS, BUILTIN_ADAPTERS, probe } from "../adapters/index.js";
|
|
6
|
+
import { ADAPTERS, AGENTS_FILE_ERRORS, BUILTIN_ADAPTERS, LEGACY, installCommand, noInstallHint, probe } from "../adapters/index.js";
|
|
7
7
|
import { agentsFiles } from "../adapters/custom.js";
|
|
8
|
-
import {
|
|
8
|
+
import { runInteractive, setAgentChoice, skippedAgents } from "./app.js";
|
|
9
|
+
import { C, dot, lineWidth, pad, wrap } from "./term.js";
|
|
9
10
|
function all() {
|
|
10
11
|
const seen = new Map();
|
|
11
12
|
for (const a of [...BUILTIN_ADAPTERS, ...ADAPTERS])
|
|
12
13
|
seen.set(a.id, a); // the roster's version wins (agents.json overrides)
|
|
13
14
|
return [...seen.values()];
|
|
14
15
|
}
|
|
15
|
-
const tilde = (p) => p.replace(
|
|
16
|
+
const tilde = (p) => p.replace(os.homedir(), "~");
|
|
17
|
+
const stateText = (r) => r.state === "ready" ? C.green("ready") : r.state === "not_authed" ? C.yellow("not signed in") : C.faint("not installed");
|
|
18
|
+
/** One agent's row: state, whether you use it, and the next command when there is one. */
|
|
19
|
+
function printRow(a, r, skipped) {
|
|
20
|
+
const from = a.source ? C.faint(` · ${tilde(a.source)}`) : "";
|
|
21
|
+
const choice = skipped ? C.faint("skipped") : C.violet("used");
|
|
22
|
+
console.log(` ${dot(a.id)} ${pad(C.bold(a.id), 13)} ${pad(C.faint(a.vendor), 13)} ${pad(stateText(r), 14)} ${from ? pad(choice, 8) + from : choice}`);
|
|
23
|
+
if (skipped)
|
|
24
|
+
return;
|
|
25
|
+
const cmd = installCommand(a);
|
|
26
|
+
// Hints wrap under the row (80 columns when piped), never past the edge.
|
|
27
|
+
const hint = (t) => wrap(t, (lineWidth() ?? 80) - 7, " ").forEach((l) => console.log(` ${C.faint(l)}`));
|
|
28
|
+
if (r.state === "not_installed")
|
|
29
|
+
hint(cmd ? "$ " + cmd : noInstallHint(a));
|
|
30
|
+
else if (r.state === "not_authed")
|
|
31
|
+
hint(r.detail);
|
|
32
|
+
}
|
|
16
33
|
export function printCatalog() {
|
|
17
34
|
console.log(`\n${C.bold("Connectors")} ${C.faint("· any CLI that runs headless and speaks MCP can join")}\n`);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const from = a.source ? C.faint(` · ${tilde(a.source)}`) : "";
|
|
22
|
-
console.log(` ${dot(a.id)} ${pad(C.bold(a.id), 13)} ${pad(C.faint(a.vendor), 13)} ${pad(state, 14)}${from}`);
|
|
23
|
-
if (r.state === "not_installed" && a.install.cmd)
|
|
24
|
-
console.log(` ${C.faint("$ " + a.install.cmd)}`);
|
|
25
|
-
else if (r.state === "not_authed")
|
|
26
|
-
console.log(` ${C.faint(r.detail)}`);
|
|
27
|
-
}
|
|
35
|
+
const skip = skippedAgents();
|
|
36
|
+
for (const a of all())
|
|
37
|
+
printRow(a, probe(a), skip.has(a.id));
|
|
28
38
|
for (const e of AGENTS_FILE_ERRORS)
|
|
29
39
|
console.log(` ${C.yellow("!")} ${C.faint(e)}`);
|
|
30
40
|
const f = agentsFiles(null);
|
|
31
41
|
console.log(`\n ${C.faint("install one:")} huddle agents install <id>`);
|
|
42
|
+
console.log(` ${C.faint("choose:")} huddle agents use <id> ${C.faint("·")} huddle agents skip <id>`);
|
|
32
43
|
console.log(` ${C.faint("add your own:")} ${tilde(f.user)} ${C.faint("(see docs/ADAPTERS.md)")}\n`);
|
|
33
44
|
}
|
|
34
45
|
/** Install a connector's CLI (and offer its sign-in) — only after a yes. */
|
|
@@ -38,32 +49,29 @@ export async function installConnector(id) {
|
|
|
38
49
|
console.log(C.red(`✗ no connector called ${id}.`) + " See `huddle agents --all`.");
|
|
39
50
|
return false;
|
|
40
51
|
}
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
const cmd = installCommand(a);
|
|
53
|
+
if (!cmd) {
|
|
54
|
+
console.log(`${a.id}: ${noInstallHint(a)}`);
|
|
43
55
|
return false;
|
|
44
56
|
}
|
|
45
57
|
if (!process.stdin.isTTY) {
|
|
46
|
-
console.log(`To install ${a.id}, run:\n ${
|
|
58
|
+
console.log(`To install ${a.id}, run:\n ${cmd}`);
|
|
47
59
|
return false;
|
|
48
60
|
}
|
|
49
61
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
50
62
|
try {
|
|
51
|
-
console.log(`\n ${dot(a.id)} ${C.bold(a.id)} ${C.faint("$ " +
|
|
63
|
+
console.log(`\n ${dot(a.id)} ${C.bold(a.id)} ${C.faint("$ " + cmd)}`);
|
|
52
64
|
const ans = (await rl.question(`Install ${a.id}? ${C.faint("[Y/n]")} `)).trim().toLowerCase();
|
|
53
65
|
if (ans && !ans.startsWith("y"))
|
|
54
66
|
return false;
|
|
55
|
-
rl
|
|
56
|
-
const ok = spawnSync("sh", ["-c", a.install.cmd], { stdio: "inherit" }).status === 0;
|
|
57
|
-
rl.resume();
|
|
67
|
+
const ok = runInteractive(rl, cmd);
|
|
58
68
|
console.log(ok ? C.green(`✓ ${a.id} installed`) : C.red(`✗ ${a.id} install failed — see output above`));
|
|
59
69
|
if (ok && a.install.login) {
|
|
60
70
|
const go = (await rl.question(`Sign in to ${a.id} now? ${C.faint("[Y/n]")} `)).trim().toLowerCase();
|
|
61
71
|
if (!go || go.startsWith("y")) {
|
|
62
72
|
if (a.install.loginNote)
|
|
63
73
|
console.log(C.faint(` ${a.install.loginNote}`));
|
|
64
|
-
rl.
|
|
65
|
-
spawnSync(a.install.login[0], a.install.login.slice(1), { stdio: "inherit" });
|
|
66
|
-
rl.resume();
|
|
74
|
+
runInteractive(rl, a.install.login);
|
|
67
75
|
}
|
|
68
76
|
}
|
|
69
77
|
return ok;
|
|
@@ -72,4 +80,34 @@ export async function installConnector(id) {
|
|
|
72
80
|
rl.close();
|
|
73
81
|
}
|
|
74
82
|
}
|
|
83
|
+
/** `huddle agents`: the agents Huddle would use here, their state, and your use/skip choice. */
|
|
84
|
+
export function printAgents() {
|
|
85
|
+
const skip = skippedAgents();
|
|
86
|
+
console.log(`\n${C.bold("Your agents")}\n`);
|
|
87
|
+
for (const a of ADAPTERS) {
|
|
88
|
+
const r = probe(a);
|
|
89
|
+
// Opt-in connectors stay out of the list until their CLI is on this machine.
|
|
90
|
+
if (LEGACY.has(a.id) && r.state === "not_installed")
|
|
91
|
+
continue;
|
|
92
|
+
printRow(a, r, skip.has(a.id));
|
|
93
|
+
}
|
|
94
|
+
console.log(`\n ${C.faint("choose:")} huddle agents use claude,codex ${C.faint("·")} huddle agents skip cursor`);
|
|
95
|
+
console.log(` ${C.faint("set up the rest:")} huddle setup`);
|
|
96
|
+
console.log(` ${C.faint("every connector:")} huddle agents --all\n`);
|
|
97
|
+
}
|
|
98
|
+
/** `huddle agents use|skip <ids>`: remembered in ~/.huddle/prefs.json. */
|
|
99
|
+
export function chooseAgents(verb, list) {
|
|
100
|
+
const ids = list.flatMap((x) => x.split(",")).map((s) => s.trim()).filter(Boolean);
|
|
101
|
+
const known = new Set(all().map((a) => a.id));
|
|
102
|
+
const unknown = ids.filter((id) => !known.has(id));
|
|
103
|
+
if (!ids.length || unknown.length) {
|
|
104
|
+
console.log(C.red(`✗ no agent called ${unknown.join(", ") || "(none given)"}.`) + ` Known: ${[...known].join(", ")}`);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
setAgentChoice(ids, verb === "use");
|
|
108
|
+
console.log(verb === "use"
|
|
109
|
+
? `${C.green("✓")} using ${ids.join(", ")} ${C.faint("— `huddle setup` installs or signs in any that aren't ready")}`
|
|
110
|
+
: `${C.green("✓")} skipping ${ids.join(", ")} ${C.faint("— never installed, asked about, or put to work. Undo: huddle agents use " + ids.join(","))}`);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
75
113
|
//# sourceMappingURL=catalog.js.map
|
package/dist/ui/dashboard.js
CHANGED
|
@@ -1,18 +1,61 @@
|
|
|
1
1
|
// The live view — the full-screen dashboard from the launch film.
|
|
2
2
|
//
|
|
3
|
-
// ⬢ huddle
|
|
4
|
-
//
|
|
5
|
-
// │ ▸ Add greet(name) with tests
|
|
6
|
-
// │ plans: CommonJS greet.js …
|
|
7
|
-
//
|
|
8
|
-
// ◆
|
|
3
|
+
// ⬢ huddle 1 agents 2 plan 3 board BUILD · ● live · ⏱ 1m12s
|
|
4
|
+
// ╭─ ● claude working 40s · 31k tok ──────╮╭─ ● codex idle · 1 merged ───╮
|
|
5
|
+
// │ ▸ Add greet(name) with tests 60% ││ ✓ merged Add farewell… │
|
|
6
|
+
// │ plans: CommonJS greet.js … ││ │
|
|
7
|
+
// ╰────────────────────────────────────────╯╰───────────────────────────────╯
|
|
8
|
+
// ◆ codex → claude: what shape is the Order type?
|
|
9
9
|
// 1 2 3 switch · q back to the prompt · ctrl-c stop agents
|
|
10
10
|
//
|
|
11
11
|
// It only reads: the Room's board and memory, the run's live stats, and the
|
|
12
|
-
// activity feed. Pressing q hands the terminal back to the prompt.
|
|
12
|
+
// activity feed. Pressing q hands the terminal back to the prompt. Every row
|
|
13
|
+
// is cut to the screen's width — a row that wraps scrolls the whole frame.
|
|
13
14
|
import { C, dot, pad, truncate, width, AGENT_COLOR } from "./term.js";
|
|
14
15
|
import { fmtDuration, fmtTokens } from "../adapters/usage.js";
|
|
16
|
+
import { taskViews } from "../room/progress.js";
|
|
17
|
+
import { getContract } from "../room/contract.js";
|
|
18
|
+
import { screenPanel } from "../room/context.js";
|
|
19
|
+
const PHASE_OF = {
|
|
20
|
+
"council.proposal": "COUNCIL",
|
|
21
|
+
"council.critique": "COUNCIL",
|
|
22
|
+
"council.adopt": "COUNCIL",
|
|
23
|
+
"council.flaw": "COUNCIL",
|
|
24
|
+
"council.ratify": "COUNCIL",
|
|
25
|
+
"council.plan": "PLAN",
|
|
26
|
+
"plan.made": "PLAN",
|
|
27
|
+
"contract.set": "PLAN",
|
|
28
|
+
"run.start": "BUILD",
|
|
29
|
+
"qa.round": "QA",
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The run's phase: the latest phase-marking event since the session began,
|
|
33
|
+
* and DONE once every task on the board is merged or failed and no agent is busy.
|
|
34
|
+
*/
|
|
35
|
+
export function runPhase(db, busy) {
|
|
36
|
+
if (!db)
|
|
37
|
+
return "WAITING";
|
|
38
|
+
let tasks = [];
|
|
39
|
+
let since = 0;
|
|
40
|
+
try {
|
|
41
|
+
tasks = db.listTasks();
|
|
42
|
+
since = (db.currentSession()?.created_at ?? 1) - 1;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return "WAITING"; // a closed db
|
|
46
|
+
}
|
|
47
|
+
const last = db.eventsOfType(Object.keys(PHASE_OF), since, 1).at(-1);
|
|
48
|
+
let phase = last ? PHASE_OF[last.type] : "WAITING";
|
|
49
|
+
// A QA round's fix tasks are BUILD work, but the run is still in QA.
|
|
50
|
+
if (phase !== "QA" && phase !== "WAITING" && db.eventsOfType(["qa.round"], since, 1).length)
|
|
51
|
+
phase = "QA";
|
|
52
|
+
if (!busy && tasks.length && tasks.every((t) => t.status === "merged" || t.status === "failed"))
|
|
53
|
+
return "DONE";
|
|
54
|
+
return phase;
|
|
55
|
+
}
|
|
15
56
|
const hexColor = (id) => C.hex(AGENT_COLOR[id.split("/").pop().split("#")[0]] ?? "#8A857C");
|
|
57
|
+
/** The shared screen's 📎/📌 rows in the view's glyphs (emoji are two cells wide and vary by font). */
|
|
58
|
+
const screenGlyphs = (l) => l.replace(/^📎/u, C.violet("+")).replace(/^📌/u, C.violet("◆"));
|
|
16
59
|
export class Dashboard {
|
|
17
60
|
src;
|
|
18
61
|
tab = "agents";
|
|
@@ -82,105 +125,205 @@ export class Dashboard {
|
|
|
82
125
|
render() {
|
|
83
126
|
if (!this.open)
|
|
84
127
|
return;
|
|
85
|
-
const
|
|
86
|
-
|
|
128
|
+
const lines = this.frame(process.stdout.columns || 100, process.stdout.rows || 30);
|
|
129
|
+
process.stdout.write("\x1b[H" + lines.map((l) => "\x1b[2K" + l).join("\n"));
|
|
130
|
+
}
|
|
131
|
+
/** One full screen, `H` rows each at most `W` cells (also how tests and samples render it). */
|
|
132
|
+
frame(W, H, tab = this.tab, now = Date.now()) {
|
|
87
133
|
const db = this.src.db();
|
|
88
134
|
const stats = this.src.stats();
|
|
89
|
-
const
|
|
135
|
+
const busy = !!stats && [...stats.agents.values()].some((a) => a.busySince);
|
|
136
|
+
const phase = runPhase(db, busy);
|
|
137
|
+
let goal = "";
|
|
138
|
+
try {
|
|
139
|
+
goal = db?.currentSession()?.goal ?? "";
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
/* a closed db */
|
|
143
|
+
}
|
|
90
144
|
const tabs = ["agents", "plan", "board"]
|
|
91
|
-
.map((t, i) => (t ===
|
|
145
|
+
.map((t, i) => (t === tab ? C.bold(C.violet(`[${i + 1} ${t}]`)) : C.faint(` ${i + 1} ${t} `)))
|
|
92
146
|
.join(" ");
|
|
93
|
-
const
|
|
94
|
-
|
|
147
|
+
const clock = stats ? `${C.faint("⏱")} ${fmtDuration((stats.endedAt ?? now) - stats.startedAt)}` : "";
|
|
148
|
+
const state = phase === "DONE"
|
|
149
|
+
? C.green("✓ done")
|
|
150
|
+
: phase === "WAITING"
|
|
151
|
+
? C.faint("○ waiting for a run")
|
|
152
|
+
: `${C.bold(C.violet(phase))} ${C.faint("·")} ${busy ? C.green("● live") : C.faint("○ between steps")}`;
|
|
153
|
+
const right = [state, clock].filter(Boolean).join(C.faint(" · "));
|
|
95
154
|
const bar = ` ${C.violet("⬢")} ${C.bold("huddle")} ${tabs}`;
|
|
96
|
-
|
|
155
|
+
// The goal takes whatever room is left between the tabs and the status.
|
|
156
|
+
const room = W - width(bar) - width(right) - 4;
|
|
157
|
+
const goalS = goal && room >= 12 ? C.faint(truncate(goal.replace(/\s+/g, " "), room)) : "";
|
|
158
|
+
const gap = Math.max(1, W - width(bar) - width(goalS) - width(right) - 3);
|
|
159
|
+
const top = goalS ? `${bar} ${goalS}${" ".repeat(gap)}${right}` : `${bar}${" ".repeat(Math.max(1, W - width(bar) - width(right) - 1))}${right}`;
|
|
97
160
|
const bodyH = H - 4;
|
|
98
|
-
const body =
|
|
161
|
+
const body = tab === "agents" ? this.agents(W, bodyH, stats, db, now) : tab === "plan" ? this.plan(W, bodyH, db) : this.board(W, bodyH, db);
|
|
99
162
|
const lastRoom = [...this.feed].reverse().find((a) => !a.agent);
|
|
100
|
-
const strip = lastRoom ? ` ${
|
|
101
|
-
const hint = C.faint(` ${this.src.hint ?? "1 2 3 switch · q back to the prompt · ctrl-c stop agents"}
|
|
102
|
-
const lines = [
|
|
103
|
-
while (lines.length < H)
|
|
104
|
-
lines.
|
|
105
|
-
|
|
163
|
+
const strip = lastRoom ? ` ${lastRoom.text}` : C.faint(" the room is quiet");
|
|
164
|
+
const hint = C.faint(` ${this.src.hint ?? "1 2 3 switch · q back to the prompt · ctrl-c stop agents"} `) + this.src.modeChip();
|
|
165
|
+
const lines = [top, C.faint("─".repeat(W)), ...body.slice(0, bodyH)];
|
|
166
|
+
while (lines.length < H - 2)
|
|
167
|
+
lines.push("");
|
|
168
|
+
lines.push(strip, hint);
|
|
169
|
+
return lines.slice(0, H).map((l) => truncate(l.replace(/[\r\n]+/g, " "), W));
|
|
106
170
|
}
|
|
107
171
|
/** Tab 1: a pane per agent — what each one is doing right now. */
|
|
108
|
-
agents(W, H, stats) {
|
|
172
|
+
agents(W, H, stats, db, now) {
|
|
109
173
|
const ids = stats ? [...stats.agents.keys()] : [...new Set(this.feed.filter((a) => a.agent).map((a) => a.agent))];
|
|
110
174
|
if (!ids.length)
|
|
111
|
-
return [C.faint("
|
|
175
|
+
return ["", C.faint(` ${this.src.empty ?? "no agents working — give Huddle a goal"}`)];
|
|
176
|
+
let views = [];
|
|
177
|
+
try {
|
|
178
|
+
views = db ? taskViews(db, now) : [];
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
/* a closed db: no pinned task line */
|
|
182
|
+
}
|
|
112
183
|
const cols = W >= 110 && ids.length > 1 ? 2 : 1;
|
|
113
184
|
const rows = Math.ceil(ids.length / cols);
|
|
114
185
|
const paneW = Math.floor(W / cols);
|
|
115
186
|
const paneH = Math.max(4, Math.floor(H / rows));
|
|
116
187
|
const out = [];
|
|
117
188
|
for (let r = 0; r < rows; r++) {
|
|
118
|
-
const panes = ids.slice(r * cols, r * cols + cols).map((id) => this.pane(id, paneW, paneH, stats?.agents.get(id)));
|
|
189
|
+
const panes = ids.slice(r * cols, r * cols + cols).map((id) => this.pane(id, paneW, paneH, stats?.agents.get(id), views, now));
|
|
119
190
|
for (let i = 0; i < paneH; i++)
|
|
120
191
|
out.push(panes.map((p) => p[i] ?? " ".repeat(paneW)).join(""));
|
|
121
192
|
}
|
|
122
193
|
return out;
|
|
123
194
|
}
|
|
124
|
-
pane(id, w, h, a) {
|
|
195
|
+
pane(id, w, h, a, views, now) {
|
|
125
196
|
const color = hexColor(id);
|
|
126
197
|
const status = a?.busySince
|
|
127
|
-
? `${C.violet("working")} ${fmtDuration(
|
|
128
|
-
: a && (a.tasks || a.reviews)
|
|
129
|
-
? C.green(`
|
|
198
|
+
? `${C.violet("working")} ${fmtDuration(now - a.busySince)}${a.liveTokens > 0 ? " · " + fmtTokens(a.liveTokens) + " tok" : ""}`
|
|
199
|
+
: a && (a.tasks || a.reviews || a.qa)
|
|
200
|
+
? C.green(`idle · ${a.tasks} merged${a.reviews ? ` · ${a.reviews} review${a.reviews === 1 ? "" : "s"}` : ""}${a.qa ? ` · ${a.qa} QA` : ""}`)
|
|
130
201
|
: C.faint("idle");
|
|
131
|
-
const title = ` ${dot(id)} ${C.bold(id)} ${status} `;
|
|
132
202
|
const inner = w - 2;
|
|
203
|
+
const title = truncate(` ${dot(id)} ${C.bold(id)} ${status} `, inner - 1);
|
|
133
204
|
const topLine = color("╭─") + title + color("─".repeat(Math.max(0, inner - 1 - width(title))) + "╮");
|
|
134
|
-
|
|
205
|
+
// The task it's on stays pinned at the top: title, %, how long since it last said anything.
|
|
206
|
+
const on = a?.busySince ? views.find((v) => v.owner === id && (v.status === "claimed" || v.status === "in_progress")) : undefined;
|
|
207
|
+
const pinned = [];
|
|
208
|
+
if (on) {
|
|
209
|
+
const pct = on.reported ? C.violet(`${on.progress_pct}%`) : C.faint("no report yet");
|
|
210
|
+
const said = on.last_note ? C.faint(` · note ${fmtDuration(on.last_update_s * 1000)} ago`) : "";
|
|
211
|
+
const tail = ` ${pct}${said}`;
|
|
212
|
+
pinned.push(`${C.violet("▸")} ${C.bold(truncate(on.title, Math.max(8, inner - 4 - width(tail))))}${tail}`);
|
|
213
|
+
}
|
|
214
|
+
const room = h - 2 - pinned.length;
|
|
215
|
+
const lines = [...pinned, ...this.feed.filter((x) => x.agent === id).slice(-room).map((x) => x.text)];
|
|
135
216
|
const body = Array.from({ length: h - 2 }, (_, i) => {
|
|
136
|
-
const l = lines[i] ? " " + truncate(lines[i]
|
|
217
|
+
const l = lines[i] ? " " + truncate(lines[i], inner - 2) : "";
|
|
137
218
|
return color("│") + pad(l, inner) + color("│");
|
|
138
219
|
});
|
|
139
220
|
return [topLine, ...body, color("╰" + "─".repeat(inner) + "╯")];
|
|
140
221
|
}
|
|
141
|
-
/** Tab 2: the plan
|
|
222
|
+
/** Tab 2: the plan, and the room talking (side by side when there's room, stacked when not). */
|
|
142
223
|
plan(W, H, db) {
|
|
143
|
-
const leftW = Math.floor(W * 0.55);
|
|
144
224
|
const left = [C.violet(" PLAN")];
|
|
145
|
-
|
|
225
|
+
let tasks = [];
|
|
226
|
+
try {
|
|
227
|
+
tasks = db?.listTasks() ?? [];
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
/* a closed db */
|
|
231
|
+
}
|
|
146
232
|
const idx = new Map(tasks.map((t, i) => [t.id, i + 1]));
|
|
147
233
|
tasks.forEach((t, i) => {
|
|
148
234
|
const deps = JSON.parse(t.depends_on || "[]");
|
|
149
235
|
const after = deps.length ? C.faint(` after ${deps.map((d) => idx.get(d)).join(",")}`) : "";
|
|
150
236
|
const who = t.claimed_by ? ` ${dot(t.claimed_by)} ${C.faint(t.claimed_by)}` : "";
|
|
151
|
-
left.push(` ${C.faint(String(i + 1) + ".")} ${t.title}${after}${who}`);
|
|
152
|
-
left.push(C.faint(` ${t.status}`));
|
|
237
|
+
left.push(` ${mark(t.status, !!t.claimed_by)} ${C.faint(String(i + 1).padStart(2) + ".")} ${t.title}${after}${who}`);
|
|
153
238
|
});
|
|
154
239
|
if (!tasks.length)
|
|
155
240
|
left.push(C.faint(" no plan yet"));
|
|
241
|
+
let contract = null;
|
|
242
|
+
try {
|
|
243
|
+
contract = db ? getContract(db) : null;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
/* a closed db: no contract line */
|
|
247
|
+
}
|
|
248
|
+
if (contract && (contract.text || contract.amendments.length)) {
|
|
249
|
+
left.push("", `${C.violet(" CONTRACT")} ${C.faint(`v${contract.version} · ${contract.amendments.length} amendment(s)`)}`);
|
|
250
|
+
for (const a of contract.amendments.slice(-3))
|
|
251
|
+
left.push(C.faint(` v${a.version} ${a.by}: ${a.section}: ${a.change}`));
|
|
252
|
+
}
|
|
156
253
|
const right = [C.violet(" ROOM") + C.faint(" · agents & people talking")];
|
|
157
254
|
for (const a of this.feed.filter((x) => !x.agent).slice(-(H - 1)))
|
|
158
255
|
right.push(" " + a.text);
|
|
159
|
-
|
|
256
|
+
if (right.length === 1)
|
|
257
|
+
right.push(C.faint(" nothing said yet"));
|
|
258
|
+
return columns(left, right, W, H, true);
|
|
160
259
|
}
|
|
161
|
-
/** Tab 3: progress
|
|
260
|
+
/** Tab 3: progress, and the shared screen + memory. */
|
|
162
261
|
board(W, H, db) {
|
|
163
|
-
const leftW = Math.floor(W * 0.55);
|
|
164
262
|
const left = [C.violet(" BOARD") + C.faint(" · live progress")];
|
|
165
|
-
|
|
166
|
-
|
|
263
|
+
let views = [];
|
|
264
|
+
try {
|
|
265
|
+
views = db ? taskViews(db) : [];
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
/* a closed db */
|
|
269
|
+
}
|
|
270
|
+
const bw = W >= 100 ? 14 : 8;
|
|
271
|
+
for (const t of views) {
|
|
167
272
|
const filled = Math.round((t.progress_pct / 100) * bw);
|
|
168
273
|
const barS = t.status === "merged" ? C.green("█".repeat(bw)) : C.violet("█".repeat(filled)) + C.faint("░".repeat(bw - filled));
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
274
|
+
// Never reported (no huddle_progress yet): say so instead of showing a made-up 0%.
|
|
275
|
+
const silent = ["claimed", "in_progress"].includes(t.status) && !t.reported;
|
|
276
|
+
const who = t.owner ? ` ${dot(t.owner)} ${C.faint(t.owner)}` : "";
|
|
277
|
+
left.push(` ${mark(t.status, !!t.owner)} ${barS} ${silent ? C.faint(" — ") : String(t.progress_pct).padStart(3) + "%"} ${t.title}${who}`);
|
|
278
|
+
}
|
|
279
|
+
if (!views.length)
|
|
280
|
+
left.push(C.faint(" no tasks yet"));
|
|
281
|
+
const right = [];
|
|
282
|
+
// The shared screen: what the user put up, and the notes pinned on it.
|
|
283
|
+
let screen = [];
|
|
284
|
+
let memory = [];
|
|
285
|
+
try {
|
|
286
|
+
screen = db ? screenPanel(db, Math.min(8, Math.floor(H / 2))) : [];
|
|
287
|
+
memory = db?.recall() ?? [];
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
/* a closed db */
|
|
291
|
+
}
|
|
292
|
+
if (screen.length)
|
|
293
|
+
right.push(C.violet(" SCREEN") + C.faint(" · pastes & files every agent reads"), ...screen.map((l) => " " + screenGlyphs(l)), "");
|
|
294
|
+
right.push(C.violet(" SHARED MEMORY") + C.faint(" · everyone reads & writes"));
|
|
295
|
+
for (const m of memory.slice(0, H - 1)) {
|
|
296
|
+
right.push(` ${C.violet("◆")} ${m.author ? C.faint(m.author + ": ") : ""}${m.body}`);
|
|
177
297
|
if (m.why)
|
|
178
298
|
right.push(C.faint(` why: ${m.why}`));
|
|
179
299
|
}
|
|
180
|
-
|
|
300
|
+
if (!memory.length)
|
|
301
|
+
right.push(C.faint(" nothing yet"));
|
|
302
|
+
return columns(left, right, W, H);
|
|
181
303
|
}
|
|
182
304
|
}
|
|
183
|
-
|
|
305
|
+
/** A task's state as one glyph, the same ones the run's output uses. */
|
|
306
|
+
function mark(status, claimed) {
|
|
307
|
+
if (status === "merged")
|
|
308
|
+
return C.green("✓");
|
|
309
|
+
if (status === "failed")
|
|
310
|
+
return C.red("✗");
|
|
311
|
+
if (status === "needs_you")
|
|
312
|
+
return C.yellow("!");
|
|
313
|
+
if (status === "review")
|
|
314
|
+
return C.violet("◎");
|
|
315
|
+
return claimed ? C.violet("▸") : C.faint("○");
|
|
316
|
+
}
|
|
317
|
+
/** Two columns side by side on a wide screen; on a narrow one, the left on top of the right. */
|
|
318
|
+
function columns(left, right, W, H, latest = false) {
|
|
319
|
+
if (W < 100) {
|
|
320
|
+
const top = left.slice(0, Math.max(3, Math.min(left.length, H - Math.min(right.length, Math.floor(H / 2)) - 1)));
|
|
321
|
+
const n = Math.max(0, H - top.length - 1);
|
|
322
|
+
// A feed keeps its newest lines (its header stays).
|
|
323
|
+
const rest = latest && right.length > n ? [right[0], ...right.slice(1).slice(-(n - 1))].slice(0, n) : right.slice(0, n);
|
|
324
|
+
return [...top, C.faint("─".repeat(W)), ...rest].map((l) => truncate(l, W));
|
|
325
|
+
}
|
|
326
|
+
const leftW = Math.floor(W * 0.55);
|
|
184
327
|
const out = [];
|
|
185
328
|
for (let i = 0; i < H; i++) {
|
|
186
329
|
const l = truncate(left[i] ?? "", leftW - 1);
|
package/dist/ui/doctor.js
CHANGED
|
@@ -6,7 +6,8 @@ import { spawnSync } from "node:child_process";
|
|
|
6
6
|
import { LEGACY } from "../adapters/index.js";
|
|
7
7
|
import { packageInfo } from "../preflight.js";
|
|
8
8
|
import { sqliteStatus } from "../room/sqlite.js";
|
|
9
|
-
import { connectAgents } from "./app.js";
|
|
9
|
+
import { connectAgents, installLine } from "./app.js";
|
|
10
|
+
import { gitFix } from "../platform.js";
|
|
10
11
|
import { C, dot } from "./term.js";
|
|
11
12
|
export async function runDoctor() {
|
|
12
13
|
let blocked = false;
|
|
@@ -23,7 +24,7 @@ export async function runDoctor() {
|
|
|
23
24
|
if (gitNum === null) {
|
|
24
25
|
blocked = true;
|
|
25
26
|
console.log(C.red("\n✗ git isn't installed.") + " Huddle gives every agent its own git worktree.");
|
|
26
|
-
console.log(C.faint("
|
|
27
|
+
console.log(C.faint(" " + gitFix()));
|
|
27
28
|
}
|
|
28
29
|
if (!db.ok) {
|
|
29
30
|
blocked = true;
|
|
@@ -35,20 +36,23 @@ export async function runDoctor() {
|
|
|
35
36
|
return blocked ? 1 : 0;
|
|
36
37
|
}
|
|
37
38
|
/** What to do when agents are missing or signed out. Returns true if at least one is ready. */
|
|
38
|
-
function nextSteps(results) {
|
|
39
|
+
export function nextSteps(results, print = console.log) {
|
|
39
40
|
const ready = results.filter((r) => r.state === "ready");
|
|
41
|
+
const pending = results.filter((r) => r.state === "not_authed" || (r.state === "not_installed" && !LEGACY.has(r.adapter.id)));
|
|
40
42
|
if (ready.length) {
|
|
41
|
-
|
|
43
|
+
print(C.faint(`\n ${ready.length} agent(s) ready — cd into a repo and run: huddle`));
|
|
44
|
+
if (pending.length)
|
|
45
|
+
print(C.faint(` set up the rest (${pending.map((r) => r.adapter.id).join(", ")}): huddle setup`));
|
|
42
46
|
return true;
|
|
43
47
|
}
|
|
44
48
|
const missing = results.filter((r) => r.state === "not_installed" && !LEGACY.has(r.adapter.id));
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
print(`\n${C.yellow("No agents ready yet.")} Huddle runs the coding agents you already use,`);
|
|
50
|
+
print("each signed in to its own account.");
|
|
51
|
+
print(` ${C.bold("huddle setup")} ${C.faint("installs and signs in the ones you pick (asks first). Or by hand:")}`);
|
|
52
|
+
for (const r of missing)
|
|
53
|
+
print(` ${dot(r.adapter.id)} ${C.bold(r.adapter.id.padEnd(12))} ${C.faint("$")} ${installLine(r.adapter)}`);
|
|
54
|
+
print(C.faint(" Then sign in with the agent itself — Huddle never sees your credentials —"));
|
|
55
|
+
print(C.faint(" and run `huddle doctor` again."));
|
|
52
56
|
return false;
|
|
53
57
|
}
|
|
54
58
|
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// `huddle lessons`: what Huddle learned across runs on this machine, and how
|
|
2
|
+
// to take a lesson back.
|
|
3
|
+
//
|
|
4
|
+
// huddle lessons list them, most-seen first
|
|
5
|
+
// huddle lessons --forget <id> drop one
|
|
6
|
+
// huddle lessons --clear [--yes] drop all (asks first)
|
|
7
|
+
import readline from "node:readline/promises";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import { C } from "./term.js";
|
|
10
|
+
import { allLessons, clearLessons, forgetLesson, lessonsFile, lessonsOff, repoKey } from "../room/lessons.js";
|
|
11
|
+
const ago = (ts) => {
|
|
12
|
+
const s = Math.max(0, Math.round((Date.now() - ts) / 1000));
|
|
13
|
+
return s < 90 ? `${s}s ago` : s < 5400 ? `${Math.round(s / 60)}m ago` : s < 129600 ? `${Math.round(s / 3600)}h ago` : `${Math.round(s / 86400)}d ago`;
|
|
14
|
+
};
|
|
15
|
+
function scopeLabel(l, here) {
|
|
16
|
+
if (l.scope === "all")
|
|
17
|
+
return "every run";
|
|
18
|
+
if (l.scope.startsWith("agent:"))
|
|
19
|
+
return l.scope.slice(6);
|
|
20
|
+
return here && l.scope === `repo:${here}` ? "this repo" : `repo ${l.scope.slice(5, 13)}`;
|
|
21
|
+
}
|
|
22
|
+
export async function runLessons(rest, cwd = process.cwd()) {
|
|
23
|
+
const file = lessonsFile().replace(os.homedir(), "~");
|
|
24
|
+
const fi = rest.indexOf("--forget");
|
|
25
|
+
if (fi >= 0) {
|
|
26
|
+
const id = rest[fi + 1];
|
|
27
|
+
if (!id || id.startsWith("--")) {
|
|
28
|
+
console.log(C.red("✗ --forget needs a lesson id") + C.faint(" (the first column of `huddle lessons`)"));
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (!forgetLesson(id)) {
|
|
33
|
+
console.log(C.red(`✗ no lesson ${id}`) + C.faint(" — `huddle lessons` lists them"));
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
return void console.log(`${C.green("✓")} forgot ${id}`);
|
|
38
|
+
}
|
|
39
|
+
if (rest.includes("--clear")) {
|
|
40
|
+
const n = allLessons().length;
|
|
41
|
+
if (!n)
|
|
42
|
+
return void console.log(C.faint("No lessons to clear."));
|
|
43
|
+
if (!rest.includes("--yes")) {
|
|
44
|
+
if (!process.stdin.isTTY) {
|
|
45
|
+
console.log(C.yellow(`! this deletes all ${n} lesson(s) in ${file}.`) + " Run it again with --yes to confirm.");
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
50
|
+
const a = (await rl.question(`Delete all ${n} lesson(s) in ${file}? ${C.faint("[y/N]")} `)).trim().toLowerCase();
|
|
51
|
+
rl.close();
|
|
52
|
+
if (!a.startsWith("y"))
|
|
53
|
+
return void console.log(C.faint("Kept them."));
|
|
54
|
+
}
|
|
55
|
+
clearLessons();
|
|
56
|
+
return void console.log(`${C.green("✓")} cleared ${n} lesson(s)`);
|
|
57
|
+
}
|
|
58
|
+
const lessons = allLessons().sort((a, b) => b.hits - a.hits || b.created - a.created);
|
|
59
|
+
if (lessonsOff())
|
|
60
|
+
console.log(C.yellow("! HUDDLE_NO_LESSONS is set: runs neither use nor learn lessons right now."));
|
|
61
|
+
if (!lessons.length) {
|
|
62
|
+
console.log(`No lessons yet ${C.faint(`(${file})`)}.`);
|
|
63
|
+
console.log(C.faint(" Huddle learns them at the end of each run — a council turn that timed out, a file agents collided on, QA bugs — and agents add them with huddle_remember(kind=\"lesson\")."));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
let here = null;
|
|
67
|
+
try {
|
|
68
|
+
here = repoKey(cwd);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
/* not in a repo */
|
|
72
|
+
}
|
|
73
|
+
console.log(`${C.violet("lessons")} ${C.faint(`· ${lessons.length} · ${file} · they go into agent and planner prompts`)}\n`);
|
|
74
|
+
for (const l of lessons) {
|
|
75
|
+
console.log(` ${C.bold(l.id)} ${l.text}`);
|
|
76
|
+
const used = l.last_used ? `used ${ago(l.last_used)}` : "not used yet";
|
|
77
|
+
console.log(C.faint(` ${scopeLabel(l, here)} · seen ${l.hits}× · ${used} · from ${l.source || "?"}${l.evidence ? ` · ${l.evidence}` : ""}`));
|
|
78
|
+
}
|
|
79
|
+
console.log(C.faint(`\n huddle lessons --forget <id> · --clear · HUDDLE_NO_LESSONS=1 turns them off`));
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=lessons.js.map
|